CV app with fastapi, web nicegui based

This commit is contained in:
Luciano Gervasoni
2025-04-30 18:41:35 +02:00
parent ccfd0f9188
commit d7df5b4ea4
7 changed files with 160 additions and 45 deletions

28
app_cv/Dockerfile Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -1,33 +1,41 @@
from flask import Flask, request, jsonify from fastapi import FastAPI
from nicegui import ui, events, run
import base64 import base64
import io import io
import numpy as np
import cv2 import cv2
import traceback import traceback
import os
import logging import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(message)s', handlers=[logging.StreamHandler()]) logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(message)s', handlers=[logging.StreamHandler()])
from cv_processor import process from cv_processor import process
app = Flask(__name__) from pydantic import BaseModel
@app.route('/process', methods=['POST']) class Item(BaseModel):
def process_image(): image: str # Base64
app = FastAPI()
@app.post('/process')
def process_image(item: Item):
logging.info("POST /process") 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: try:
image_data = data['image'] image_data = item.image
if (image_data is None):
return {"error": "No image data provided"}
# Decode base64 string # Decode base64 string
image_bytes = base64.b64decode(image_data) image_bytes = base64.b64decode(image_data)
image_stream = io.BytesIO(image_bytes) 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 # Process the image
results = process(image_stream) results = process(img_bgr)
# Encode processed image to base64 # Encode processed image to base64
_, buffer = cv2.imencode('.jpg', results.get("image"), [cv2.IMWRITE_JPEG_QUALITY, 100]) _, 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 results["image_b64"] = processed_image_base64
# Pop image (not serializable) # Pop image (not serializable)
results.pop("image") results.pop("image")
# Jsonify return results
return jsonify(results)
except Exception as e: except Exception as e:
logging.warning("Exception: {}".format(traceback.format_exc())) 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__': if __name__ == '__main__':
app.run(debug=os.getenv("DEBUG_MODE", False)) ui.run_with(app, title="CV")

View File

@@ -14,8 +14,8 @@ class CV():
def __init__(self): def __init__(self):
args = argparse.ArgumentParser() args = argparse.ArgumentParser()
args.add_argument("--device", type=str, default="cpu") 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("--checkpoint", default="models/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("--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("--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("--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") 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) self.predictor_age = Predictor(args)
# Initialize # 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) # detector = NudeDetector(model_path="downloaded_640m.onnx path", inference_resolution=640)
# https://github.com/notAI-tech/NudeNet?tab=readme-ov-file#available-models # https://github.com/notAI-tech/NudeNet?tab=readme-ov-file#available-models
@@ -112,16 +112,9 @@ class CV():
} }
return results return results
def process(image_bytes): def process(img_bgr):
try: try:
logging.info("Processing image") 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 # Process
results = CV().process_image(img_bgr) results = CV().process_image(img_bgr)
logging.info("Returning results") logging.info("Returning results")

19
app_cv/docker-compose.yml Normal file
View File

@@ -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

7
app_cv/requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
opencv-python
git+https://github.com/wildchlamydia/mivolo.git
nudenet>=3.4.2
torch==2.5
nicegui
fastapi
gunicorn

55
app_cv_face/ABC.ipynb Normal file
View File

@@ -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: <FILE>\" 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
}