Added working api

This commit is contained in:
Mathieu Broillet 2024-07-17 18:57:51 +02:00
commit 71a20dff5e
Signed by: mathieu
GPG Key ID: A08E484FE95074C1
8 changed files with 369 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
assets/
libs/
venv/
build/
dist/
tmp/
*.spec

78
README.md Normal file
View File

@ -0,0 +1,78 @@
# Easy local ALPR (Automatic License Plate Recognition)
This script is a REST API server that uses [ultimateALPR-SDK](https://github.com/DoubangoTelecom/ultimateALPR-SDK) to process images and return the license plate
information. The server is created using Flask and the ultimateALPR SDK is used to process the images.
This script is intended to be used as a faster local alternative to the large and resource heavy [CodeProject AI](https://www.codeproject.com/AI/docs) software.
> [!IMPORTANT]
> The ultimateALPR SDK is a lightweight and much faster alternative (on CPU and GPU) to the CodeProject AI software but it has **a few limitations** with it's free version:
> - The last character of the license plate is masked with an asterisk
> - The SDK supposedly has a limit of requests per program execution *(never encountered yet)* **but I have implemented a workaround for this by restarting the SDK after 3000 requests just in case.**
## Usage
The server listens on port 5000 and has one endpoint: /v1/image/alpr. The endpoint accepts POST requests with an
image file in the 'upload' field. The image is processed using the ultimateALPR SDK and the license plate
information is returned in JSON format. The reponse follows the CodeProject AI ALPR API format. So it can be used
as a drop-in replacement for the [CodeProject AI ALPR API](https://www.codeproject.com/AI/docs/api/api_reference.html#license-plate-reader).
> POST: http://localhost:32168/v1/vision/alpr
**Parameters**
- upload: (File) The image file to process. (see [Pillow.Image.open()](https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.open) for supported formats)
**Response**
```json
{
"success": (Boolean) // True if successful.
"message": (String) // A summary of the inference operation.
"error": (String) // (Optional) An description of the error if success was false.
"predictions": (Object[]) // An array of objects with the x_max, x_min, max, y_min bounds of the plate, label, the plate chars and confidence.
"processMs": (Integer) // The time (ms) to process the image (includes inference and image manipulation operations).
}
```
## Included models in built executable
When using the built executable, only the **latin** charset models are bundled by default. If you want to use a different
charset, you need to set the charset in the JSON_CONFIG variable and rebuild the executable with the according
models found [here](https://github.com/DoubangoTelecom/ultimateALPR-SDK/tree/master/assets)
To build the executable, you can use the ``build_alpr_api.sh`` script, which will create an executable named ``alpr_api`` in
the ``dist`` folder.
## Setup development environment
### Install ultimateALPR SDK
#### Use already built wheel
I have already built the ultimateALPR SDK for x86_64 and ARM64 and included the python3.10 wheel in the wheel folder.
You can install the wheel using : ``pip install wheel/*.whl``
#### Manually build the wheel
If you want to build the wheel yourself, you can use the ``build_and_setup_ultimatealvr.sh`` script. It will create a new
directory ``tmp`` and build the wheel in there. It also includes the assets and libs folders needed when developing.
### Copy necessary files/folders
Then you need to copy the ``assets`` and ``libs`` folders to the same directory as the script.
If you built the wheel in the previous step, you can copy the ``assets`` and ``libs`` folders from the ``tmp`` directory.
If you used the already built wheel, you can find the 'assets' and 'libs' folders on the [GitHub repository](https://github.com/DoubangoTelecom/ultimateALPR-SDK/tree/master/assets)
The structure should look like this:
```bash
.
├── alpr_api.py
├── assets
│ ├── fonts
│ └── models
├── libs
│ ├── libxxxxxx.so
│ ├── ...
│ └── libxxxxxx.so
└── ...
```
### Important notes
When building or developing the script, make sure to set the ``LD_LIBRARY_PATH`` environment variable to the libs folder
*(limitation of the ultimateALPR SDK)*.
```bash
export LD_LIBRARY_PATH=libs:$LD_LIBRARY_PATH
```

199
alpr_api.py Normal file
View File

@ -0,0 +1,199 @@
import json
import os
import sys
import threading
from time import sleep
import ultimateAlprSdk
from PIL import Image
from flask import Flask, request, jsonify
counter = 0
"""
Hi there!
This script is a REST API server that uses the ultimateALPR SDK to process images and return the license plate
information. The server is created using Flask and the ultimateALPR SDK is used to process the images.
See the README.md file for more information on how to run this script.
"""
# Defines the default JSON configuration. More information at https://www.doubango.org/SDKs/anpr/docs/Configuration_options.html
JSON_CONFIG = {
"debug_level": "info",
"debug_write_input_image_enabled": False,
"debug_internal_data_path": ".",
"num_threads": -1,
"gpgpu_enabled": True,
"max_latency": -1,
"klass_vcr_gamma": 1.5,
"detect_roi": [0, 0, 0, 0],
"detect_minscore": 0.35,
"car_noplate_detect_min_score": 0.8,
"pyramidal_search_enabled": True,
"pyramidal_search_sensitivity": 0.38, # default 0.28
"pyramidal_search_minscore": 0.8,
"pyramidal_search_min_image_size_inpixels": 800,
"recogn_rectify_enabled": True, # heavy on cpu
"recogn_minscore": 0.4,
"recogn_score_type": "min"
}
IMAGE_TYPES_MAPPING = {
'RGB': ultimateAlprSdk.ULTALPR_SDK_IMAGE_TYPE_RGB24,
'RGBA': ultimateAlprSdk.ULTALPR_SDK_IMAGE_TYPE_RGBA32,
'L': ultimateAlprSdk.ULTALPR_SDK_IMAGE_TYPE_Y
}
def load_engine():
bundle_dir = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__)))
JSON_CONFIG["assets_folder"] = os.path.join(bundle_dir, "assets")
JSON_CONFIG["charset"] = "latin"
JSON_CONFIG["car_noplate_detect_enabled"] = False # Whether to detect and return cars with no plate
JSON_CONFIG[
"ienv_enabled"] = False # Whether to enable Image Enhancement for Night-Vision (IENV). More info about IENV at https://www.doubango.org/SDKs/anpr/docs/Features.html#image-enhancement-for-night-vision-ienv. Default: true for x86-64 and false for ARM.
JSON_CONFIG[
"openvino_enabled"] = False # Whether to enable OpenVINO. Tensorflow will be used when OpenVINO is disabled
JSON_CONFIG[
"openvino_device"] = "GPU" # Defines the OpenVINO device to use (CPU, GPU, FPGA...). More info at https://www.doubango.org/SDKs/anpr/docs/Configuration_options.html#openvino-device
JSON_CONFIG["npu_enabled"] = False # Whether to enable NPU (Neural Processing Unit) acceleration
JSON_CONFIG[
"klass_lpci_enabled"] = False # Whether to enable License Plate Country Identification (LPCI). More info at https://www.doubango.org/SDKs/anpr/docs/Features.html#license-plate-country-identification-lpci
JSON_CONFIG[
"klass_vcr_enabled"] = False # Whether to enable Vehicle Color Recognition (VCR). More info at https://www.doubango.org/SDKs/anpr/docs/Features.html#vehicle-color-recognition-vcr
JSON_CONFIG[
"klass_vmmr_enabled"] = False # Whether to enable Vehicle Make Model Recognition (VMMR). More info at https://www.doubango.org/SDKs/anpr/docs/Features.html#vehicle-make-model-recognition-vmmr
JSON_CONFIG[
"klass_vbsr_enabled"] = False # Whether to enable Vehicle Body Style Recognition (VBSR). More info at https://www.doubango.org/SDKs/anpr/docs/Features.html#vehicle-body-style-recognition-vbsr
JSON_CONFIG["license_token_file"] = "" # Path to license token file
JSON_CONFIG["license_token_data"] = "" # Base64 license token data
result = ultimateAlprSdk.UltAlprSdkEngine_init(json.dumps(JSON_CONFIG))
if not result.isOK():
raise RuntimeError("Init failed: %s" % result.phrase())
while counter < 3000:
sleep(1)
unload_engine()
load_engine()
def unload_engine():
result = ultimateAlprSdk.UltAlprSdkEngine_deInit()
if not result.isOK():
raise RuntimeError("DeInit failed: %s" % result.phrase())
def process_image(image: Image) -> str:
global counter
counter += 1
width, height = image.size
if image.mode in IMAGE_TYPES_MAPPING:
image_type = IMAGE_TYPES_MAPPING[image.mode]
else:
raise ValueError("Invalid mode: %s" % image.mode)
result = ultimateAlprSdk.UltAlprSdkEngine_process(
image_type,
image.tobytes(), # type(x) == bytes
width,
height,
0, # stride
1 # exifOrientation (already rotated in load_image -> use default value: 1)
)
if not result.isOK():
raise RuntimeError("Process failed: %s" % result.phrase())
else:
return result.json()
def create_rest_server_flask():
app = Flask(__name__)
@app.route('/v1/<string:domain>/<string:module>', methods=['POST'])
def alpr(domain, module):
# Only care about the ALPR endpoint
if domain == 'image' and module == 'alpr':
if 'upload' not in request.files:
return jsonify({'error': 'No image found'})
image = request.files['upload']
if image.filename == '':
return jsonify({'error': 'No selected file'})
image = Image.open(image)
result = process_image(image)
result = convert_to_cpai_compatible(result)
return jsonify(result)
else:
return jsonify({'error': 'Endpoint not implemented'}), 404
return app
def convert_to_cpai_compatible(result):
result = json.loads(result)
response = {
'success': "true",
'processMs': result['duration'],
'inferenceMs': result['duration'],
'predictions': [],
'message': '',
'moduleId': 'ALPR',
'moduleName': 'License Plate Reader',
'code': 200,
'command': 'alpr',
'requestId': 'null',
'inferenceDevice': 'none',
'analysisRoundTripMs': 0,
'processedBy': 'none',
'timestamp': ''
}
if 'plates' in result:
plates = result['plates']
for plate in plates:
warpedBox = plate['warpedBox']
x_coords = warpedBox[0::2]
y_coords = warpedBox[1::2]
x_min = min(x_coords)
x_max = max(x_coords)
y_min = min(y_coords)
y_max = max(y_coords)
response['predictions'].append({
'confidence': plate['confidence'] / 100,
'label': "Plate: " + plate['text'],
'plate': plate['text'],
'x_min': x_min,
'x_max': x_max,
'y_min': y_min,
'y_max': y_max
})
return response
if __name__ == '__main__':
engine = threading.Thread(target=load_engine, daemon=True)
engine.start()
app = create_rest_server_flask()
app.run(host='0.0.0.0', port=5000)
unload_engine()

1
build_alpr_api.sh Executable file
View File

@ -0,0 +1 @@
pyinstaller --noconfirm --onefile --console --add-data libs:. --add-data assets:assets --name alpr_api "alpr_api.py"

58
build_and_setup_ultimatealvr.sh Executable file
View File

@ -0,0 +1,58 @@
# clone sdk
mkdir ./tmp
cd tmp
wget https://github.com/DoubangoTelecom/ultimateALPR-SDK/archive/8130c76140fe8edc60fe20f875796121a8d22fed.zip -O temp-sdk.zip
unzip temp-sdk.zip
rm temp-sdk.zip
mkdir temp-sdk
mv ultimateALPR-SDK*/* ./temp-sdk
rm -R ultimateALPR-SDK*
# create env to build ultimatealpr-sdk for python
python3.10 -m venv venv
source venv/bin/activate
pip install setuptools wheel Cython
cd temp-sdk
# move folders to simplify build
mkdir -p binaries/linux/x86_64/c++
cp c++/* binaries/linux/x86_64/c++
cp python/* binaries/linux/x86_64/
# edit setup.py to simplify build
cd binaries/linux/x86_64/
sed -i "s|sources=\[os.path.abspath('../../../python/ultimateALPR-SDK-API-PUBLIC-SWIG_python.cxx')\]|sources=[os.path.abspath('ultimateALPR-SDK-API-PUBLIC-SWIG_python.cxx')]|g" setup.py
sed -i "s|include_dirs=\['../../../c++'\]|include_dirs=['c++']|g" setup.py
sed -i "s|library_dirs=\['.'\]|library_dirs=['libs']|g" setup.py
# move all .so files into libs folder
mkdir libs
mv *.so libs/
mv *.so.* libs/
# build the wheel
python setup.py bdist_wheel -v
# move the built whl and the libs back to root dir
mv dist/* ../../../../
mv libs ../../../../
# move the assets to root dir
cd ../../../
mv assets ../assets
## install the whl
#cd ..
#pip install *.whl
#rm *.whl
cd ../
# remove sdk
rm -R temp-sdk
echo "UltimateALPR SDK built and setup successfully"
echo "You can now install the wheel using 'pip install ultimateAlprSdk-*.whl'"

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
flask
pillow
ultimateAlprSdk

23
test.py Normal file
View File

@ -0,0 +1,23 @@
import threading
import time
from PIL import Image
import alpr_api
from alpr_api import process_image
if __name__ == '__main__':
threading.Thread(target=alpr_api.load_engine, daemon=True).start()
try:
counter = 0
while True:
counter += 1
# make test request with test_image.png
image = Image.open('test_image.jpg')
result = process_image(image)
print(str(counter) + " - " + result)
time.sleep(1)
except KeyboardInterrupt:
alpr_api.unload_engine()
exit(0)

BIN
test_image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB