feat: 降低 NSFW 检测阈值,显示更多检测结果

This commit is contained in:
2025-07-15 21:11:04 +08:00
parent f589592535
commit 8ac21e6941
4 changed files with 86 additions and 50 deletions
+73 -27
View File
@@ -22,17 +22,17 @@ import json
import os
import shutil
import sys
import tempfile
import threading
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import cv2
import numpy as np
from nudenet import NudeDetector
from nudenet.nudenet import _read_image
# 只报 OpenCV 致命错误,别让 imread 的 WARN 刷屏(也避免误导排查)
try:
import cv2
cv2.setLogLevel(8) # LOG_LEVEL_ERROR
except Exception:
pass
@@ -145,6 +145,74 @@ def _new_detector():
return NudeDetector()
def _raw_detect(det, data):
"""直接调用 ONNX 推理,用 0.05 低阈值后处理(NudeNet 内置的 detect 用 0.2 太严格)。
绕过 NudeDetector.detect() 的硬编码 0.2 阈值,让低置信度的检测结果也能返回,
最终判定交给 Go 侧的用户配置阈值。
"""
mat = cv2.imdecode(np.frombuffer(data, np.uint8), -1)
if mat is None:
raise OSError("图片解码失败")
mat_c3 = cv2.cvtColor(mat, cv2.COLOR_RGBA2BGR)
ow, oh = mat_c3.shape[1], mat_c3.shape[0]
max_size = max(ow, oh)
x_pad = max_size - ow
y_pad = max_size - oh
x_ratio = max_size / ow
y_ratio = max_size / oh
mat_pad = cv2.copyMakeBorder(mat_c3, 0, y_pad, 0, x_pad, cv2.BORDER_CONSTANT)
blob = cv2.dnn.blobFromImage(mat_pad, 1 / 255.0, (det.input_width, det.input_height), (0, 0, 0), swapRB=True, crop=False)
outputs = det.onnx_session.run(None, {det.input_name: blob})
outs = np.transpose(np.squeeze(outputs[0]))
rows = outs.shape[0]
boxes, scores, class_ids = [], [], []
for i in range(rows):
cls_scores = outs[i][4:]
max_score = np.amax(cls_scores)
if max_score >= 0.05: # 低阈值:NudeNet 内置是 0.2,这里降到 0.05
class_id = int(np.argmax(cls_scores))
x, y, w, h = outs[i][0:4]
x, y = x - w / 2, y - h / 2
w_ = w * (ow + x_pad) / det.input_width
h_ = h * (oh + y_pad) / det.input_height
x = x * (ow + x_pad) / det.input_width
y = y * (oh + y_pad) / det.input_height
x, y = max(0, min(x, ow)), max(0, min(y, oh))
w_ = min(w_, ow - x)
h_ = min(h_, oh - y)
boxes.append([x, y, w_, h_])
scores.append(float(max_score))
class_ids.append(class_id)
indices = cv2.dnn.NMSBoxes(boxes, scores, 0.1, 0.45)
detections = []
for i in indices:
idx = i if isinstance(i, int) else i[0]
detections.append({
"class": __labels[class_ids[idx]],
"score": scores[idx],
"box": [int(boxes[idx][0]), int(boxes[idx][1]), int(boxes[idx][2]), int(boxes[idx][3])],
})
return detections
# NudeNet 的标签表(和 nudenet.nudenet.__labels 一致)
__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 _longpath(path):
"""Windows 下给绝对路径加 \\\\?\\ 前缀,让 Python 原生 open 也能处理超长路径。"""
if os.name != "nt":
@@ -171,39 +239,17 @@ def _read_bytes(path):
return data
def _detect_bytes(det, data, path):
"""把图片字节喂给 NudeNet。v3 直接吃 bytes;老版 v2 只收路径时落到临时短路径重试。
用线程自己的检测器实例推理,各实例互不干扰,可以真·并行。
"""
try:
return det.detect(data)
except (TypeError, AttributeError):
tmp = None
try:
fd, tmp = tempfile.mkstemp(suffix=os.path.splitext(path)[1] or ".jpg")
with os.fdopen(fd, "wb") as f:
f.write(data)
return det.detect(tmp)
finally:
if tmp:
try:
os.unlink(tmp)
except OSError:
pass
def _infer(path):
"""整个推理任务(读文件 + 检测),提交到固定线程池执行。
读文件在池外并行进行;推理用线程各自的独立检测器,N 个 worker 真·并行。
使用 _raw_detect 绕过 NudeNet 内置的 0.2 阈值,用 0.05 低阈值返回更多结果。
"""
data = _read_bytes(path)
det = getattr(_thread_local, "detector", None)
if det is None:
det = _new_detector()
_thread_local.detector = det
return _detect_bytes(det, data, path)
return _raw_detect(det, data)
class Handler(BaseHTTPRequestHandler):