diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..91e1368 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# Use the official Python base image +FROM python:3.11-slim + +# Install other dependencies +RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y + +# Set the working directory inside the container +WORKDIR /app + +# Copy the requirements file to the working directory +COPY requirements.txt . + +# Install the Python dependencies +RUN pip install -r requirements.txt + +# Copy the application code to the working directory +COPY . . + +# Expose the port on which the application will run +EXPOSE 8000 + +# Run the FastAPI application using uvicorn server +CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/README.md b/README.md index 2fd2713..8d27ea2 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,52 @@ Runs ANPR on a list of images and return a list of detected number plates. - `rec_poly` (List[List[int]]): Polygon coordinates of detected texts. - `rec_conf` (float): Confidence score of recognition. +## FastAPI +To start a FastAPI server locally from your console: +```bash +uvicorn api:app +``` +### Usage +```python +import base64 +import requests + +# Step 1: Read the image file +image_path = 'tests/images/image001.jpg' +with open(image_path, 'rb') as image_file: + image_data = image_file.read() + +# Step 2: Convert the image to a base64 encoded string +base64_image_str = base64.b64encode(image_data).decode('utf-8') + +# Prepare the data for the POST request (assuming the API expects JSON) +data = {'image': base64_image_str} + +# Step 3: Send a POST request +response = requests.post(url='http://127.0.0.1:8000/recognise', json=data) + +# Check the response +if response.status_code == 200: + # 'number_plates': [ + # { + # 'det_box': [682, 414, 779, 455], + # 'det_conf': 0.29964497685432434, + # 'rec_poly': [[688, 420], [775, 420], [775, 451], [688, 451]], + # 'rec_text': 'BVH826', + # 'rec_conf': 0.940690815448761 + # } + # ] + print(response.json()) +else: + print(f"Request failed with status code {response.status_code}.") +``` + +## Docker +Hosting a FastAPI server can also be done by building a docker file as from console: +```bash +docker build -t fastanpr-app . +docker run -p 8000:8000 fastanpr-app +``` ## Licence This project incorporates the YOLOv8 model from Ultralytics, which is licensed under the AGPL-3.0 license. As such, this project is also distributed under the [GNU Affero General Public License v3.0 (AGPL-3.0)](LICENSE) to comply with the licensing requirements. diff --git a/api.py b/api.py new file mode 100644 index 0000000..7dddaf6 --- /dev/null +++ b/api.py @@ -0,0 +1,43 @@ +import io +import base64 +import uvicorn +import fastanpr +import numpy as np + +from PIL import Image +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI( + title="FastANPR", + description="A web server for FastANPR hosted using FastAPI", + version=fastanpr.__version__ +) +fast_anpr = fastanpr.FastANPR() + + +class FastANPRRequest(BaseModel): + image: str + + +class FastANPRResponse(BaseModel): + number_plates: list[fastanpr.NumberPlate] = None + + +def base64_image_to_ndarray(base64_image_str: str) -> np.ndarray: + image_data = base64.b64decode(base64_image_str) + image = Image.open(io.BytesIO(image_data)) + return np.array(image, dtype=np.uint8) + + +@app.post("/recognise", response_model=FastANPRResponse) +async def recognise(request: FastANPRRequest): + image = base64_image_to_ndarray(request.image) + number_plates = (await fast_anpr.run(image))[0] + return FastANPRResponse( + number_plates=[fastanpr.NumberPlate.parse_obj(number_plate.__dict__) for number_plate in number_plates] + ) + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000, log_level="debug") diff --git a/fastanpr/__init__.py b/fastanpr/__init__.py index 215504d..8a0dd32 100644 --- a/fastanpr/__init__.py +++ b/fastanpr/__init__.py @@ -1,5 +1,6 @@ -from .fastanpr import FastANPR from .version import __version__ +from .fastanpr import FastANPR, NumberPlate __version__ = __version__ FastANPR = FastANPR +NumberPlate = NumberPlate diff --git a/fastanpr/detection.py b/fastanpr/detection.py index 8982691..143d027 100644 --- a/fastanpr/detection.py +++ b/fastanpr/detection.py @@ -1,17 +1,20 @@ from pathlib import Path from ultralytics import YOLO +from pydantic import BaseModel from typing import Union, List -from dataclasses import dataclass import numpy as np -@dataclass(frozen=True) -class Detection: +class Detection(BaseModel): image: np.ndarray box: List[int] conf: float + class Config: + frozen = True + arbitrary_types_allowed = True + class Detector: def __init__(self, detection_model: Union[str, Path], device: str): @@ -28,6 +31,8 @@ def run(self, images: List[np.ndarray]) -> List[List[Detection]]: det_confs = detection.boxes.cpu().conf.numpy().tolist() for det_box, det_conf in zip(det_boxes, det_confs): x_min, x_max, y_min, y_max = det_box[0], det_box[2], det_box[1], det_box[3] - image_detections.append(Detection(image[y_min:y_max, x_min:x_max, :], det_box[:4], det_conf)) + image_detections.append( + Detection(image=image[y_min:y_max, x_min:x_max, :], box=det_box[:4], conf=det_conf) + ) results.append(image_detections) return results diff --git a/fastanpr/fastanpr.py b/fastanpr/fastanpr.py index 589b242..c48a5ca 100644 --- a/fastanpr/fastanpr.py +++ b/fastanpr/fastanpr.py @@ -46,11 +46,15 @@ async def run(self, images: Union[np.ndarray, List[np.ndarray]]) -> List[List[Nu offset_recog_poly = self._offset_recognition_poly(detection.box, recognition.poly) image_results.append( NumberPlate( - detection.box, detection.conf, offset_recog_poly, recognition.text, recognition.conf + det_box=detection.box, + det_conf=detection.conf, + rec_poly=offset_recog_poly, + rec_text=recognition.text, + rec_conf=recognition.conf ) ) else: - image_results.append(NumberPlate(detection.box, detection.conf)) + image_results.append(NumberPlate(det_box=detection.box, det_conf=detection.conf)) results.append(image_results) return results diff --git a/fastanpr/numberplate.py b/fastanpr/numberplate.py index edb91c0..e51d871 100644 --- a/fastanpr/numberplate.py +++ b/fastanpr/numberplate.py @@ -1,11 +1,13 @@ -from dataclasses import dataclass from typing import List +from pydantic import BaseModel -@dataclass(frozen=True) -class NumberPlate: +class NumberPlate(BaseModel): det_box: List[int] det_conf: float rec_poly: List[List[int]] = None rec_text: str = None rec_conf: float = None + + class Config: + frozen = True diff --git a/fastanpr/recognition.py b/fastanpr/recognition.py index 4f11b7f..5816fa8 100644 --- a/fastanpr/recognition.py +++ b/fastanpr/recognition.py @@ -1,14 +1,16 @@ from paddleocr import PaddleOCR -from dataclasses import dataclass +from pydantic import BaseModel from typing import List, Tuple, Optional -@dataclass(frozen=True) -class Recognition: +class Recognition(BaseModel): text: str poly: List[List[int]] conf: float + class Config: + frozen = True + class Recogniser: def __init__(self, device: str): @@ -33,7 +35,7 @@ def run(self, image) -> Optional[Recognition]: clean_poly = polys[0] clean_text = _clean_text(texts[0]) clean_conf = confs[0] - return Recognition(clean_text, clean_poly, clean_conf) + return Recognition(text=clean_text, poly=clean_poly, conf=clean_conf) else: return None diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..06c2826 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +ultralytics>=8.1.34 +paddlepaddle>=2.6.1 +paddleocr==2.7.2 +fastapi>=0.110.0 +pydantic>=2.6.4 +uvicorn>=0.29.0 diff --git a/setup.py b/setup.py index 556b6a9..da67415 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ packages=find_packages(), package_data={'': ['*.pt'], 'fastanpr': ['*.pt']}, include_package_data=True, - install_requires=['ultralytics>=8.1.34', 'paddlepaddle>=2.6.1', 'paddleocr>=2.7.2'], + install_requires=['ultralytics>=8.1.34', 'paddlepaddle>=2.6.1', 'paddleocr==2.7.2', 'pydantic>=2.6.4'], python_requires=PYTHON_REQUIRES, extras_require={ 'dev': ['pytest', 'pytest-asyncio', 'twine', 'python-Levenshtein', 'setuptools', 'wheel', 'twine', 'flake8']