Skip to content

Commit 4afcfc0

Browse files
committed
added the DetectorForNSFW node.
1 parent 6a26442 commit 4afcfc0

File tree

10 files changed

+481
-6
lines changed

10 files changed

+481
-6
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,12 @@ Added the node for convenience. The code is originally from ComfyUI-Custom-Scrip
6767
According to the input image ratio, decide which standard SDXL training size is the closest match. This is useful for subsequent image resizing and other processes.
6868

6969
## UpscaleImageWithModelIfNeed
70-
Enhanced the official UpscaleImageWithModel node by adding a judge. If the input image area exceeds a predefined threshold, upscaling is bypassed. The threshold is a percentage of the SDXL standard size (1024x1024) area.
70+
Enhanced the official UpscaleImageWithModel node by adding a judge. If the input image area exceeds a predefined threshold, upscaling is bypassed. The threshold is a percentage of the SDXL standard size (1024x1024) area.
71+
72+
## DetectorForNSFW
73+
This node adapts the original model and inference code from [nudenet](https://github.com/notAI-tech/NudeNet.git) for use with Comfy. A small 10MB default model, [320n.onnx] (https://github.com/notAI-tech/NudeNet?tab=readme-ov-file#available-models), is provided. If you wish to use other models from that repository, download the [ONNX model] (https://github.com/notAI-tech/NudeNet?tab=readme-ov-file#available-models) and place it in the models/nsfw directory, then set the appropriate detect_size.
74+
75+
From initial testing, the filtering effect is better than classifier models such as Falconsai/nsfw_image_detection.
76+
77+
<img src="assets/detectorForNSFW.png" width="100%"/>
78+
You can also adjust the confidence levels for various rules such as buttocks_exposed to be more lenient or strict. Lower confidence levels will filter out more potential NSFW images. Setting the value to 1 will stop filtering for that specific feature.

__init__.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import logging
44
logger = logging.getLogger(__file__)
55
import os
6+
import importlib.util
67
import shutil,filecmp
78
import __main__
89

9-
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS, GenderWordsConfig
10-
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
10+
from .py.nodes import GenderWordsConfig
1111

1212

1313
@server.PromptServer.instance.routes.get("/utils_node/reload_gender_words_config")
@@ -45,4 +45,34 @@ def update_javascript():
4545
shutil.copy(src_file, dst_file)
4646

4747

48-
update_javascript()
48+
update_javascript()
49+
50+
51+
NODE_CLASS_MAPPINGS = {}
52+
NODE_DISPLAY_NAME_MAPPINGS = {}
53+
54+
def get_ext_dir(subpath=None, mkdir=False):
55+
dir = os.path.dirname(__file__)
56+
if subpath is not None:
57+
dir = os.path.join(dir, subpath)
58+
59+
dir = os.path.abspath(dir)
60+
61+
if mkdir and not os.path.exists(dir):
62+
os.makedirs(dir)
63+
return dir
64+
65+
py = get_ext_dir("py")
66+
files = os.listdir(py)
67+
for file in files:
68+
if not file.endswith(".py"):
69+
continue
70+
name = os.path.splitext(file)[0]
71+
imported_module = importlib.import_module(".py.{}".format(name), __name__)
72+
try:
73+
NODE_CLASS_MAPPINGS = {**NODE_CLASS_MAPPINGS, **imported_module.NODE_CLASS_MAPPINGS}
74+
NODE_DISPLAY_NAME_MAPPINGS = {**NODE_DISPLAY_NAME_MAPPINGS, **imported_module.NODE_DISPLAY_NAME_MAPPINGS}
75+
except:
76+
logger.info(f"import module failure:{name}")
77+
78+
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]

assets/detectorForNSFW.png

332 KB
Loading
File renamed without changes.

