Open Solmyr1982 opened 9 months ago
same problem
Huh, the model is the same. It is possible that there is some mistake in the pre processing code.
Let me re-check this. Will update here.
Thank you @bedapudi6788. I tried to run the web app locally, using the same model files as used in Python code (best.onnx and nms-yolov8.onnx), and got exactly the same results. Now I'm trying to check the pre-processing code (probably by using the blob from JS in the Python code, omitting the _read_image function) but it's not my area of knowledge, I'm not sure how far and how fast I can progress. I'll share any findings if I have them.
Hey, thanks, i found the issue, testing locally. Will update the version in a bit.
https://github.com/notAI-tech/NudeNet/commit/b5cbff72b5503107e670504d683d07168d263875
i think this should fix it. image pre-processing with aspect ratio being preserved is added. new version pushed to pypi.
Thank you. It indeed works better, but still not as accurate as the web version, unfortunately.
Hmm, may be some pre procesaing steps still not the same. I will try to see where the difference is.
In some cases, the difference is pretty high, with more than 50% probability from the web app and nothing from the Python code:
Could you email me some of the images? If possible? praneeth at bpraneeth.com
It's sent.
I'm getting the same results using the same image on both tests:
Web results from https://nudenet.notai.tech:
Local docker (NO GENITALIA DETECTED):
{
"prediction": [
[
{
"class": "MALE_BREAST_EXPOSED",
"score": 0.5861272811889648,
"box": [
238,
69,
73,
35
]
},
{
"class": "ARMPITS_EXPOSED",
"score": 0.5417417287826538,
"box": [
371,
78,
46,
22
]
},
{
"class": "BELLY_EXPOSED",
"score": 0.527684211730957,
"box": [
257,
104,
87,
39
]
}
]
],
"success": true
}
I haven't yet gotten around to debugging this. will need to look at the pre-process ops in details. may be this or coming weeked, I will and update.
I haven't yet gotten around to debugging this. will need to look at the pre-process ops in details. may be this or coming weeked, I will and update.
Thank you! it would be great to have same results as your web example :)
@bedapudi6788 I sent you a link to test different images and see the results, check your email, thanks!
@AlexUrrutia got your email.
not sure why the results are different from https://nudenet.notai.tech/
the web example, runs nudedetector in-browser (i.e: model is loaded in your browser itself in javascript)
Your webpage is calling the python module in the backend right? There are some pre-processing differences between python and js implementations, which is causing the difference in results.
You will be able to re-produce the same results with the in-browser version
About the difference in pre-processing, I will fix it, i.e: python and js versions working same, when I get some free time.
Yes the python module is being called from the webpage, now I understand why the difference, hope it gets fixed soon :) thanks a lot!
web page code has lower max score / scores than the python implementation so it picks up more. Lower the scores in the python code and you should get same results. that worked for me.
web page code has lower max score / scores than the python implementation so it picks up more. Lower the scores in the python code and you should get same results. that worked for me.
Can you provide a sample of the scores you've used? Thanks
oh yaa, that might be the reason. in web page it's 0.4 and 0.25. let me check and update the package.
oh yaa, that might be the reason. in web page it's 0.4 and 0.25. let me check and update the package.
@bedapudi6788 Please let me know when docker image is updated so I can test again, thanks!
@Solmyr1982 Did you get it to work?
@bedapudi6788 @mustbeinsane @Solmyr1982 Issues using the latest docker instance:
{"prediction": [[{"class": "FEMALE_BREAST_EXPOSED", "score": 0.6442430019378662, "box": [330, 328, 235, 256]}, {"class": "FEMALE_BREAST_EXPOSED", "score": 0.5400181412696838, "box": [336, 588, 254, 318]}]], "success": true}
{"prediction": [[{"class": "FACE_FEMALE", "score": 0.6978989243507385, "box": [195, 260, 341, 825]}, {"class": "MALE_GENITALIA_EXPOSED", "score": 0.6808986663818359, "box": [114, 832, 255, 447]}]], "success": true}
and so on ...
You can test it here: LINK
@Solmyr1982 Did you get it to work?
Not really, I tried the solution mentioned by @mustbeinsane but it doesn't work, the problem seems to be somewhere else. Hopefully, @bedapudi6788 will have some spare time to debug and find where this difference is coming from.
I updated the code with almost same as pre-processing as web version. tried to match it wherever possible. result scores seem to be very close (but not exactly same).
One observation is, the original pytorch results seem better than both. I will add a different version of nudenet based on the pytorch model.
I updated the code with almost same as pre-processing as web version. tried to match it wherever possible. result scores seem to be very close (but not exactly same).
One observation is, the original pytorch results seem better than both. I will add a different version of nudenet based on the pytorch model.
Ok cool, I’m going to test the docker container and post results here, thanks!
@AlexUrrutia wait 10 min to start your testing. the new docker container based on 3.0.8 is being pushed.
@AlexUrrutia wait 10 min to start your testing. the new docker container based on 3.0.8 is being pushed.
OK, I was testing your web demo and I got these results (is that normal?):
yupp, it has false positives sometimes. I am also training a bigger model which will have better accuracy. Will update the repo in couple of days.
Thank you for the update @bedapudi6788 . Could you please let me know, regarding the bigger model you mentioned - you mean to share this model in a couple of days?
Just a brief explanation of my situation: I have around 300000 photos, and I have a self-made solution that randomly shows the photos via web browser - as a result, I'm using an old iPad as a digital photo frame (though it could be any device that can run a web browser). Taking into account the number of photos, it's impossible to go through them manually and detect all the unsafe ones. Therefore I started to look for a solution and found yours, which works really well. In my case, I'd rather wait days or weeks for the more precise model since I want to reduce the chance of having any unsafe photo randomly shown in my living room as much as possible.
Few more things I noticed while creating a code for crawling through my gallery (probably outside of this bug):
If you rotate the photo 90, 180, 270 and try using the NudeDetector on each iteration, it significantly increases the chances of finding an unsafe photo. The downside - you're processing four pictures instead of one, but it worth it.
Moreover, if you split the photo into four equal pieces (just by cutting it horizontally and vertically), and try to use NudeNet on each part, it also increases the chances.
As a result, it works a way longer, but the precision is becoming higher.
Maybe it could be an option to include those improvements in the NudeNet itself, but as I mentioned, the performance isn't that good.
any progress on this? i am commonly getting false positives/negatives using the python install, hoping when this is fixed its going to resolve it :)
too many false detect
Hello there, any updates on this bug? Thanks for The hard work ✅
no solution yet?
Also, if we wanted to train the model further, which model is this using? I see that under docs, there are two files: the yolov8, and something called best.onnx. What model is best.onnx?
Just going to weigh in on this as well - a few things: I wanted to be able to process HEIC images, I used pillow - but the conversion between HEIC and jpeg causes issues for the detection - not sure if this is the GRB2RGB issue or not - haven't been able to make it work reliably - also some older rotated images are not actuality rotated - I second the re-detection on rotated images to get the best score.
Lastly I second the web version being much more accurate - have been trying to figure it out, but to no luck. Would love to get a python version working with greater accuracy
Hello, thanks for the work.
I have discovered what causes the problem, the cv2.resize() function in _read_image, after going through it and saving it as an image, the web version does not detect the same boxes.
I have replaced it with the PIL resize and it works perfectly
def _read_image(image_path, target_size=320):
if isinstance(image_path, str):
img = cv2.imread(image_path)
elif isinstance(image_path, np.ndarray):
img = image_path
else:
raise ValueError('please make sure the image_path is str or np.ndarray')
img_height, img_width = img.shape[:2]
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
aspect = img_width / img_height
if img_height > img_width:
new_height = target_size
new_width = int(round(target_size * aspect))
else:
new_width = target_size
new_height = int(round(target_size / aspect))
resize_factor = math.sqrt(
(img_width**2 + img_height**2) / (new_width**2 + new_height**2)
)
#img = cv2.resize(img, (new_width, new_height))
#img = resizetoCV2(img, (new_width, new_height))
#pad_x = target_size - new_width
#pad_y = target_size - new_height
#pad_top, pad_bottom = [int(i) for i in np.floor([pad_y, pad_y]) / 2]
#pad_left, pad_right = [int(i) for i in np.floor([pad_x, pad_x]) / 2]
#img = cv2.copyMakeBorder(
# img,
# pad_top,
# pad_bottom,
# pad_left,
# pad_right,
# cv2.BORDER_CONSTANT,
# value=[0, 0, 0],
#)
pad_left = int((target_size - new_width) /2)
pad_top = int((target_size - new_height) /2)
#img = cv2.resize(img, (target_size, target_size))
img = resizetoCV2(img, (target_size, target_size))
image_data = img.astype("float32") / 255.0 # normalize
image_data = np.transpose(image_data, (2, 0, 1))
image_data = np.expand_dims(image_data, axis=0)
return image_data, resize_factor, pad_left, pad_top
def resizetoCV2(img_or, new_size):
if isinstance(img_or, str):
PILimg = PIL.Image.open(img_or)
elif isinstance(img_or, np.ndarray):
PILimg = PIL.Image.fromarray(img_or)
PILimg.thumbnail(new_size, PIL.Image.Resampling.LANCZOS)
#PILimg = PILimg.convert('RGB')
# img = np.array(PILimg)
# Convert RGB to BGR
# img =img[:, :, ::-1].copy()
new_im = PIL.Image.new("RGB", new_size)
box = tuple((n - o) // 2 for n, o in zip(new_size, PILimg.size))
new_im.paste(PILimg, box)
return np.array(new_im)
With this modification it gives me the same results in both environments.
Greetings.
P.S Sorry but my English is not very good and I have to go to Google Translate
@Serrallonga25 that's awesome! thank you for the insight. I will push an updated version.
@Serrallonga25 that's awesome! thank you for the insight. I will push an updated version.
Great! Hope to get the fixed version on Docker soon, thanks
Hello @bedapudi6788 , I'm testing https://nudenet.notai.tech/ vs Python on my server and got this results after using the code provided by @Serrallonga25:
Python output with same image:
[ { "box": [217, 194, 267, 303], "class": "FACE_MALE", "score": 0.638852000236511 }, { "box": [4, 738, 607, 533], "class": "MALE_GENITALIA_EXPOSED", "score": 0.574285209178925 } ]
nudenet.py
from flask import Flask, request, jsonify
import os
import math
import cv2
import numpy as np
import urllib.request
import onnxruntime
from onnxruntime.capi import _pybind_state as C
import PIL.Image
app = Flask(__name__)
__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 _read_image(image_path, target_size=320):
if isinstance(image_path, str):
img = cv2.imread(image_path)
elif isinstance(image_path, np.ndarray):
img = image_path
else:
raise ValueError('please make sure the image_path is str or np.ndarray')
img_height, img_width = img.shape[:2]
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
aspect = img_width / img_height
if img_height > img_width:
new_height = target_size
new_width = int(round(target_size * aspect))
else:
new_width = target_size
new_height = int(round(target_size / aspect))
resize_factor = math.sqrt(
(img_width**2 + img_height**2) / (new_width**2 + new_height**2)
)
# pad_left = int((target_size - new_width) / 2)
# pad_top = int((target_size - new_height) / 2)
# img = resizetoCV2(img, (target_size, target_size))
# image_data = img.astype("float32") / 255.0 # normalize
# image_data = np.transpose(image_data, (2, 0, 1))
# image_data = np.expand_dims(image_data, axis=0)
# return image_data, resize_factor, pad_left, pad_top
pad_left = int((target_size - new_width) /2)
pad_top = int((target_size - new_height) /2)
#img = cv2.resize(img, (target_size, target_size))
img = resizetoCV2(img, (target_size, target_size))
image_data = img.astype("float32") / 255.0 # normalize
image_data = np.transpose(image_data, (2, 0, 1))
image_data = np.expand_dims(image_data, axis=0)
return image_data, resize_factor, pad_left, pad_top
def resizetoCV2(img_or, new_size):
if isinstance(img_or, str):
PILimg = Image.open(img_or)
elif isinstance(img_or, np.ndarray):
PILimg = PIL.Image.fromarray(img_or)
else:
raise ValueError('please make sure the img_or is str or np.ndarray')
#PILimg.thumbnail(new_size, Image.LANCZOS) # Use Image.LANCZOS for compatibility
PILimg.thumbnail(new_size, PIL.Image.Resampling.LANCZOS)
new_im = PIL.Image.new("RGB", new_size)
box = tuple((n - o) // 2 for n, o in zip(new_size, PILimg.size))
new_im.paste(PILimg, box)
return np.array(new_im)
def _postprocess(output, resize_factor, pad_left, pad_top):
outputs = np.transpose(np.squeeze(output[0]))
rows = outputs.shape[0]
boxes = []
scores = []
class_ids = []
for i in range(rows):
classes_scores = outputs[i][4:]
max_score = np.amax(classes_scores)
if max_score >= 0.2:
class_id = np.argmax(classes_scores)
x, y, w, h = outputs[i][0], outputs[i][1], outputs[i][2], outputs[i][3]
left = int(round((x - w * 0.5 - pad_left) * resize_factor))
top = int(round((y - h * 0.5 - pad_top) * resize_factor))
width = int(round(w * resize_factor))
height = int(round(h * resize_factor))
class_ids.append(class_id)
scores.append(max_score)
boxes.append([left, top, width, height])
indices = cv2.dnn.NMSBoxes(boxes, scores, 0.25, 0.45)
detections = []
for i in indices:
box = boxes[i]
score = scores[i]
class_id = class_ids[i]
detections.append(
{"class": __labels[class_id], "score": float(score), "box": box}
)
return detections
class NudeDetector:
def __init__(self, providers=None):
self.onnx_session = onnxruntime.InferenceSession(
os.path.join(os.path.dirname(__file__), "best.onnx"),
providers=C.get_available_providers() if not providers else providers,
)
model_inputs = self.onnx_session.get_inputs()
input_shape = model_inputs[0].shape
self.input_width = input_shape[2] # 320
self.input_height = input_shape[3] # 320
self.input_name = model_inputs[0].name
def detect(self, image_path):
preprocessed_image, resize_factor, pad_left, pad_top = _read_image(
image_path, self.input_width
)
outputs = self.onnx_session.run(None, {self.input_name: preprocessed_image})
detections = _postprocess(outputs, resize_factor, pad_left, pad_top)
return detections
def censor(self, image_path, classes=[], output_path=None):
detections = self.detect(image_path)
if classes:
detections = [
detection for detection in detections if detection["class"] in classes
]
img = cv2.imread(image_path)
for detection in detections:
box = detection["box"]
x, y, w, h = box[0], box[1], box[2], box[3]
# change these pixels to pure black
img[y : y + h, x : x + w] = (0, 0, 0)
if not output_path:
image_path, ext = os.path.splitext(image_path)
output_path = f"{image_path}_censored{ext}"
cv2.imwrite(output_path, img)
return output_path
@app.route("/detect", methods=["GET"])
def detect_nudity():
# Check if 'url' parameter is provided
image_url = request.args.get("url")
if not image_url:
return jsonify({"error": "'url' parameter is required"}), 400
# Download the image from the URL
try:
req = urllib.request.urlopen(image_url)
image_data = req.read()
except Exception as e:
return jsonify({"error": f"Failed to download image: {str(e)}"}), 400
# Save the image temporarily
file_path = "/tmp/temp_image.jpg"
with open(file_path, "wb") as f:
f.write(image_data)
# Detect nudity
detector = NudeDetector()
detections = detector.detect(file_path)
# Remove the temporary file
os.remove(file_path)
return jsonify(detections)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=3018, debug=True)
I think the model is outdated, isn't it?
I can also confirm if you rotate the image three times (90,180,270 degrees) you get four different results, sometimes better than with the initial orientation. I think this could be solved during training by feeding each image of the dataset in the original and rotated orientation.
I can also confirm if you rotate the image three times (90,180,270 degrees) you get four different results, sometimes better than with the initial orientation. I think this could be solved during training by feeding each image of the dataset in the original and rotated orientation.
Pushed new models which are trained with lot of data aug, hopefully that will solve this issue.
@AlexUrrutia have updated the pre-processing to be exactly same as js version. using the exact same function calls as opencv.
Weirdly even the ultralytics official onnx inference code is also wrong.
Appreciate if you can test and let me know any issues
@AlexUrrutia I am also in the process of pushing a different type of model (not object detection) as stage 2 for more accuracy, will update here once done.
I'm trying to get rid of the nudity in the local files and noticed that some of the files are not properly detected. I tried one of the problematic files in the browser using this link: https://nudenet.notai.tech/ and it works perfectly well. Could you please let me know what could be the reason? p.s. first I upgraded NudeNet, then uninstalled NudeNet and installed it from scratch - nothing helped.
The sample from the browser:![image](https://github.com/notAI-tech/NudeNet/assets/63911084/89cbc4ad-f2ed-48ca-a968-8799fc0fb256)
My local code:![image](https://github.com/notAI-tech/NudeNet/assets/63911084/500ec8de-4425-400f-89b8-ac921dbdc10a)