Added working api
This commit is contained in:
commit
71a20dff5e
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
assets/
|
||||||
|
libs/
|
||||||
|
venv/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
tmp/
|
||||||
|
*.spec
|
78
README.md
Normal file
78
README.md
Normal 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
199
alpr_api.py
Normal 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
1
build_alpr_api.sh
Executable 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
58
build_and_setup_ultimatealvr.sh
Executable 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
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
flask
|
||||||
|
pillow
|
||||||
|
ultimateAlprSdk
|
23
test.py
Normal file
23
test.py
Normal 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
BIN
test_image.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 84 KiB |
Loading…
Reference in New Issue
Block a user