Skip to content

Commit

Permalink
Merge pull request #12 from arvindrajan92/FASTANPR-3
Browse files Browse the repository at this point in the history
FASTANPR-3 - Adding Support for Running FastAPI Server hosting FastANPR
  • Loading branch information
arvindrajan92 authored Mar 30, 2024
2 parents 0d5ae91 + c40298e commit 9639647
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 15 deletions.
23 changes: 23 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
43 changes: 43 additions & 0 deletions api.py
Original file line number Diff line number Diff line change
@@ -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")
3 changes: 2 additions & 1 deletion fastanpr/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .fastanpr import FastANPR
from .version import __version__
from .fastanpr import FastANPR, NumberPlate

__version__ = __version__
FastANPR = FastANPR
NumberPlate = NumberPlate
13 changes: 9 additions & 4 deletions fastanpr/detection.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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
8 changes: 6 additions & 2 deletions fastanpr/fastanpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 5 additions & 3 deletions fastanpr/numberplate.py
Original file line number Diff line number Diff line change
@@ -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
10 changes: 6 additions & 4 deletions fastanpr/recognition.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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

Expand Down
6 changes: 6 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down

0 comments on commit 9639647

Please sign in to comment.