Skip to content

Commit 9639647

Browse files
Merge pull request #12 from arvindrajan92/FASTANPR-3
FASTANPR-3 - Adding Support for Running FastAPI Server hosting FastANPR
2 parents 0d5ae91 + c40298e commit 9639647

10 files changed

+147
-15
lines changed

Dockerfile

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Use the official Python base image
2+
FROM python:3.11-slim
3+
4+
# Install other dependencies
5+
RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y
6+
7+
# Set the working directory inside the container
8+
WORKDIR /app
9+
10+
# Copy the requirements file to the working directory
11+
COPY requirements.txt .
12+
13+
# Install the Python dependencies
14+
RUN pip install -r requirements.txt
15+
16+
# Copy the application code to the working directory
17+
COPY . .
18+
19+
# Expose the port on which the application will run
20+
EXPOSE 8000
21+
22+
# Run the FastAPI application using uvicorn server
23+
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"]

README.md

+46
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,52 @@ Runs ANPR on a list of images and return a list of detected number plates.
7878
- `rec_poly` (List[List[int]]): Polygon coordinates of detected texts.
7979
- `rec_conf` (float): Confidence score of recognition.
8080

81+
## FastAPI
82+
To start a FastAPI server locally from your console:
83+
```bash
84+
uvicorn api:app
85+
```
86+
### Usage
87+
```python
88+
import base64
89+
import requests
90+
91+
# Step 1: Read the image file
92+
image_path = 'tests/images/image001.jpg'
93+
with open(image_path, 'rb') as image_file:
94+
image_data = image_file.read()
95+
96+
# Step 2: Convert the image to a base64 encoded string
97+
base64_image_str = base64.b64encode(image_data).decode('utf-8')
98+
99+
# Prepare the data for the POST request (assuming the API expects JSON)
100+
data = {'image': base64_image_str}
101+
102+
# Step 3: Send a POST request
103+
response = requests.post(url='http://127.0.0.1:8000/recognise', json=data)
104+
105+
# Check the response
106+
if response.status_code == 200:
107+
# 'number_plates': [
108+
# {
109+
# 'det_box': [682, 414, 779, 455],
110+
# 'det_conf': 0.29964497685432434,
111+
# 'rec_poly': [[688, 420], [775, 420], [775, 451], [688, 451]],
112+
# 'rec_text': 'BVH826',
113+
# 'rec_conf': 0.940690815448761
114+
# }
115+
# ]
116+
print(response.json())
117+
else:
118+
print(f"Request failed with status code {response.status_code}.")
119+
```
120+
121+
## Docker
122+
Hosting a FastAPI server can also be done by building a docker file as from console:
123+
```bash
124+
docker build -t fastanpr-app .
125+
docker run -p 8000:8000 fastanpr-app
126+
```
81127

82128
## Licence
83129
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.

api.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import io
2+
import base64
3+
import uvicorn
4+
import fastanpr
5+
import numpy as np
6+
7+
from PIL import Image
8+
from fastapi import FastAPI
9+
from pydantic import BaseModel
10+
11+
app = FastAPI(
12+
title="FastANPR",
13+
description="A web server for FastANPR hosted using FastAPI",
14+
version=fastanpr.__version__
15+
)
16+
fast_anpr = fastanpr.FastANPR()
17+
18+
19+
class FastANPRRequest(BaseModel):
20+
image: str
21+
22+
23+
class FastANPRResponse(BaseModel):
24+
number_plates: list[fastanpr.NumberPlate] = None
25+
26+
27+
def base64_image_to_ndarray(base64_image_str: str) -> np.ndarray:
28+
image_data = base64.b64decode(base64_image_str)
29+
image = Image.open(io.BytesIO(image_data))
30+
return np.array(image, dtype=np.uint8)
31+
32+
33+
@app.post("/recognise", response_model=FastANPRResponse)
34+
async def recognise(request: FastANPRRequest):
35+
image = base64_image_to_ndarray(request.image)
36+
number_plates = (await fast_anpr.run(image))[0]
37+
return FastANPRResponse(
38+
number_plates=[fastanpr.NumberPlate.parse_obj(number_plate.__dict__) for number_plate in number_plates]
39+
)
40+
41+
42+
if __name__ == "__main__":
43+
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="debug")

fastanpr/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from .fastanpr import FastANPR
21
from .version import __version__
2+
from .fastanpr import FastANPR, NumberPlate
33

44
__version__ = __version__
55
FastANPR = FastANPR
6+
NumberPlate = NumberPlate

