diff --git a/app_cv/Dockerfile b/app_cv/Dockerfile new file mode 100644 index 0000000..a0dfbce --- /dev/null +++ b/app_cv/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.12 + +WORKDIR /app + +# LibGL for OpenCV +RUN apt-get update && apt-get install libgl1 -y + +# Download models +RUN mkdir models + +# https://github.com/wildchlamydia/mivolo +RUN curl "https://drive.usercontent.google.com/download?id=11i8pKctxz3wVkDBlWKvhYIh7kpVFXSZ4&confirm=xxx" -o models/model_imdb_cross_person_4.22_99.46.pth.tar +RUN curl "https://drive.usercontent.google.com/download?id=1CGNCkZQNj5WkP3rLpENWAOgrBQkUWRdw&confirm=xxx" -o models/yolov8x_person_face.pt + +# https://github.com/notAI-tech/NudeNet +# Upload to an accessible link: https://github.com/notAI-tech/NudeNet/releases/download/v3.4-weights/640m.onnx +RUN curl "https://drive.usercontent.google.com/download?id=1lHTrW1rmYoYnMSUlhLwqFCW61-w2hvKX&confirm=xxx" -o models/640m.onnx + +COPY . . +RUN pip install --no-cache-dir -r requirements.txt + +EXPOSE 5000 + +# CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "app:app"] +CMD ["uvicorn", "--port", "5000", "--workers", "2", "app:app"] + +# docker build -t fetcher_cv . +# docker run --rm -p 5000:5000 fetcher_cv \ No newline at end of file diff --git a/app_cv/README.md b/app_cv/README.md deleted file mode 100644 index 792d809..0000000 --- a/app_cv/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Requirements - -``` -pip install git+https://github.com/wildchlamydia/mivolo.git "nudenet>=3.4.2" -``` - -- Download checkpoints - - https://github.com/wildchlamydia/mivolo - - models/mivolo/model_imdb_cross_person_4.22_99.46.pth.tar - - models/mivolo/yolov8x_person_face.pt - - https://github.com/notAI-tech/NudeNet?tab=readme-ov-file#available-models - - models/nude_detector/640m.onnx - - -# TODO - -- Client side inference: https://github.com/notAI-tech/NudeNet/tree/v3/in_browser diff --git a/app_cv/app.py b/app_cv/app.py index c73d1e3..5da2c20 100644 --- a/app_cv/app.py +++ b/app_cv/app.py @@ -1,33 +1,41 @@ -from flask import Flask, request, jsonify +from fastapi import FastAPI +from nicegui import ui, events, run import base64 import io +import numpy as np import cv2 import traceback -import os import logging logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(message)s', handlers=[logging.StreamHandler()]) from cv_processor import process -app = Flask(__name__) +from pydantic import BaseModel -@app.route('/process', methods=['POST']) -def process_image(): +class Item(BaseModel): + image: str # Base64 + +app = FastAPI() + +@app.post('/process') +def process_image(item: Item): logging.info("POST /process") - # Json - data = request.get_json() - # Valid data? - if not data or 'image' not in data: - return jsonify({"error": "No image data provided"}), 400 - try: - image_data = data['image'] + image_data = item.image + if (image_data is None): + return {"error": "No image data provided"} # Decode base64 string image_bytes = base64.b64decode(image_data) image_stream = io.BytesIO(image_bytes) + # Convert bytes to NumPy array + img_array = np.frombuffer(image_stream.getvalue(), dtype=np.uint8) + # Decode image using OpenCV + img_bgr = cv2.imdecode(img_array, cv2.IMREAD_COLOR) + # Valid image + assert(img_bgr is not None) # Process the image - results = process(image_stream) + results = process(img_bgr) # Encode processed image to base64 _, buffer = cv2.imencode('.jpg', results.get("image"), [cv2.IMWRITE_JPEG_QUALITY, 100]) @@ -37,12 +45,34 @@ def process_image(): results["image_b64"] = processed_image_base64 # Pop image (not serializable) results.pop("image") - # Jsonify - return jsonify(results) + return results except Exception as e: logging.warning("Exception: {}".format(traceback.format_exc())) - return jsonify({"error": traceback.format_exc()}), 400 + return {"error": traceback.format_exc()} + +# Define the NiceGUI UI components +@ui.page("/") +def main_page(): + + async def handle_upload(e: events.UploadEventArguments) -> None: + ui.notify('Processing...') + # Read content -> image + nparr = np.frombuffer(e.content.read(), np.uint8) + img_np_bgr = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + # Async process + results = await run.io_bound(process, img_np_bgr) + + # Display + with ui.dialog() as dialog: + # Encode + retval, buffer = cv2.imencode('.png', results.get("image")) + img_buffer_encoded = base64.b64encode(buffer).decode('utf-8') + img_encoded = "data:image/png;base64,{}".format(img_buffer_encoded) + content = ui.image(img_encoded).props('fit=scale-down') + dialog.open() + + ui.upload(on_upload=handle_upload, auto_upload=True, on_rejected=lambda: ui.notify('Rejected!')).props('accept=image').classes('max-w-full') if __name__ == '__main__': - app.run(debug=os.getenv("DEBUG_MODE", False)) + ui.run_with(app, title="CV") diff --git a/app_cv/cv_processor.py b/app_cv/cv_processor.py index 445f114..2ec45ed 100644 --- a/app_cv/cv_processor.py +++ b/app_cv/cv_processor.py @@ -14,8 +14,8 @@ class CV(): def __init__(self): args = argparse.ArgumentParser() args.add_argument("--device", type=str, default="cpu") - args.add_argument("--checkpoint", default="models/mivolo/model_imdb_cross_person_4.22_99.46.pth.tar") - args.add_argument("--detector_weights", default="models/mivolo/yolov8x_person_face.pt") + args.add_argument("--checkpoint", default="models/model_imdb_cross_person_4.22_99.46.pth.tar") + args.add_argument("--detector_weights", default="models/yolov8x_person_face.pt") args.add_argument("--with-persons", action="store_true", default=False, help="If set model will run with persons, if available") args.add_argument("--disable-faces", action="store_true", default=False, help="If set model will use only persons if available") args.add_argument("--draw", action="store_true", default=False, help="If set, resulted images will be drawn") @@ -24,7 +24,7 @@ class CV(): self.predictor_age = Predictor(args) # Initialize - self.nude_detector = NudeDetector(model_path="models/nude_detector/640m.onnx", inference_resolution=640) + self.nude_detector = NudeDetector(model_path="models/640m.onnx", inference_resolution=640) # detector = NudeDetector(model_path="downloaded_640m.onnx path", inference_resolution=640) # https://github.com/notAI-tech/NudeNet?tab=readme-ov-file#available-models @@ -112,16 +112,9 @@ class CV(): } return results -def process(image_bytes): +def process(img_bgr): try: logging.info("Processing image") - # Convert bytes to NumPy array - img_array = np.frombuffer(image_bytes.getvalue(), dtype=np.uint8) - # Decode image using OpenCV - img_bgr = cv2.imdecode(img_array, cv2.IMREAD_COLOR) - - if img_bgr is None: - return {} # Process results = CV().process_image(img_bgr) logging.info("Returning results") diff --git a/app_cv/docker-compose.yml b/app_cv/docker-compose.yml new file mode 100644 index 0000000..6099a77 --- /dev/null +++ b/app_cv/docker-compose.yml @@ -0,0 +1,19 @@ +services: + matitos_cv: + build: + context: . + image: fetcher_app_cv + container_name: fetcher_app_cv + restart: unless-stopped + ports: + - 5000 + environment: + - DEBUG_MODE=0 + labels: # Reverse proxy sample + - "traefik.enable=true" + - "traefik.http.routers.cv.rule=Host(`cv.matitos.org`)" + - "traefik.http.routers.cv.entrypoints=websecure" + - "traefik.http.routers.cv.tls.certresolver=myresolvercd" + - "traefik.http.services.cv.loadbalancer.server.port=5000" + networks: + - docker_default # Reverse proxy network diff --git a/app_cv/requirements.txt b/app_cv/requirements.txt new file mode 100644 index 0000000..750c5e9 --- /dev/null +++ b/app_cv/requirements.txt @@ -0,0 +1,7 @@ +opencv-python +git+https://github.com/wildchlamydia/mivolo.git +nudenet>=3.4.2 +torch==2.5 +nicegui +fastapi +gunicorn \ No newline at end of file diff --git a/app_cv_face/ABC.ipynb b/app_cv_face/ABC.ipynb new file mode 100644 index 0000000..f6394d0 --- /dev/null +++ b/app_cv_face/ABC.ipynb @@ -0,0 +1,55 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Warning: Binary output can mess up your terminal. Use \"--output -\" to tell \n", + "Warning: curl to output it to your terminal anyway, or consider \"--output \n", + "Warning: \" to save to a file.\n" + ] + } + ], + "source": [ + "!curl https://api.missingkids.org/photographs/NCMC2049364c1.jpg" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install deepface\n", + "# !pip install tf-keras\n", + "from deepface import DeepFace" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "matitos_cv_face", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}