py/node_nsfw.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import cv2
2+
import numpy as np
3+
from ..r_nudenet.nudenet import NudeDetector
4+
import os
5+
import torch
6+
import folder_paths as comfy_paths
7+
from folder_paths import models_dir
8+
from typing import Union, List
9+
import json
10+
import logging
11+
logger = logging.getLogger(__file__)
12+
13+
comfy_paths.folder_names_and_paths["nsfw"] = ([os.path.join(models_dir, "nsfw")], {".pt",".onnx"})
14+
15+
def tensor2np(tensor: torch.Tensor):
16+
if len(tensor.shape) == 3: # Single image
17+
return np.clip(255.0 * tensor.cpu().numpy(), 0, 255).astype(np.uint8)
18+
else: # Batch of images
19+
return [np.clip(255.0 * t.cpu().numpy(), 0, 255).astype(np.uint8) for t in tensor]
20+
21+
def np2tensor(img_np: Union[np.ndarray, List[np.ndarray]]) -> torch.Tensor:
22+
if isinstance(img_np, list):
23+
return torch.cat([np2tensor(img) for img in img_np], dim=0)
24+
return torch.from_numpy(img_np.astype(np.float32) / 255.0).unsqueeze(0)
25+
26+
27+
class DetectorForNSFW:
28+
29+
def __init__(self) -> None:
30+
self.model = None
31+
32+
@classmethod
33+
def INPUT_TYPES(cls):
34+
return {
35+
"required": {
36+
"image": ("IMAGE",),
37+
"detect_size":([640, 320], {"default": 320}),
38+
"provider": (["CPU", "CUDA", "ROCM"], ),
39+
},
40+
"optional": {
41+
"model_name": (comfy_paths.get_filename_list("nsfw"), {"default": None}),
42+
"alternative_image": ("IMAGE",),
43+
"buttocks_exposed": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.05}),
44+
"female_breast_exposed": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.05}),
45+
"female_genitalia_exposed": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05}),
46+
"anus_exposed": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.05}),
47+
"male_genitalia_exposed": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05}),
48+
},
49+
}
50+
51+
RETURN_TYPES = ("IMAGE", "STRING")
52+
RETURN_NAMES = ("filtered_image", "detect_result")
53+
FUNCTION = "filter_exposure"
54+
55+
CATEGORY = "utils/filter"
56+
57+
all_labels = [
58+
"FEMALE_GENITALIA_COVERED",
59+
"FACE_FEMALE",
60+
"BUTTOCKS_EXPOSED",
61+
"FEMALE_BREAST_EXPOSED",
62+
"FEMALE_GENITALIA_EXPOSED",
63+
"MALE_BREAST_EXPOSED",
64+
"ANUS_EXPOSED",
65+
"FEET_EXPOSED",
66+
"BELLY_COVERED",
67+
"FEET_COVERED",
68+
"ARMPITS_COVERED",
69+
"ARMPITS_EXPOSED",
70+
"FACE_MALE",
71+
"BELLY_EXPOSED",
72+
"MALE_GENITALIA_EXPOSED",
73+
"ANUS_COVERED",
74+
"FEMALE_BREAST_COVERED",
75+
"BUTTOCKS_COVERED",
76+
]
77+
78+
def filter_exposure(self, image, model_name=None, detect_size=320, provider="CPU", alternative_image=None, **kwargs):
79+
if self.model is None:
80+
self.init_model(model_name, detect_size, provider)
81+
82+
if alternative_image is not None:
83+
alternative_image = tensor2np(alternative_image)
84+
85+
images = tensor2np(image)
86+
if not isinstance(images, List):
87+
images = [images]
88+
89+
results, result_info = [],[]
90+
for img in images:
91+
detect_result = self.model.detect(img)
92+
93+
logger.debug(f"nudenet detect result:{detect_result}")
94+
filtered_results = []
95+
for item in detect_result:
96+
label = item['class']
97+
score = item['score']
98+
confidence_level = kwargs.get(label.lower())
99+
if label.lower() in kwargs and score > confidence_level:
100+
filtered_results.append(item)
101+
info = {"detect_result":detect_result}
102+
if len(filtered_results) == 0:
103+
results.append(img)
104+
info["nsfw"] = False
105+
else:
106+
placeholder_image = alternative_image if alternative_image is not None else np.ones_like(img) * 255
107+
results.append(placeholder_image)
108+
info["nsfw"] = True
109+
110+
result_info.append(info)
111+
112+
result_tensor = np2tensor(results)
113+
result_info = json.dumps(result_info)
114+
return (result_tensor, result_info,)
115+
116+
def init_model(self, model_name, detect_size, provider):
117+
model_path = comfy_paths.get_full_path("nsfw", model_name) if model_name else None
118+
self.model = NudeDetector(model_path=model_path, providers=[provider + 'ExecutionProvider',], inference_resolution=detect_size)
119+
120+
121+
NODE_CLASS_MAPPINGS = {
122+
#image
123+
"DetectorForNSFW": DetectorForNSFW,
124+
125+
}
126+
127+
NODE_DISPLAY_NAME_MAPPINGS = {
128+
# Image
129+
"DetectorForNSFW": "detector for the NSFW",
130+
131+
}

nodes.py renamed to py/nodes.py

File renamed without changes.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "comfyui-utils-nodes"
3-
description = "Nodes:LoadImageWithSwitch, ImageBatchOneOrMore, ModifyTextGender, GenderControlOutput, ImageCompositeMaskedWithSwitch, ColorCorrectOfUtils, SplitMask, MaskFastGrow, CheckpointLoaderSimpleWithSwitch, ImageResizeTo8x, MatchImageRatioToPreset, UpscaleImageWithModelIfNeed, MaskFromFaceModel, MaskCoverFourCorners etc."
4-
version = "1.1.3"
3+
description = "Nodes:LoadImageWithSwitch, ImageBatchOneOrMore, ModifyTextGender, GenderControlOutput, ImageCompositeMaskedWithSwitch, ColorCorrectOfUtils, SplitMask, MaskFastGrow, CheckpointLoaderSimpleWithSwitch, ImageResizeTo8x, MatchImageRatioToPreset, UpscaleImageWithModelIfNeed, MaskFromFaceModel, MaskCoverFourCorners, DetectorForNSFW etc."
4+
version = "1.1.5"
55
license = { file = "LICENSE" }
66
dependencies = []
77

r_nudenet/320n.onnx

11.6 MB
Binary file not shown.

0 commit comments

Comments
 (0)