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 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")

View File

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

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
}