fastanpr/detection.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
from pathlib import Path
22
from ultralytics import YOLO
3+
from pydantic import BaseModel
34
from typing import Union, List
4-
from dataclasses import dataclass
55

66
import numpy as np
77

88

9-
@dataclass(frozen=True)
10-
class Detection:
9+
class Detection(BaseModel):
1110
image: np.ndarray
1211
box: List[int]
1312
conf: float
1413

14+
class Config:
15+
frozen = True
16+
arbitrary_types_allowed = True
17+
1518

1619
class Detector:
1720
def __init__(self, detection_model: Union[str, Path], device: str):
@@ -28,6 +31,8 @@ def run(self, images: List[np.ndarray]) -> List[List[Detection]]:
2831
det_confs = detection.boxes.cpu().conf.numpy().tolist()
2932
for det_box, det_conf in zip(det_boxes, det_confs):
3033
x_min, x_max, y_min, y_max = det_box[0], det_box[2], det_box[1], det_box[3]
31-
image_detections.append(Detection(image[y_min:y_max, x_min:x_max, :], det_box[:4], det_conf))
34+
image_detections.append(
35+
Detection(image=image[y_min:y_max, x_min:x_max, :], box=det_box[:4], conf=det_conf)
36+
)
3237
results.append(image_detections)
3338
return results

fastanpr/fastanpr.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,15 @@ async def run(self, images: Union[np.ndarray, List[np.ndarray]]) -> List[List[Nu
4646
offset_recog_poly = self._offset_recognition_poly(detection.box, recognition.poly)
4747
image_results.append(
4848
NumberPlate(
49-
detection.box, detection.conf, offset_recog_poly, recognition.text, recognition.conf
49+
det_box=detection.box,
50+
det_conf=detection.conf,
51+
rec_poly=offset_recog_poly,
52+
rec_text=recognition.text,
53+
rec_conf=recognition.conf
5054
)
5155
)
5256
else:
53-
image_results.append(NumberPlate(detection.box, detection.conf))
57+
image_results.append(NumberPlate(det_box=detection.box, det_conf=detection.conf))
5458
results.append(image_results)
5559
return results
5660

fastanpr/numberplate.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
from dataclasses import dataclass
21
from typing import List
2+
from pydantic import BaseModel
33

44

5-
@dataclass(frozen=True)
6-
class NumberPlate:
5+
class NumberPlate(BaseModel):
76
det_box: List[int]
87
det_conf: float
98
rec_poly: List[List[int]] = None
109
rec_text: str = None
1110
rec_conf: float = None
11+
12+
class Config:
13+
frozen = True

fastanpr/recognition.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
from paddleocr import PaddleOCR
2-
from dataclasses import dataclass
2+
from pydantic import BaseModel
33
from typing import List, Tuple, Optional
44

55

6-
@dataclass(frozen=True)
7-
class Recognition:
6+
class Recognition(BaseModel):
87
text: str
98
poly: List[List[int]]
109
conf: float
1110

11+
class Config:
12+
frozen = True
13+
1214

1315
class Recogniser:
1416
def __init__(self, device: str):
@@ -33,7 +35,7 @@ def run(self, image) -> Optional[Recognition]:
3335
clean_poly = polys[0]
3436
clean_text = _clean_text(texts[0])
3537
clean_conf = confs[0]
36-
return Recognition(clean_text, clean_poly, clean_conf)
38+
return Recognition(text=clean_text, poly=clean_poly, conf=clean_conf)
3739
else:
3840
return None
3941

requirements.txt

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
ultralytics>=8.1.34
2+
paddlepaddle>=2.6.1
3+
paddleocr==2.7.2
4+
fastapi>=0.110.0
5+
pydantic>=2.6.4
6+
uvicorn>=0.29.0

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
packages=find_packages(),
4949
package_data={'': ['*.pt'], 'fastanpr': ['*.pt']},
5050
include_package_data=True,
51-
install_requires=['ultralytics>=8.1.34', 'paddlepaddle>=2.6.1', 'paddleocr>=2.7.2'],
51+
install_requires=['ultralytics>=8.1.34', 'paddlepaddle>=2.6.1', 'paddleocr==2.7.2', 'pydantic>=2.6.4'],
5252
python_requires=PYTHON_REQUIRES,
5353
extras_require={
5454
'dev': ['pytest', 'pytest-asyncio', 'twine', 'python-Levenshtein', 'setuptools', 'wheel', 'twine', 'flake8']

0 commit comments

Comments
 (0)