Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,14 @@
"console": "integratedTerminal",
"justMyCode": false,
},
{
"name": "Multi anonymization",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/model_face/main_multi_anonymize.py",
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"justMyCode": false,
},
],
}
Binary file modified images/1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/4.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/5.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions model_face/detector/yolov8_face_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ def detect(self, srcimg: np.ndarray) -> sv.Detections:

# Perform inference on the image
det_xywh, det_conf, det_classid, landmarks = self.post_process(outputs, scale_h, scale_w, padh, padw)
if len(det_xywh) == 0:
return sv.Detections.empty()

# Padding : Increase width/height by +padding in px
det_xywh[:, 2:] += self.padding
Expand Down Expand Up @@ -180,12 +182,17 @@ def post_process(self, preds, scale_h, scale_w, padh, padw):
class_ids = class_ids[mask]
landmarks = landmarks[mask]

# Check : No detections
if len(bboxes_wh) == 0:
return np.array([]), np.array([]), np.array([]), np.array([])

indices = cv2.dnn.NMSBoxes(
bboxes_wh.tolist(),
confidences.tolist(),
self.conf_threshold,
self.iou_threshold,
).flatten() ## type: ignore

if len(indices) > 0:
mlvl_bboxes = bboxes_wh[indices]
confidences = confidences[indices]
Expand Down
28 changes: 0 additions & 28 deletions model_face/main.py → model_face/main_anonymize.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,6 @@ def configure_logging() -> None:
)


def main_detect() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--imgpath", type=str, default="images/1.jpg", help="image path")
parser.add_argument(
"--modelpath",
type=str,
default="zoo/yolov8n-face.onnx",
help="onnx filepath",
)
parser.add_argument("--confThreshold", default=0.20, type=float, help="class confidence")
parser.add_argument("--nmsThreshold", default=0.40, type=float, help="nms iou thresh")
args = parser.parse_args()

# Initialize YOLOv8_face object detector
source = cv2.imread(args.imgpath)

# Detect Objects
detector = YOLOv8FaceDetection(args.modelpath, conf_thres=args.confThreshold, iou_thres=args.nmsThreshold)
detections = detector.detect(source)

# Draw detections
annotated = source.copy()
annotator = sv.BoxAnnotator()
scene = annotator.annotate(scene=annotated, detections=detections)
cv2.imshow("YOLOv8 Face Detection", scene)
cv2.waitKey(0)


def main_anonymize() -> None:
"""Main function to run the detection."""
# Configure logging
Expand Down
49 changes: 49 additions & 0 deletions model_face/main_detection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import argparse
import logging

import cv2
import supervision as sv

from model_face.detector.yolov8_face_detector import YOLOv8FaceDetection

logger = logging.getLogger(__name__)


def configure_logging() -> None:
"""Configure logging for the application."""
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)


def main_detect() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--imgpath", type=str, default="images/1.jpg", help="image path")
parser.add_argument(
"--modelpath",
type=str,
default="zoo/yolov8n-face.onnx",
help="onnx filepath",
)
parser.add_argument("--confThreshold", default=0.20, type=float, help="class confidence")
parser.add_argument("--nmsThreshold", default=0.40, type=float, help="nms iou thresh")
args = parser.parse_args()

# Initialize YOLOv8_face object detector
source = cv2.imread(args.imgpath)

# Detect Objects
detector = YOLOv8FaceDetection(args.modelpath, conf_thres=args.confThreshold, iou_thres=args.nmsThreshold)
detections = detector.detect(source)

# Draw detections
annotated = source.copy()
annotator = sv.BoxAnnotator()
scene = annotator.annotate(scene=annotated, detections=detections)
cv2.imshow("YOLOv8 Face Detection", scene)
cv2.waitKey(0)


if __name__ == "__main__":
main_detect()
128 changes: 128 additions & 0 deletions model_face/main_multi_anonymize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import argparse
import logging
import os
from typing import Any

import cv2
import supervision as sv
from tqdm import tqdm # type: ignore
from yaya_tools.helpers.dataset import load_directory_images_annotatations # type: ignore

from model_face.detector.yolov8_face_detector import YOLOv8FaceDetection
from model_face.helpers.transformations import blur_box # type: ignore

logger = logging.getLogger(__name__)


def anonymize_file(detector: YOLOv8FaceDetection, image_name: str, source_directory: str) -> tuple[str, bool]:
"""Anonymize an image by detecting faces and applying a blur to the detected regions."""
try:
source_path = os.path.join(source_directory, image_name)
image = cv2.imread(source_path)
if image is None:
logger.error(f"Could not read image {image_name}")
return image_name, False

detections = detector.detect(image)

# Logging : How many faces detected
if detections == sv.Detections.empty():
logger.info("No faces on %s image.", image_name)
else:
logger.info(
"Detected %u faces inside %s image. Average conf %2.2f.",
len(detections),
image_name,
detections.confidence.mean(), # type: ignore
)

# Anonymize
anonymized_im = image
for xyxy in detections.xyxy:
anonymized_im = blur_box(image=anonymized_im, box=xyxy)

# Save : Only if inplace is set and found faces
if detections != sv.Detections.empty():
cv2.imwrite(source_path, anonymized_im)
logger.info(f"Saved anonymized image to {source_path}")

return image_name, True

except Exception as e:
logger.error(f"Error anonymizing image {image_name}: {e}")
return image_name, False


def multiprocess_anonymize(
detector: YOLOv8FaceDetection,
source_directory: str,
images_names: list[str],
pool_size: int = 5,
) -> tuple[list[str], list[str]]:
"""
Anonymizes images using a single loop instead of multiprocessing,
since the detector object cannot be pickled.
"""
sucess_files: list[str] = []
failed_files: list[str] = []
for image_name in tqdm(images_names, desc="Anonymizing images"):
result = anonymize_file(detector, image_name, source_directory)
if result[1]:
sucess_files.append(image_name)
else:
failed_files.append(image_name)

return sucess_files, failed_files


def configure_logging() -> None:
"""Configure logging for the application."""
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)


def main_anonymize() -> None:
"""Main function to run the detection."""
# Configure logging
configure_logging()

parser = argparse.ArgumentParser()
parser.add_argument("--images", type=str, default="images/", help="Directory of images to anonymize in place")
parser.add_argument(
"--modelpath",
type=str,
default="zoo/yolov8n-face.onnx",
help="onnx filepath",
)
parser.add_argument("--confThreshold", default=0.10, type=float, help="class confidence")
parser.add_argument("--nmsThreshold", default=0.40, type=float, help="nms iou thresh")
parser.add_argument("--pixelate", action="store_true", help="pixelate the face instead of blurring")
parser.add_argument("--padding", default=5, type=int, help="Bounding box padding size in px")
args = parser.parse_args()

# Initialize YOLOv8_face object detector
images_annotated: dict[str, Any] = load_directory_images_annotatations(args.images)

# Detector : Create
detector = YOLOv8FaceDetection(
args.modelpath,
conf_thres=args.confThreshold,
iou_thres=args.nmsThreshold,
padding=args.padding,
)

# Multiprocess : Anonymize
logger.info("Anonymizing images...")
sucess_files, failed_files = multiprocess_anonymize(
detector=detector,
source_directory=args.images,
images_names=list(images_annotated.keys()),
pool_size=5,
)
logger.info("Anonymization completed.")


if __name__ == "__main__":
main_anonymize()
9 changes: 7 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies = [
"dotenv>=0.9.9",
"opencv-python>=4.11.0.86",
"supervision>=0.25.1",
"yaya-tools",
]


Expand Down Expand Up @@ -91,7 +92,11 @@ markers = [
"gpu: marks tests that require GPU",
]

[tool.uv.sources]
yaya-tools = { url = "https://github.com/AISP-PL/yaya-tools/releases/download/v1.2.12/yaya_tools-1.1.0-py3-none-any.whl" }
Comment thread
folkien marked this conversation as resolved.

# Scripts
[project.scripts]
face-detect = "model_face.main:main_detect"
face-anonymize = "model_face.main:main_anonymize"
face-detect = "model_face.main_detection:main_detect"
face-anonymize = "model_face.main_anonymize:main_anonymize"
face-multi-anonymize = "model_face.main_multi_anonymize:main_anonymize"
Loading