diff --git a/pyproject.toml b/pyproject.toml index f065cb7..f9c9d70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ dependencies = [ requires-python = ">=3.10" [project.optional-dependencies] -dev = ["pytest>=7.0.0"] +dev = ["pytest>=7.0.0", "ruff>=0.4.0"] gpu = ["onnxruntime-gpu>=1.16.0"] [project.urls] @@ -35,3 +35,13 @@ packages = { find = {} } [tool.setuptools.package-data] "uniface" = ["*.txt", "*.md"] + +[tool.ruff] +line-length = 120 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "I", "W"] + +[tool.ruff.lint.isort] +known-first-party = ["uniface"] diff --git a/scripts/batch_process.py b/scripts/batch_process.py index ed67a41..d67d21e 100644 --- a/scripts/batch_process.py +++ b/scripts/batch_process.py @@ -33,7 +33,15 @@ def process_image(detector, image_path: Path, output_path: Path, threshold: floa landmarks = [f["landmarks"] for f in faces] draw_detections(image, bboxes, scores, landmarks, vis_threshold=threshold) - cv2.putText(image, f"Faces: {len(faces)}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) + cv2.putText( + image, + f"Faces: {len(faces)}", + (10, 30), + cv2.FONT_HERSHEY_SIMPLEX, + 1, + (0, 255, 0), + 2, + ) cv2.imwrite(str(output_path), image) return len(faces) diff --git a/scripts/run_age_gender.py b/scripts/run_age_gender.py index 126c176..3681cb5 100644 --- a/scripts/run_age_gender.py +++ b/scripts/run_age_gender.py @@ -21,7 +21,13 @@ def draw_age_gender_label(image, bbox, gender: str, age: int): cv2.putText(image, text, (x1 + 5, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2) -def process_image(detector, age_gender, image_path: str, save_dir: str = "outputs", threshold: float = 0.6): +def process_image( + detector, + age_gender, + image_path: str, + save_dir: str = "outputs", + threshold: float = 0.6, +): image = cv2.imread(image_path) if image is None: print(f"Error: Failed to load image from '{image_path}'") @@ -75,7 +81,15 @@ def run_webcam(detector, age_gender, threshold: float = 0.6): gender, age = age_gender.predict(frame, face["bbox"]) # predict per face draw_age_gender_label(frame, face["bbox"], gender, age) - cv2.putText(frame, f"Faces: {len(faces)}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) + cv2.putText( + frame, + f"Faces: {len(faces)}", + (10, 30), + cv2.FONT_HERSHEY_SIMPLEX, + 1, + (0, 255, 0), + 2, + ) cv2.imshow("Age & Gender Detection", frame) if cv2.waitKey(1) & 0xFF == ord("q"): diff --git a/scripts/run_detection.py b/scripts/run_detection.py index 192872b..d946864 100644 --- a/scripts/run_detection.py +++ b/scripts/run_detection.py @@ -53,7 +53,15 @@ def run_webcam(detector, threshold: float = 0.6): landmarks = [f["landmarks"] for f in faces] draw_detections(frame, bboxes, scores, landmarks, vis_threshold=threshold) - cv2.putText(frame, f"Faces: {len(faces)}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) + cv2.putText( + frame, + f"Faces: {len(faces)}", + (10, 30), + cv2.FONT_HERSHEY_SIMPLEX, + 1, + (0, 255, 0), + 2, + ) cv2.imshow("Face Detection", frame) if cv2.waitKey(1) & 0xFF == ord("q"): diff --git a/scripts/run_face_search.py b/scripts/run_face_search.py index e230438..3a026a2 100644 --- a/scripts/run_face_search.py +++ b/scripts/run_face_search.py @@ -76,7 +76,12 @@ def main(): parser.add_argument("--image", type=str, required=True, help="Reference face image") parser.add_argument("--threshold", type=float, default=0.4, help="Match threshold") parser.add_argument("--detector", type=str, default="scrfd", choices=["retinaface", "scrfd"]) - parser.add_argument("--recognizer", type=str, default="arcface", choices=["arcface", "mobileface", "sphereface"]) + parser.add_argument( + "--recognizer", + type=str, + default="arcface", + choices=["arcface", "mobileface", "sphereface"], + ) args = parser.parse_args() detector = RetinaFace() if args.detector == "retinaface" else SCRFD() diff --git a/scripts/run_landmarks.py b/scripts/run_landmarks.py index 4e8869a..3f979a7 100644 --- a/scripts/run_landmarks.py +++ b/scripts/run_landmarks.py @@ -34,7 +34,15 @@ def process_image(detector, landmarker, image_path: str, save_dir: str = "output for x, y in landmarks.astype(int): cv2.circle(image, (x, y), 1, (0, 255, 0), -1) - cv2.putText(image, f"Face {i + 1}", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) + cv2.putText( + image, + f"Face {i + 1}", + (x1, y1 - 10), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (0, 255, 0), + 2, + ) os.makedirs(save_dir, exist_ok=True) output_path = os.path.join(save_dir, f"{Path(image_path).stem}_landmarks.jpg") @@ -67,7 +75,15 @@ def run_webcam(detector, landmarker): for x, y in landmarks.astype(int): cv2.circle(frame, (x, y), 1, (0, 255, 0), -1) - cv2.putText(frame, f"Faces: {len(faces)}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) + cv2.putText( + frame, + f"Faces: {len(faces)}", + (10, 30), + cv2.FONT_HERSHEY_SIMPLEX, + 1, + (0, 255, 0), + 2, + ) cv2.imshow("106-Point Landmarks", frame) if cv2.waitKey(1) & 0xFF == ord("q"): diff --git a/scripts/run_recognition.py b/scripts/run_recognition.py index 18f839e..48c0b94 100644 --- a/scripts/run_recognition.py +++ b/scripts/run_recognition.py @@ -79,7 +79,12 @@ def main(): parser.add_argument("--image2", type=str, help="Second image for comparison") parser.add_argument("--threshold", type=float, default=0.35, help="Similarity threshold") parser.add_argument("--detector", type=str, default="retinaface", choices=["retinaface", "scrfd"]) - parser.add_argument("--recognizer", type=str, default="arcface", choices=["arcface", "mobileface", "sphereface"]) + parser.add_argument( + "--recognizer", + type=str, + default="arcface", + choices=["arcface", "mobileface", "sphereface"], + ) args = parser.parse_args() detector = RetinaFace() if args.detector == "retinaface" else SCRFD() diff --git a/scripts/run_video_detection.py b/scripts/run_video_detection.py index de3547d..a9988f4 100644 --- a/scripts/run_video_detection.py +++ b/scripts/run_video_detection.py @@ -11,7 +11,13 @@ from uniface import SCRFD, RetinaFace from uniface.visualization import draw_detections -def process_video(detector, input_path: str, output_path: str, threshold: float = 0.6, show_preview: bool = False): +def process_video( + detector, + input_path: str, + output_path: str, + threshold: float = 0.6, + show_preview: bool = False, +): cap = cv2.VideoCapture(input_path) if not cap.isOpened(): print(f"Error: Cannot open video file '{input_path}'") @@ -51,7 +57,15 @@ def process_video(detector, input_path: str, output_path: str, threshold: float landmarks = [f["landmarks"] for f in faces] draw_detections(frame, bboxes, scores, landmarks, vis_threshold=threshold) - cv2.putText(frame, f"Faces: {len(faces)}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) + cv2.putText( + frame, + f"Faces: {len(faces)}", + (10, 30), + cv2.FONT_HERSHEY_SIMPLEX, + 1, + (0, 255, 0), + 2, + ) out.write(frame) if show_preview: diff --git a/tests/test_age_gender.py b/tests/test_age_gender.py index 9816995..34c0092 100644 --- a/tests/test_age_gender.py +++ b/tests/test_age_gender.py @@ -31,7 +31,7 @@ def test_prediction_output_format(age_gender_model, mock_image, mock_bbox): def test_gender_values(age_gender_model, mock_image, mock_bbox): gender, age = age_gender_model.predict(mock_image, mock_bbox) - assert gender in ['Male', 'Female'], f"Gender should be 'Male' or 'Female', got '{gender}'" + assert gender in ["Male", "Female"], f"Gender should be 'Male' or 'Female', got '{gender}'" def test_age_range(age_gender_model, mock_image, mock_bbox): @@ -48,7 +48,7 @@ def test_different_bbox_sizes(age_gender_model, mock_image): for bbox in test_bboxes: gender, age = age_gender_model.predict(mock_image, bbox) - assert gender in ['Male', 'Female'], f"Failed for bbox {bbox}" + assert gender in ["Male", "Female"], f"Failed for bbox {bbox}" assert 0 <= age <= 120, f"Age out of range for bbox {bbox}" @@ -58,7 +58,7 @@ def test_different_image_sizes(age_gender_model, mock_bbox): for size in test_sizes: mock_image = np.random.randint(0, 255, size, dtype=np.uint8) gender, age = age_gender_model.predict(mock_image, mock_bbox) - assert gender in ['Male', 'Female'], f"Failed for image size {size}" + assert gender in ["Male", "Female"], f"Failed for image size {size}" assert 0 <= age <= 120, f"Age out of range for image size {size}" @@ -73,14 +73,14 @@ def test_consistency(age_gender_model, mock_image, mock_bbox): def test_bbox_list_format(age_gender_model, mock_image): bbox_list = [100, 100, 300, 300] gender, age = age_gender_model.predict(mock_image, bbox_list) - assert gender in ['Male', 'Female'], "Should work with bbox as list" + assert gender in ["Male", "Female"], "Should work with bbox as list" assert 0 <= age <= 120, "Age should be in valid range" def test_bbox_array_format(age_gender_model, mock_image): bbox_array = np.array([100, 100, 300, 300]) gender, age = age_gender_model.predict(mock_image, bbox_array) - assert gender in ['Male', 'Female'], "Should work with bbox as numpy array" + assert gender in ["Male", "Female"], "Should work with bbox as numpy array" assert 0 <= age <= 120, "Age should be in valid range" @@ -98,7 +98,7 @@ def test_multiple_predictions(age_gender_model, mock_image): assert len(results) == 3, "Should have 3 predictions" for gender, age in results: - assert gender in ['Male', 'Female'] + assert gender in ["Male", "Female"] assert 0 <= age <= 120 diff --git a/tests/test_factory.py b/tests/test_factory.py index 911fe48..9f30ba2 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -16,7 +16,7 @@ def test_create_detector_retinaface(): """ Test creating a RetinaFace detector using factory function. """ - detector = create_detector('retinaface') + detector = create_detector("retinaface") assert detector is not None, "Failed to create RetinaFace detector" @@ -24,7 +24,7 @@ def test_create_detector_scrfd(): """ Test creating a SCRFD detector using factory function. """ - detector = create_detector('scrfd') + detector = create_detector("scrfd") assert detector is not None, "Failed to create SCRFD detector" @@ -33,10 +33,10 @@ def test_create_detector_with_config(): Test creating detector with custom configuration. """ detector = create_detector( - 'retinaface', + "retinaface", model_name=RetinaFaceWeights.MNET_V2, conf_thresh=0.8, - nms_thresh=0.3 + nms_thresh=0.3, ) assert detector is not None, "Failed to create detector with custom config" @@ -46,18 +46,14 @@ def test_create_detector_invalid_method(): Test that invalid detector method raises an error. """ with pytest.raises((ValueError, KeyError)): - create_detector('invalid_method') + create_detector("invalid_method") def test_create_detector_scrfd_with_model(): """ Test creating SCRFD detector with specific model. """ - detector = create_detector( - 'scrfd', - model_name=SCRFDWeights.SCRFD_10G_KPS, - conf_thresh=0.5 - ) + detector = create_detector("scrfd", model_name=SCRFDWeights.SCRFD_10G_KPS, conf_thresh=0.5) assert detector is not None, "Failed to create SCRFD with specific model" @@ -66,7 +62,7 @@ def test_create_recognizer_arcface(): """ Test creating an ArcFace recognizer using factory function. """ - recognizer = create_recognizer('arcface') + recognizer = create_recognizer("arcface") assert recognizer is not None, "Failed to create ArcFace recognizer" @@ -74,7 +70,7 @@ def test_create_recognizer_mobileface(): """ Test creating a MobileFace recognizer using factory function. """ - recognizer = create_recognizer('mobileface') + recognizer = create_recognizer("mobileface") assert recognizer is not None, "Failed to create MobileFace recognizer" @@ -82,7 +78,7 @@ def test_create_recognizer_sphereface(): """ Test creating a SphereFace recognizer using factory function. """ - recognizer = create_recognizer('sphereface') + recognizer = create_recognizer("sphereface") assert recognizer is not None, "Failed to create SphereFace recognizer" @@ -91,7 +87,7 @@ def test_create_recognizer_invalid_method(): Test that invalid recognizer method raises an error. """ with pytest.raises((ValueError, KeyError)): - create_recognizer('invalid_method') + create_recognizer("invalid_method") # create_landmarker tests @@ -99,7 +95,7 @@ def test_create_landmarker(): """ Test creating a Landmark106 detector using factory function. """ - landmarker = create_landmarker('2d106det') + landmarker = create_landmarker("2d106det") assert landmarker is not None, "Failed to create Landmark106 detector" @@ -116,7 +112,7 @@ def test_create_landmarker_invalid_method(): Test that invalid landmarker method raises an error. """ with pytest.raises((ValueError, KeyError)): - create_landmarker('invalid_method') + create_landmarker("invalid_method") # detect_faces tests @@ -125,7 +121,7 @@ def test_detect_faces_retinaface(): Test high-level detect_faces function with RetinaFace. """ mock_image = np.random.randint(0, 255, (640, 640, 3), dtype=np.uint8) - faces = detect_faces(mock_image, method='retinaface') + faces = detect_faces(mock_image, method="retinaface") assert isinstance(faces, list), "detect_faces should return a list" @@ -135,7 +131,7 @@ def test_detect_faces_scrfd(): Test high-level detect_faces function with SCRFD. """ mock_image = np.random.randint(0, 255, (640, 640, 3), dtype=np.uint8) - faces = detect_faces(mock_image, method='scrfd') + faces = detect_faces(mock_image, method="scrfd") assert isinstance(faces, list), "detect_faces should return a list" @@ -145,13 +141,13 @@ def test_detect_faces_with_threshold(): Test detect_faces with custom confidence threshold. """ mock_image = np.random.randint(0, 255, (640, 640, 3), dtype=np.uint8) - faces = detect_faces(mock_image, method='retinaface', conf_thresh=0.8) + faces = detect_faces(mock_image, method="retinaface", conf_thresh=0.8) assert isinstance(faces, list), "detect_faces should return a list" # All detections should respect threshold for face in faces: - assert face['confidence'] >= 0.8, "All detections should meet confidence threshold" + assert face["confidence"] >= 0.8, "All detections should meet confidence threshold" def test_detect_faces_default_method(): @@ -169,7 +165,7 @@ def test_detect_faces_empty_image(): Test detect_faces on a blank image. """ empty_image = np.zeros((640, 640, 3), dtype=np.uint8) - faces = detect_faces(empty_image, method='retinaface') + faces = detect_faces(empty_image, method="retinaface") assert isinstance(faces, list), "Should return a list even for empty image" assert len(faces) == 0, "Should detect no faces in blank image" @@ -193,8 +189,8 @@ def test_list_available_detectors_contents(): detectors = list_available_detectors() # Should include at least these detectors - assert 'retinaface' in detectors, "Should include 'retinaface'" - assert 'scrfd' in detectors, "Should include 'scrfd'" + assert "retinaface" in detectors, "Should include 'retinaface'" + assert "scrfd" in detectors, "Should include 'scrfd'" # Integration tests @@ -202,7 +198,7 @@ def test_detector_inference_from_factory(): """ Test that detector created from factory can perform inference. """ - detector = create_detector('retinaface') + detector = create_detector("retinaface") mock_image = np.random.randint(0, 255, (640, 640, 3), dtype=np.uint8) faces = detector.detect(mock_image) @@ -213,7 +209,7 @@ def test_recognizer_inference_from_factory(): """ Test that recognizer created from factory can perform inference. """ - recognizer = create_recognizer('arcface') + recognizer = create_recognizer("arcface") mock_image = np.random.randint(0, 255, (112, 112, 3), dtype=np.uint8) embedding = recognizer.get_embedding(mock_image) @@ -225,7 +221,7 @@ def test_landmarker_inference_from_factory(): """ Test that landmarker created from factory can perform inference. """ - landmarker = create_landmarker('2d106det') + landmarker = create_landmarker("2d106det") mock_image = np.random.randint(0, 255, (640, 640, 3), dtype=np.uint8) mock_bbox = [100, 100, 300, 300] @@ -238,8 +234,8 @@ def test_multiple_detector_creation(): """ Test that multiple detectors can be created independently. """ - detector1 = create_detector('retinaface') - detector2 = create_detector('scrfd') + detector1 = create_detector("retinaface") + detector2 = create_detector("scrfd") assert detector1 is not None assert detector2 is not None @@ -250,8 +246,8 @@ def test_detector_with_different_configs(): """ Test creating multiple detectors with different configurations. """ - detector_high_thresh = create_detector('retinaface', conf_thresh=0.9) - detector_low_thresh = create_detector('retinaface', conf_thresh=0.3) + detector_high_thresh = create_detector("retinaface", conf_thresh=0.9) + detector_low_thresh = create_detector("retinaface", conf_thresh=0.3) mock_image = np.random.randint(0, 255, (640, 640, 3), dtype=np.uint8) @@ -269,9 +265,9 @@ def test_factory_returns_correct_types(): """ from uniface import RetinaFace, ArcFace, Landmark106 - detector = create_detector('retinaface') - recognizer = create_recognizer('arcface') - landmarker = create_landmarker('2d106det') + detector = create_detector("retinaface") + recognizer = create_recognizer("arcface") + landmarker = create_landmarker("2d106det") assert isinstance(detector, RetinaFace), "Should return RetinaFace instance" assert isinstance(recognizer, ArcFace), "Should return ArcFace instance" diff --git a/tests/test_recognition.py b/tests/test_recognition.py index 91d373c..769b55c 100644 --- a/tests/test_recognition.py +++ b/tests/test_recognition.py @@ -41,13 +41,16 @@ def mock_landmarks(): """ Create mock 5-point facial landmarks. """ - return np.array([ - [38.2946, 51.6963], - [73.5318, 51.5014], - [56.0252, 71.7366], - [41.5493, 92.3655], - [70.7299, 92.2041] - ], dtype=np.float32) + return np.array( + [ + [38.2946, 51.6963], + [73.5318, 51.5014], + [56.0252, 71.7366], + [41.5493, 92.3655], + [70.7299, 92.2041], + ], + dtype=np.float32, + ) # ArcFace Tests @@ -173,8 +176,7 @@ def test_different_models_different_embeddings(arcface_model, mobileface_model, # Embeddings should be different (with high probability for random input) # We check that they're not identical - assert not np.allclose(arcface_emb, mobileface_emb), \ - "Different models should produce different embeddings" + assert not np.allclose(arcface_emb, mobileface_emb), "Different models should produce different embeddings" def test_embedding_similarity_computation(arcface_model, mock_aligned_face): @@ -191,6 +193,7 @@ def test_embedding_similarity_computation(arcface_model, mock_aligned_face): # Compute cosine similarity from uniface import compute_similarity + similarity = compute_similarity(emb1, emb2) # Similarity should be between -1 and 1 @@ -205,6 +208,7 @@ def test_same_face_high_similarity(arcface_model, mock_aligned_face): emb2 = arcface_model.get_embedding(mock_aligned_face) from uniface import compute_similarity + similarity = compute_similarity(emb1, emb2) # Same image should have similarity close to 1.0 diff --git a/tests/test_utils.py b/tests/test_utils.py index 653fa7f..15aa745 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -18,13 +18,16 @@ def mock_landmarks(): Create mock 5-point facial landmarks. Standard positions for a face roughly centered at (112/2, 112/2). """ - return np.array([ - [38.2946, 51.6963], # Left eye - [73.5318, 51.5014], # Right eye - [56.0252, 71.7366], # Nose - [41.5493, 92.3655], # Left mouth corner - [70.7299, 92.2041] # Right mouth corner - ], dtype=np.float32) + return np.array( + [ + [38.2946, 51.6963], # Left eye + [73.5318, 51.5014], # Right eye + [56.0252, 71.7366], # Nose + [41.5493, 92.3655], # Left mouth corner + [70.7299, 92.2041], # Right mouth corner + ], + dtype=np.float32, + ) # compute_similarity tests @@ -166,7 +169,7 @@ def test_face_alignment_landmarks_as_list(mock_image): [73.5318, 51.5014], [56.0252, 71.7366], [41.5493, 92.3655], - [70.7299, 92.2041] + [70.7299, 92.2041], ] # Convert list to numpy array before passing to face_alignment @@ -201,9 +204,18 @@ def test_face_alignment_from_different_positions(mock_image): """ # Landmarks at different positions positions = [ - np.array([[100, 100], [150, 100], [125, 130], [110, 150], [140, 150]], dtype=np.float32), - np.array([[300, 200], [350, 200], [325, 230], [310, 250], [340, 250]], dtype=np.float32), - np.array([[500, 400], [550, 400], [525, 430], [510, 450], [540, 450]], dtype=np.float32), + np.array( + [[100, 100], [150, 100], [125, 130], [110, 150], [140, 150]], + dtype=np.float32, + ), + np.array( + [[300, 200], [350, 200], [325, 230], [310, 250], [340, 250]], + dtype=np.float32, + ), + np.array( + [[500, 400], [550, 400], [525, 430], [510, 450], [540, 450]], + dtype=np.float32, + ), ] for landmarks in positions: @@ -216,13 +228,16 @@ def test_face_alignment_landmark_count(mock_image): Test that face_alignment works specifically with 5-point landmarks. """ # Standard 5-point landmarks - landmarks_5pt = np.array([ - [38.2946, 51.6963], - [73.5318, 51.5014], - [56.0252, 71.7366], - [41.5493, 92.3655], - [70.7299, 92.2041] - ], dtype=np.float32) + landmarks_5pt = np.array( + [ + [38.2946, 51.6963], + [73.5318, 51.5014], + [56.0252, 71.7366], + [41.5493, 92.3655], + [70.7299, 92.2041], + ], + dtype=np.float32, + ) aligned, _ = face_alignment(mock_image, landmarks_5pt, image_size=(112, 112)) assert aligned.shape == (112, 112, 3), "Should work with 5-point landmarks"