From 666438909d8478f8ce2750bf53da011a57f5e8fb Mon Sep 17 00:00:00 2001 From: yakhyo Date: Sat, 8 Nov 2025 01:15:25 +0900 Subject: [PATCH] improve logging system with verbose flag - silent by default (only warnings/errors) - add --verbose flag to all scripts - add enable_logging() function for library users - cleaner output for end users --- scripts/run_detection.py | 7 +- scripts/run_face_search.py | 45 ++++--- scripts/run_recognition.py | 5 + uniface/__init__.py | 3 +- uniface/detection/retinaface.py | 2 +- uniface/landmark/models.py | 6 +- uniface/log.py | 30 ++++- uniface/model_store.py | 224 ++++++++++++++++---------------- 8 files changed, 178 insertions(+), 144 deletions(-) diff --git a/scripts/run_detection.py b/scripts/run_detection.py index 8729395..b89a296 100644 --- a/scripts/run_detection.py +++ b/scripts/run_detection.py @@ -26,7 +26,7 @@ def run_inference(detector, image_path: str, vis_threshold: float = 0.6, save_di # 1. Get the list of face dictionaries from the detector faces = detector.detect(image) - + if faces: # 2. Unpack the data into separate lists bboxes = [face['bbox'] for face in faces] @@ -56,9 +56,14 @@ def main(): parser.add_argument("--threshold", type=float, default=0.6, help="Visualization confidence threshold") parser.add_argument("--iterations", type=int, default=1, help="Number of inference runs for benchmarking") parser.add_argument("--save_dir", type=str, default="outputs", help="Directory to save output images") + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") args = parser.parse_args() + if args.verbose: + from uniface import enable_logging + enable_logging() + print(f"Initializing detector: {args.method}") detector = create_detector(method=args.method) diff --git a/scripts/run_face_search.py b/scripts/run_face_search.py index e67c8bc..1f93e2c 100644 --- a/scripts/run_face_search.py +++ b/scripts/run_face_search.py @@ -1,11 +1,12 @@ -import cv2 import argparse + +import cv2 import numpy as np # Use the new high-level factory functions from uniface.detection import create_detector -from uniface.recognition import create_recognizer from uniface.face_utils import compute_similarity +from uniface.recognition import create_recognizer def extract_reference_embedding(detector, recognizer, image_path: str) -> np.ndarray: @@ -19,8 +20,8 @@ def extract_reference_embedding(detector, recognizer, image_path: str) -> np.nda raise RuntimeError("No faces found in reference image.") # Get landmarks from the first detected face dictionary - landmarks = np.array(faces[0]['landmarks']) - + landmarks = np.array(faces[0]["landmarks"]) + # Use normalized embedding for more reliable similarity comparison embedding = recognizer.get_normalized_embedding(image, landmarks) return embedding @@ -43,17 +44,17 @@ def run_video(detector, recognizer, ref_embedding: np.ndarray, threshold: float # Loop through each detected face for face in faces: # Extract bbox and landmarks from the dictionary - bbox = face['bbox'] - landmarks = np.array(face['landmarks']) - + bbox = face["bbox"] + landmarks = np.array(face["landmarks"]) + x1, y1, x2, y2 = map(int, bbox) - + # Get the normalized embedding for the current face embedding = recognizer.get_normalized_embedding(frame, landmarks) - + # Compare with the reference embedding sim = compute_similarity(ref_embedding, embedding) - + # Draw results label = f"Match ({sim:.2f})" if sim > threshold else f"Unknown ({sim:.2f})" color = (0, 255, 0) if sim > threshold else (0, 0, 255) @@ -61,7 +62,7 @@ def run_video(detector, recognizer, ref_embedding: np.ndarray, threshold: float cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imshow("Face Recognition", frame) - if cv2.waitKey(1) & 0xFF == ord('q'): + if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() @@ -72,30 +73,32 @@ def main(): parser = argparse.ArgumentParser(description="Face recognition using a reference image.") parser.add_argument("--image", type=str, required=True, help="Path to the reference face image.") parser.add_argument( - "--detector", - type=str, - default="scrfd", - choices=['retinaface', 'scrfd'], - help="Face detection method." + "--detector", type=str, default="scrfd", choices=["retinaface", "scrfd"], help="Face detection method." ) parser.add_argument( "--recognizer", type=str, default="arcface", - choices=['arcface', 'mobileface', 'sphereface'], - help="Face recognition method." + choices=["arcface", "mobileface", "sphereface"], + help="Face recognition method.", ) + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") args = parser.parse_args() + if args.verbose: + from uniface import enable_logging + + enable_logging() + print("Initializing models...") detector = create_detector(method=args.detector) recognizer = create_recognizer(method=args.recognizer) - + print("Extracting reference embedding...") ref_embedding = extract_reference_embedding(detector, recognizer, args.image) - + run_video(detector, recognizer, ref_embedding) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/scripts/run_recognition.py b/scripts/run_recognition.py index 9e3d140..b7bf8fc 100644 --- a/scripts/run_recognition.py +++ b/scripts/run_recognition.py @@ -60,9 +60,14 @@ def main(): choices=['arcface', 'mobileface', 'sphereface'], help="Face recognition method to use." ) + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") args = parser.parse_args() + if args.verbose: + from uniface import enable_logging + enable_logging() + print(f"Initializing detector: {args.detector}") detector = create_detector(method=args.detector) diff --git a/uniface/__init__.py b/uniface/__init__.py index c72a79b..351da5d 100644 --- a/uniface/__init__.py +++ b/uniface/__init__.py @@ -17,7 +17,7 @@ __version__ = "0.1.9" from uniface.face_utils import compute_similarity, face_alignment -from uniface.log import Logger +from uniface.log import Logger, enable_logging from uniface.model_store import verify_model_weights from uniface.visualization import draw_detections @@ -54,4 +54,5 @@ __all__ = [ "face_alignment", "verify_model_weights", "Logger", + "enable_logging", ] diff --git a/uniface/detection/retinaface.py b/uniface/detection/retinaface.py index cff370c..7957262 100644 --- a/uniface/detection/retinaface.py +++ b/uniface/detection/retinaface.py @@ -144,7 +144,7 @@ class RetinaFace(BaseDetector): metric (Literal["default", "max"]): Metric for ranking detections when `max_num` is limited. - "default": Prioritize detections closer to the image center. - "max": Prioritize detections with larger bounding box areas. - center_weight (float): Weight for penalizing detections farther from the image center + center_weight (float): Weight for penalizing detections farther from the image center when using the "default" metric. Defaults to 2.0. Returns: diff --git a/uniface/landmark/models.py b/uniface/landmark/models.py index 824727d..8dcd100 100644 --- a/uniface/landmark/models.py +++ b/uniface/landmark/models.py @@ -104,7 +104,7 @@ class Landmark106(BaseLandmarker): width, height = bbox[2] - bbox[0], bbox[3] - bbox[1] center = ((bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2) scale = self.input_size[0] / (max(width, height) * 1.5) - + aligned_face, transform_matrix = bbox_center_alignment(image, center, self.input_size[0], scale, 0.0) face_blob = cv2.dnn.blobFromImage( @@ -130,7 +130,7 @@ class Landmark106(BaseLandmarker): landmarks = predictions.reshape((-1, 2)) landmarks[:, 0:2] += 1 landmarks[:, 0:2] *= (self.input_size[0] // 2) - + inverse_matrix = cv2.invertAffineTransform(transform_matrix) landmarks = transform_points_2d(landmarks, inverse_matrix) return landmarks @@ -193,7 +193,7 @@ if __name__ == "__main__": for face in faces: # Extract the bounding box bbox = face['bbox'] - + # 4. Get landmarks for the current face using its bounding box landmarks = landmarker.get_landmarks(frame, bbox) diff --git a/uniface/log.py b/uniface/log.py index ab3d4fc..4c3c3b7 100644 --- a/uniface/log.py +++ b/uniface/log.py @@ -1,8 +1,28 @@ import logging -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(levelname)s - %(message)s", - datefmt="%Y-%m-%d %H:%M:%S" -) +# Create logger for uniface Logger = logging.getLogger("uniface") +Logger.setLevel(logging.WARNING) # Only show warnings/errors by default +Logger.addHandler(logging.NullHandler()) + + +def enable_logging(level=logging.INFO): + """ + Enable verbose logging for uniface. + + Args: + level: Logging level (logging.DEBUG, logging.INFO, etc.) + + Example: + >>> from uniface import enable_logging + >>> enable_logging() # Show INFO logs + """ + Logger.handlers.clear() + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S" + )) + Logger.addHandler(handler) + Logger.setLevel(level) + Logger.propagate = False diff --git a/uniface/model_store.py b/uniface/model_store.py index 4648aed..615c850 100644 --- a/uniface/model_store.py +++ b/uniface/model_store.py @@ -1,112 +1,112 @@ -# Copyright 2025 Yakhyokhuja Valikhujaev -# Author: Yakhyokhuja Valikhujaev -# GitHub: https://github.com/yakhyo - -import os -import hashlib -import requests -from tqdm import tqdm - -from uniface.log import Logger -import uniface.constants as const - - -__all__ = ['verify_model_weights'] - - -def verify_model_weights(model_name: str, root: str = '~/.uniface/models') -> str: - """ - Ensure model weights are present, downloading and verifying them using SHA-256 if necessary. - - Given a model identifier from an Enum class (e.g., `RetinaFaceWeights.MNET_V2`), this function checks if - the corresponding `.onnx` weight file exists locally. If not, it downloads the file from a predefined URL. - After download, the file’s integrity is verified using a SHA-256 hash. If verification fails, the file is deleted - and an error is raised. - - Args: - model_name (Enum): Model weight identifier (e.g., `RetinaFaceWeights.MNET_V2`, `ArcFaceWeights.RESNET`, etc.). - root (str, optional): Directory to store or locate the model weights. Defaults to '~/.uniface/models'. - - Returns: - str: Absolute path to the verified model weights file. - - Raises: - ValueError: If the model is unknown or SHA-256 verification fails. - ConnectionError: If downloading the file fails. - - Examples: - >>> from uniface.models import RetinaFaceWeights, verify_model_weights - >>> verify_model_weights(RetinaFaceWeights.MNET_V2) - '/home/user/.uniface/models/retinaface_mnet_v2.onnx' - - >>> verify_model_weights(RetinaFaceWeights.RESNET34, root='/custom/dir') - '/custom/dir/retinaface_r34.onnx' - """ - - root = os.path.expanduser(root) - os.makedirs(root, exist_ok=True) - - # Keep model_name as enum for dictionary lookup - url = const.MODEL_URLS.get(model_name) - if not url: - Logger.error(f"No URL found for model '{model_name}'") - raise ValueError(f"No URL found for model '{model_name}'") - - file_ext = os.path.splitext(url)[1] - model_path = os.path.normpath(os.path.join(root, f'{model_name.value}{file_ext}')) - - if not os.path.exists(model_path): - Logger.info(f"Downloading model '{model_name}' from {url}") - try: - download_file(url, model_path) - Logger.info(f"Successfully downloaded '{model_name}' to {model_path}") - except Exception as e: - Logger.error(f"Failed to download model '{model_name}': {e}") - raise ConnectionError(f"Download failed for '{model_name}'") - - expected_hash = const.MODEL_SHA256.get(model_name) - if expected_hash and not verify_file_hash(model_path, expected_hash): - os.remove(model_path) # Remove corrupted file - Logger.warning("Corrupted weight detected. Removing...") - raise ValueError(f"Hash mismatch for '{model_name}'. The file may be corrupted; please try downloading again.") - - return model_path - - -def download_file(url: str, dest_path: str) -> None: - """Download a file from a URL in chunks and save it to the destination path.""" - try: - response = requests.get(url, stream=True) - response.raise_for_status() - with open(dest_path, "wb") as file, tqdm( - desc=f"Downloading {dest_path}", - unit='B', - unit_scale=True, - unit_divisor=1024 - ) as progress: - for chunk in response.iter_content(chunk_size=const.CHUNK_SIZE): - if chunk: - file.write(chunk) - progress.update(len(chunk)) - except requests.RequestException as e: - raise ConnectionError(f"Failed to download file from {url}. Error: {e}") - - -def verify_file_hash(file_path: str, expected_hash: str) -> bool: - """Compute the SHA-256 hash of the file and compare it with the expected hash.""" - file_hash = hashlib.sha256() - with open(file_path, "rb") as f: - for chunk in iter(lambda: f.read(const.CHUNK_SIZE), b""): - file_hash.update(chunk) - actual_hash = file_hash.hexdigest() - if actual_hash != expected_hash: - Logger.warning(f"Expected hash: {expected_hash}, but got: {actual_hash}") - return actual_hash == expected_hash - - -if __name__ == "__main__": - model_names = [model.value for model in const.RetinaFaceWeights] - - # Download each model in the list - for model_name in model_names: - model_path = verify_model_weights(model_name) +# Copyright 2025 Yakhyokhuja Valikhujaev +# Author: Yakhyokhuja Valikhujaev +# GitHub: https://github.com/yakhyo + +import os +import hashlib +import requests +from tqdm import tqdm + +from uniface.log import Logger +import uniface.constants as const + + +__all__ = ['verify_model_weights'] + + +def verify_model_weights(model_name: str, root: str = '~/.uniface/models') -> str: + """ + Ensure model weights are present, downloading and verifying them using SHA-256 if necessary. + + Given a model identifier from an Enum class (e.g., `RetinaFaceWeights.MNET_V2`), this function checks if + the corresponding `.onnx` weight file exists locally. If not, it downloads the file from a predefined URL. + After download, the file’s integrity is verified using a SHA-256 hash. If verification fails, the file is deleted + and an error is raised. + + Args: + model_name (Enum): Model weight identifier (e.g., `RetinaFaceWeights.MNET_V2`, `ArcFaceWeights.RESNET`, etc.). + root (str, optional): Directory to store or locate the model weights. Defaults to '~/.uniface/models'. + + Returns: + str: Absolute path to the verified model weights file. + + Raises: + ValueError: If the model is unknown or SHA-256 verification fails. + ConnectionError: If downloading the file fails. + + Examples: + >>> from uniface.models import RetinaFaceWeights, verify_model_weights + >>> verify_model_weights(RetinaFaceWeights.MNET_V2) + '/home/user/.uniface/models/retinaface_mnet_v2.onnx' + + >>> verify_model_weights(RetinaFaceWeights.RESNET34, root='/custom/dir') + '/custom/dir/retinaface_r34.onnx' + """ + + root = os.path.expanduser(root) + os.makedirs(root, exist_ok=True) + + # Keep model_name as enum for dictionary lookup + url = const.MODEL_URLS.get(model_name) + if not url: + Logger.error(f"No URL found for model '{model_name}'") + raise ValueError(f"No URL found for model '{model_name}'") + + file_ext = os.path.splitext(url)[1] + model_path = os.path.normpath(os.path.join(root, f'{model_name.value}{file_ext}')) + + if not os.path.exists(model_path): + Logger.info(f"Downloading model '{model_name}' from {url}") + try: + download_file(url, model_path) + Logger.info(f"Successfully downloaded '{model_name}' to {model_path}") + except Exception as e: + Logger.error(f"Failed to download model '{model_name}': {e}") + raise ConnectionError(f"Download failed for '{model_name}'") + + expected_hash = const.MODEL_SHA256.get(model_name) + if expected_hash and not verify_file_hash(model_path, expected_hash): + os.remove(model_path) # Remove corrupted file + Logger.warning("Corrupted weight detected. Removing...") + raise ValueError(f"Hash mismatch for '{model_name}'. The file may be corrupted; please try downloading again.") + + return model_path + + +def download_file(url: str, dest_path: str) -> None: + """Download a file from a URL in chunks and save it to the destination path.""" + try: + response = requests.get(url, stream=True) + response.raise_for_status() + with open(dest_path, "wb") as file, tqdm( + desc=f"Downloading {dest_path}", + unit='B', + unit_scale=True, + unit_divisor=1024 + ) as progress: + for chunk in response.iter_content(chunk_size=const.CHUNK_SIZE): + if chunk: + file.write(chunk) + progress.update(len(chunk)) + except requests.RequestException as e: + raise ConnectionError(f"Failed to download file from {url}. Error: {e}") + + +def verify_file_hash(file_path: str, expected_hash: str) -> bool: + """Compute the SHA-256 hash of the file and compare it with the expected hash.""" + file_hash = hashlib.sha256() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(const.CHUNK_SIZE), b""): + file_hash.update(chunk) + actual_hash = file_hash.hexdigest() + if actual_hash != expected_hash: + Logger.warning(f"Expected hash: {expected_hash}, but got: {actual_hash}") + return actual_hash == expected_hash + + +if __name__ == "__main__": + model_names = [model.value for model in const.RetinaFaceWeights] + + # Download each model in the list + for model_name in model_names: + model_path = verify_model_weights(model_name)