diff --git a/docs/source/api_doc/detect/index.rst b/docs/source/api_doc/detect/index.rst
index 48a1360957d..1d45e9c5ac6 100644
--- a/docs/source/api_doc/detect/index.rst
+++ b/docs/source/api_doc/detect/index.rst
@@ -15,6 +15,7 @@ imgutils.detect
halfbody
hand
head
+ nudenet
person
text
visual
diff --git a/docs/source/api_doc/detect/nudenet.rst b/docs/source/api_doc/detect/nudenet.rst
new file mode 100644
index 00000000000..9b9400a7579
--- /dev/null
+++ b/docs/source/api_doc/detect/nudenet.rst
@@ -0,0 +1,14 @@
+imgutils.detect.nudenet
+==========================
+
+.. currentmodule:: imgutils.detect.nudenet
+
+.. automodule:: imgutils.detect.nudenet
+
+
+detect_with_nudenet
+------------------------------
+
+.. autofunction:: detect_with_nudenet
+
+
diff --git a/docs/source/api_doc/detect/nudenet_detect_benchmark.plot.py b/docs/source/api_doc/detect/nudenet_detect_benchmark.plot.py
new file mode 100644
index 00000000000..c79ca3f8c87
--- /dev/null
+++ b/docs/source/api_doc/detect/nudenet_detect_benchmark.plot.py
@@ -0,0 +1,34 @@
+import random
+
+from benchmark import BaseBenchmark, create_plot_cli
+from imgutils.detect import detect_with_nudenet
+
+
+class NudenetDetectBenchmark(BaseBenchmark):
+ def __init__(self):
+ BaseBenchmark.__init__(self)
+
+ def load(self):
+ from imgutils.detect.nudenet import _open_nudenet_yolo, _open_nudenet_nms
+ _ = _open_nudenet_yolo()
+ _ = _open_nudenet_nms()
+
+ def unload(self):
+ from imgutils.detect.nudenet import _open_nudenet_yolo, _open_nudenet_nms
+ _open_nudenet_yolo.cache_clear()
+ _open_nudenet_nms.cache_clear()
+
+ def run(self):
+ image_file = random.choice(self.all_images)
+ _ = detect_with_nudenet(image_file)
+
+
+if __name__ == '__main__':
+ create_plot_cli(
+ [
+ ('Nudenet', NudenetDetectBenchmark()),
+ ],
+ title='Benchmark for Anime NudeNet Detections',
+ run_times=10,
+ try_times=20,
+ )()
diff --git a/docs/source/api_doc/detect/nudenet_detect_benchmark.plot.py.svg b/docs/source/api_doc/detect/nudenet_detect_benchmark.plot.py.svg
new file mode 100644
index 00000000000..bd785288b2b
--- /dev/null
+++ b/docs/source/api_doc/detect/nudenet_detect_benchmark.plot.py.svg
@@ -0,0 +1,2237 @@
+
+
+
diff --git a/docs/source/api_doc/detect/nudenet_detect_demo.plot.py b/docs/source/api_doc/detect/nudenet_detect_demo.plot.py
new file mode 100644
index 00000000000..f5b612f24f1
--- /dev/null
+++ b/docs/source/api_doc/detect/nudenet_detect_demo.plot.py
@@ -0,0 +1,19 @@
+from imgutils.detect.nudenet import _LABELS, detect_with_nudenet
+from imgutils.detect.visual import detection_visualize
+from plot import image_plot
+
+
+def _detect(img, **kwargs):
+ return detection_visualize(img, detect_with_nudenet(img, **kwargs), _LABELS)
+
+
+if __name__ == '__main__':
+ image_plot(
+ (_detect('censor/nude_girl.png'), 'simple nude'),
+ (_detect('censor/simple_sex.jpg'), 'simple sex'),
+ (_detect('censor/complex_pose.jpg'), 'complex pose'),
+ (_detect('censor/complex_sex.jpg'), 'complex sex'),
+ columns=2,
+ figsize=(9, 9),
+ autocensor=False,
+ )
diff --git a/docs/source/api_doc/detect/nudenet_detect_demo.plot.py.svg b/docs/source/api_doc/detect/nudenet_detect_demo.plot.py.svg
new file mode 100644
index 00000000000..06e4689beac
--- /dev/null
+++ b/docs/source/api_doc/detect/nudenet_detect_demo.plot.py.svg
@@ -0,0 +1,398 @@
+
+
+
diff --git a/imgutils/detect/__init__.py b/imgutils/detect/__init__.py
index 2252e8641c8..fbe64d360e8 100644
--- a/imgutils/detect/__init__.py
+++ b/imgutils/detect/__init__.py
@@ -14,6 +14,7 @@
from .halfbody import detect_halfbody
from .hand import detect_hands
from .head import detect_heads
+from .nudenet import detect_with_nudenet
from .person import detect_person
from .text import detect_text
from .visual import detection_visualize
diff --git a/imgutils/detect/nudenet.py b/imgutils/detect/nudenet.py
new file mode 100644
index 00000000000..e5db39157db
--- /dev/null
+++ b/imgutils/detect/nudenet.py
@@ -0,0 +1,241 @@
+"""
+Overview:
+ This module provides functionality for detecting nudity in images using the NudeNet model.
+
+ The module includes functions for preprocessing images, running the NudeNet YOLO model,
+ applying non-maximum suppression (NMS), and postprocessing the results. It utilizes
+ ONNX models hosted on `deepghs/nudenet_onnx `_
+ for efficient inference. The original project is
+ `notAI-tech/NudeNet `_.
+
+ .. collapse:: Overview of NudeNet Detect (NSFW Warning!!!)
+
+ .. image:: nudenet_detect_demo.plot.py.svg
+ :align: center
+
+ The main function :func:`detect_with_nudenet` can be used to perform nudity detection on
+ given images, returning a list of bounding boxes, labels, and confidence scores.
+
+ This is an overall benchmark of all the nudenet models:
+
+ .. image:: nudenet_detect_benchmark.plot.py.svg
+ :align: center
+
+ .. note::
+
+ Here is a detailed list of labels from the NudeNet detection model and their respective meanings:
+
+ .. list-table::
+ :widths: 25 75
+ :header-rows: 1
+
+ * - Label
+ - Description
+ * - FEMALE_GENITALIA_COVERED
+ - Detects covered female genitalia in the image.
+ * - FACE_FEMALE
+ - Detects the face of a female in the image.
+ * - BUTTOCKS_EXPOSED
+ - Detects exposed buttocks in the image.
+ * - FEMALE_BREAST_EXPOSED
+ - Detects exposed female breasts in the image.
+ * - FEMALE_GENITALIA_EXPOSED
+ - Detects exposed female genitalia in the image.
+ * - MALE_BREAST_EXPOSED
+ - Detects exposed male breasts in the image.
+ * - ANUS_EXPOSED
+ - Detects exposed anus in the image.
+ * - FEET_EXPOSED
+ - Detects exposed feet in the image.
+ * - BELLY_COVERED
+ - Detects a covered belly in the image.
+ * - FEET_COVERED
+ - Detects covered feet in the image.
+ * - ARMPITS_COVERED
+ - Detects covered armpits in the image.
+ * - ARMPITS_EXPOSED
+ - Detects exposed armpits in the image.
+ * - FACE_MALE
+ - Detects the face of a male in the image.
+ * - BELLY_EXPOSED
+ - Detects an exposed belly in the image.
+ * - MALE_GENITALIA_EXPOSED
+ - Detects exposed male genitalia in the image.
+ * - ANUS_COVERED
+ - Detects a covered anus in the image.
+ * - FEMALE_BREAST_COVERED
+ - Detects covered female breasts in the image.
+ * - BUTTOCKS_COVERED
+ - Detects covered buttocks in the image.
+
+
+ .. note::
+
+ This module requires onnxruntime version 1.18 or higher.
+"""
+
+from functools import lru_cache
+from typing import Tuple, List
+
+import numpy as np
+from PIL import Image
+from hbutils.testing.requires.version import VersionInfo
+from huggingface_hub import hf_hub_download
+
+from imgutils.data import ImageTyping
+from imgutils.utils import open_onnx_model
+from ..data import load_image
+
+
+def _check_compatibility() -> bool:
+ """
+ Check if the installed onnxruntime version is compatible with NudeNet.
+
+ :raises EnvironmentError: If the onnxruntime version is less than 1.18.
+ """
+ import onnxruntime
+ if VersionInfo(onnxruntime.__version__) < '1.18':
+ raise EnvironmentError(f'Nudenet not supported on onnxruntime {onnxruntime.__version__}, '
+ f'please upgrade it to 1.18+ version.\n'
+ f'If you are running on CPU, use "pip install -U onnxruntime" .\n'
+ f'If you are running on GPU, use "pip install -U onnxruntime-gpu" .') # pragma: no cover
+
+
+_REPO_ID = 'deepghs/nudenet_onnx'
+
+
+@lru_cache()
+def _open_nudenet_yolo():
+ """
+ Open and cache the NudeNet YOLO ONNX model.
+
+ :return: The loaded ONNX model for YOLO.
+ """
+ return open_onnx_model(hf_hub_download(
+ repo_id=_REPO_ID,
+ repo_type='model',
+ filename='320n.onnx',
+ ))
+
+
+@lru_cache()
+def _open_nudenet_nms():
+ """
+ Open and cache the NudeNet NMS ONNX model.
+
+ :return: The loaded ONNX model for NMS.
+ """
+ return open_onnx_model(hf_hub_download(
+ repo_id=_REPO_ID,
+ repo_type='model',
+ filename='nms-yolov8.onnx',
+ ))
+
+
+def _nn_preprocessing(image: ImageTyping, model_size: int = 320) -> Tuple[np.ndarray, float]:
+ """
+ Preprocess the input image for the NudeNet model.
+
+ :param image: The input image.
+ :param model_size: The size to which the image should be resized (default: 320).
+ :return: A tuple containing the preprocessed image array and the scaling ratio.
+ """
+ image = load_image(image, mode='RGB', force_background='white')
+ assert image.mode == 'RGB'
+ mat = np.array(image)
+
+ max_size = max(image.width, image.height)
+
+ mat_pad = np.zeros((max_size, max_size, 3), dtype=np.uint8)
+ mat_pad[:mat.shape[0], :mat.shape[1], :] = mat
+ img_resized = Image.fromarray(mat_pad, mode='RGB').resize((model_size, model_size), resample=Image.BILINEAR)
+
+ input_data = np.array(img_resized).transpose(2, 0, 1).astype(np.float32) / 255.0
+ input_data = np.expand_dims(input_data, axis=0)
+ return input_data, max_size / model_size
+
+
+def _make_np_config(topk: int = 100, iou_threshold: float = 0.45, score_threshold: float = 0.25) -> np.ndarray:
+ """
+ Create a configuration array for the NMS model.
+
+ :param topk: The maximum number of detections to keep (default: 100).
+ :param iou_threshold: The IoU threshold for NMS (default: 0.45).
+ :param score_threshold: The score threshold for detections (default: 0.25).
+ :return: A numpy array containing the configuration parameters.
+ """
+ return np.array([topk, iou_threshold, score_threshold]).astype(np.float32)
+
+
+def _nn_postprocess(selected, global_ratio: float):
+ """
+ Postprocess the model output to generate bounding boxes and labels.
+
+ :param selected: The output from the NMS model.
+ :param global_ratio: The scaling ratio to apply to the bounding boxes.
+ :return: A list of tuples, each containing a bounding box, label, and confidence score.
+ """
+ bboxes = []
+ num_boxes = selected.shape[0]
+ for idx in range(num_boxes):
+ data = selected[idx, :]
+
+ scores = data[4:]
+ score = np.max(scores)
+ label = np.argmax(scores)
+
+ box = data[:4] * global_ratio
+ x = (box[0] - 0.5 * box[2]).item()
+ y = (box[1] - 0.5 * box[3]).item()
+ w = box[2].item()
+ h = box[3].item()
+
+ bboxes.append(((x, y, x + w, y + h), _LABELS[label], score.item()))
+
+ return bboxes
+
+
+_LABELS = [
+ "FEMALE_GENITALIA_COVERED",
+ "FACE_FEMALE",
+ "BUTTOCKS_EXPOSED",
+ "FEMALE_BREAST_EXPOSED",
+ "FEMALE_GENITALIA_EXPOSED",
+ "MALE_BREAST_EXPOSED",
+ "ANUS_EXPOSED",
+ "FEET_EXPOSED",
+ "BELLY_COVERED",
+ "FEET_COVERED",
+ "ARMPITS_COVERED",
+ "ARMPITS_EXPOSED",
+ "FACE_MALE",
+ "BELLY_EXPOSED",
+ "MALE_GENITALIA_EXPOSED",
+ "ANUS_COVERED",
+ "FEMALE_BREAST_COVERED",
+ "BUTTOCKS_COVERED"
+]
+
+
+def detect_with_nudenet(image: ImageTyping, topk: int = 100,
+ iou_threshold: float = 0.45, score_threshold: float = 0.25) \
+ -> List[Tuple[Tuple[int, int, int, int], str, float]]:
+ """
+ Detect nudity in the given image using the NudeNet model.
+
+ :param image: The input image to analyze.
+ :param topk: The maximum number of detections to keep (default: 100).
+ :param iou_threshold: The IoU threshold for NMS (default: 0.45).
+ :param score_threshold: The score threshold for detections (default: 0.25).
+ :return: A list of tuples, each containing:
+
+ - A bounding box as (x1, y1, x2, y2)
+ - A label string
+ - A confidence score
+ """
+ _check_compatibility()
+ input_, global_ratio = _nn_preprocessing(image, model_size=320)
+ config = _make_np_config(topk, iou_threshold, score_threshold)
+ output0, = _open_nudenet_yolo().run(['output0'], {'images': input_})
+ selected, = _open_nudenet_nms().run(['selected'], {'detection': output0, 'config': config})
+ return _nn_postprocess(selected[0], global_ratio=global_ratio)
diff --git a/imgutils/detect/text.py b/imgutils/detect/text.py
index 28208b70c0c..b097c821140 100644
--- a/imgutils/detect/text.py
+++ b/imgutils/detect/text.py
@@ -118,7 +118,7 @@ def _get_bounding_box_of_text(image: ImageTyping, model: str, threshold: float)
return bboxes
-@deprecated(deprecated_in="0.2.10", removed_in="0.4", current_version=__VERSION__,
+@deprecated(deprecated_in="0.2.10", removed_in="0.5", current_version=__VERSION__,
details="Use the new function :func:`imgutils.ocr.detect_text_with_ocr` instead")
def detect_text(image: ImageTyping, model: str = _DEFAULT_MODEL, threshold: float = 0.05,
max_area_size: Optional[int] = 640):
diff --git a/test/detect/test_nudenet.py b/test/detect/test_nudenet.py
new file mode 100644
index 00000000000..1a8ff6ed964
--- /dev/null
+++ b/test/detect/test_nudenet.py
@@ -0,0 +1,85 @@
+import pytest
+from PIL import Image
+
+from imgutils.detect import detect_with_nudenet
+from imgutils.detect.nudenet import _open_nudenet_nms, _open_nudenet_yolo
+from ..testings import get_testfile
+
+
+@pytest.fixture(scope='module', autouse=True)
+def _release_model_after_run():
+ try:
+ yield
+ finally:
+ _open_nudenet_yolo.cache_clear()
+ _open_nudenet_nms.cache_clear()
+
+
+@pytest.fixture()
+def nude_girl_file():
+ return get_testfile('nude_girl.png')
+
+
+@pytest.fixture()
+def nude_girl_image(nude_girl_file):
+ return Image.open(nude_girl_file)
+
+
+@pytest.fixture()
+def nude_girl_detection():
+ return [
+ ((321.3878631591797, 242.3542022705078, 429.8410186767578, 345.7248992919922),
+ 'FEMALE_BREAST_EXPOSED',
+ 0.832775890827179),
+ ((207.8404312133789, 243.68451690673828, 307.2947006225586, 336.3175582885742),
+ 'FEMALE_BREAST_EXPOSED',
+ 0.8057667016983032),
+ ((203.23711395263672,
+ 348.42012786865234,
+ 351.32117462158203,
+ 511.34781646728516),
+ 'BELLY_EXPOSED',
+ 0.7703637480735779),
+ ((280.81117248535156,
+ 678.6565170288086,
+ 436.11827087402344,
+ 767.8816909790039),
+ 'FEET_EXPOSED',
+ 0.747696578502655),
+ ((185.25140380859375, 518.0437889099121, 252.96240234375, 625.8465919494629),
+ 'FEMALE_GENITALIA_EXPOSED',
+ 0.7381105422973633),
+ ((287.9706840515137, 124.07051467895508, 392.7693061828613, 225.3848991394043),
+ 'FACE_FEMALE',
+ 0.6556487083435059),
+ ((103.20288848876953,
+ 564.7838439941406,
+ 352.05843353271484,
+ 707.6390075683594),
+ 'BUTTOCKS_EXPOSED',
+ 0.44306617975234985),
+ ((396.1982898712158, 224.24786376953125, 450.53956413269043, 290.279541015625),
+ 'ARMPITS_EXPOSED',
+ 0.31386712193489075)
+ ]
+
+
+@pytest.mark.unittest
+class TestDetectNudeNet:
+ def test_detect_with_nudenet_file(self, nude_girl_file, nude_girl_detection):
+ detection = detect_with_nudenet(nude_girl_file)
+ assert [label for _, label, _ in detection] == \
+ [label for _, label, _ in nude_girl_detection]
+ for (actual_box, _, _), (expected_box, _, _) in zip(detection, nude_girl_detection):
+ assert actual_box == pytest.approx(expected_box)
+ assert [score for _, _, score in detection] == \
+ pytest.approx([score for _, _, score in nude_girl_detection], abs=1e-4)
+
+ def test_detect_with_nudenet_image(self, nude_girl_image, nude_girl_detection):
+ detection = detect_with_nudenet(nude_girl_image)
+ assert [label for _, label, _ in detection] == \
+ [label for _, label, _ in nude_girl_detection]
+ for (actual_box, _, _), (expected_box, _, _) in zip(detection, nude_girl_detection):
+ assert actual_box == pytest.approx(expected_box)
+ assert [score for _, _, score in detection] == \
+ pytest.approx([score for _, _, score in nude_girl_detection], abs=1e-4)