Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

13 changed files with 184 additions and 936 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

167
README.md
View File

@ -1,65 +1,28 @@
![logo](.git-assets/logo.webp) # Easy local ALPR (Automatic License Plate Recognition)
![ALPR](.git-assets/preview-webui.webp)
# 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
This project is a simple local ALPR (Automatic License Plate Recognition) server that uses the [ultimateALPR-SDK](https://github.com/DoubangoTelecom/ultimateALPR-SDK) to information. The server is created using Flask and the ultimateALPR SDK is used to process the images.
process images and return the license plate information found in the image while focusing on being:
- **Fast** *(~100ms per image on decent CPU)*
- **Lightweight** *(~100MB of RAM)*
- **Easy to use** *(REST API)*
- **Easy to setup** *(one command setup)*
- **Offline** *(no internet connection required)*
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] > [!IMPORTANT]
> This project relies on the [ultimateALPR-SDK](https://github.com/DoubangoTelecom/ultimateALPR-SDK), which is a commercial product but has a free version with a few limitations. > 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:
> For any commercial use, you will need to take a look at their licensing terms. > - The last character of the license plate is masked with an asterisk
> **I am not affiliated with ultimateALPR-SDK in any way, and I am not responsible for any misuse of the software.** > - 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.**
> [!NOTE]
> The [ultimateALPR-SDK](https://github.com/DoubangoTelecom/ultimateALPR-SDK) is a lightweight and much faster alternative (on CPU and GPU) to existing solutions like
> [CodeProject AI](https://www.codeproject.com/AI/docs/index.html) but it has **one important restriction** with it's free version:
> - The last character of the license plate is masked with an asterisk *(e.g. ``ABC1234`` -> ``ABC123*``)*
## Installation
Simply download the latest release from the [releases page](./releases) and run the executable.
The following platforms are currently supported:
- **Linux** (x86_64)
## Usage ## 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).
The server listens on port 5000 and has a few endpoints documented below, the most important one being [``/v1/image/alpr``](#v1visionalpr). > POST: http://localhost:32168/v1/vision/alpr
### /v1/vision/alpr
> POST: http://localhost:5000/v1/vision/alpr
**Description**
This endpoint processes an image and returns the license plate information (if any) found in the image.
This endpoint follows
the [CodeProject AI ALPR API](https://www.codeproject.com/AI/docs/api/api_reference.html#license-plate-reader) format *(
example below)* so it can be used as a **drop-in replacement** for the CodeProject AI software.
**Parameters** **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)
- 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, almost any image format is supported)*
- grid_size: (Integer, optional) Size of grid to divide the image into and retry on each cell when no match have been
found on the whole image *(default: 3)* **[(more info)](#more-information-about-the-grid-parameter)**
- wanted_cells: (String, optional) The cells you want to process *(default: all cells)* *
*[(see here)](#v1visionalpr_grid_debug)**
- format: ``1,2,3,4,...`` *(comma separated list of integers, max: grid_size^2)*
- *Example for a grid_size of 3:*
```
1 | 2 | 3
4 | 5 | 6
7 | 8 | 9
```
**Response** **Response**
```json
```jsonc
{ {
"success": (Boolean) // True if successful. "success": (Boolean) // True if successful.
"message": (String) // A summary of the inference operation. "message": (String) // A summary of the inference operation.
@ -69,82 +32,31 @@ example below)* so it can be used as a **drop-in replacement** for the CodeProje
} }
``` ```
### /v1/vision/alpr_grid_debug
> POST: http://localhost:5000/v1/vision/alpr_grid_debug
**Description**
This endpoint displays the grid and each cell's number on the image.
It is intended to be used for debugging purposes to see which cells are being processed.
**Parameters**
*same as [v1/vision/alpr](#v1visionalpr)*
**Response**
```jsonc
{
"image": (Base64) // The image with the grid and cell numbers drawn on it.
}
```
## More information about the grid parameter
When you send an image to the server, sometimes the ALPR software cannot find any plate because the image is too big or
the plate is too small in the image.
To solve this problem, if no plate is found on the whole image, the server will divide the image into a grid of cells
and retry the ALPR software on each cell.
You can specify the size of the grid with the ``grid_size`` parameter in each of your requests.
> [!CAUTION]
> The higher the grid size, the longer the processing time will be. It is recommended to keep the grid size between 3
> and 4.
> Note: The processing time is in no way multiplied by the grid size (usually takes 2x the time)
You can speed up the processing time by specifying the ``wanted_cells`` parameter. This parameter allows you to specify
which cells you want to run plate detection on.
This can be useful if you know the plates can only be in certain areas of the image.
> [!TIP]
> You can use the [``/v1/vision/alpr_grid_debug`` endpoint](#v1visionalpr_grid_debug) to see the grid and cell numbers
> overlaid on your image.
> You can then specify the ``wanted_cells`` parameter to only process the cells you want.
**If you wish not to use the grid, you can set the ``grid_size`` parameter to
0 *(and leave the ``wanted_cells`` parameter empty)*.**
### Example
Let's say your driveway camera looks something like this:
![Driveway camera](.git-assets/example_grid.webp)
If you set the ``grid_size`` parameter to 2, the image will be divided into a 2x2 grid like this:
![Driveway camera grid](.git-assets/example_grid_2.webp)
You can see that cell 1 and 2 are empty and cells 3 and 4 might contain license plates.
You can then set the ``wanted_cells`` parameter to ``3,4`` to only process cells 3 and 4, reducing the processing time
as only half the image will be processed.
## Included models in built executable ## 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
When using the built executable, only the **latin** charset models are bundled by default. If you want to use a charset, you need to set the charset in the JSON_CONFIG variable and rebuild the executable with the according
different charset, you need to set the charset in the JSON_CONFIG variable and rebuild the executable with the models found [here](https://github.com/DoubangoTelecom/ultimateALPR-SDK/tree/master/assets)
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
To build the executable, you can use the ``build_alpr_api.sh`` script, which will create an executable the ``dist`` folder.
named ``alpr_api`` in the ``dist`` folder.
## Setup development environment ## Setup development environment
### Use automatic setup script ### 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.
> [!IMPORTANT] ### Copy necessary files/folders
> Make sure to install the package python3-dev (APT) python3-devel (RPM) before running the build and setup script. Then you need to copy the ``assets`` and ``libs`` folders to the same directory as the script.
> You can use the ``build_and_setup_ultimatealvr.sh`` script to automatically install the necessary packages and build
> the
> ultimateALPR SDK wheel, copy the assets and the libs.
The end structure should look like this: 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 ```bash
. .
├── alpr_api.py ├── alpr_api.py
@ -159,23 +71,8 @@ The end structure should look like this:
``` ```
### Important notes ### Important notes
When building or developing the script, make sure to set the ``LD_LIBRARY_PATH`` environment variable to the libs folder
When running, building or developing the script, make sure to set the ``LD_LIBRARY_PATH`` environment variable to the
libs folder
*(limitation of the ultimateALPR SDK)*. *(limitation of the ultimateALPR SDK)*.
```bash ```bash
export LD_LIBRARY_PATH=libs:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=libs:$LD_LIBRARY_PATH
``` ```
### Error handling
#### GLIBC_ABI_DT_RELR not found
If you encounter an error like this:
```bash
/lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_ABI_DT_RELR' not found
```
Then make sure your GLIBC version is >= 2.36

View File

@ -1,25 +1,14 @@
import base64
import io
import json import json
import logging
import os import os
import sys import sys
import threading import threading
import time from time import sleep
import traceback
import ultimateAlprSdk import ultimateAlprSdk
from PIL import Image, ImageDraw, ImageFont from PIL import Image
from flask import Flask, request, jsonify, render_template from flask import Flask, request, jsonify
# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
counter_lock = threading.Lock()
counter = 0 counter = 0
bundle_dir = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__)))
boot_time = time.time()
""" """
Hi there! Hi there!
@ -30,46 +19,32 @@ information. The server is created using Flask and the ultimateALPR SDK is used
See the README.md file for more information on how to run this script. See the README.md file for more information on how to run this script.
""" """
# Load configuration # Defines the default JSON configuration. More information at https://www.doubango.org/SDKs/anpr/docs/Configuration_options.html
CONFIG_PATH = os.path.join(bundle_dir, JSON_CONFIG = {
'config.json') # TODO: store config file outside of bundle (to remove need for compilation by users) "debug_level": "info",
if os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH, 'r') as config_file:
JSON_CONFIG = json.load(config_file)
else:
JSON_CONFIG = {
"assets_folder": os.path.join(bundle_dir, "assets"),
"charset": "latin",
"car_noplate_detect_enabled": False,
"ienv_enabled": False,
"openvino_enabled": True,
"openvino_device": "CPU",
"npu_enabled": False,
"klass_lpci_enabled": False,
"klass_vcr_enabled": False,
"klass_vmmr_enabled": False,
"klass_vbsr_enabled": False,
"license_token_file": "",
"license_token_data": "",
"debug_level": "fatal",
"debug_write_input_image_enabled": False, "debug_write_input_image_enabled": False,
"debug_internal_data_path": ".", "debug_internal_data_path": ".",
"num_threads": -1, "num_threads": -1,
"gpgpu_enabled": True, "gpgpu_enabled": True,
"max_latency": -1, "max_latency": -1,
"klass_vcr_gamma": 1.5, "klass_vcr_gamma": 1.5,
"detect_roi": [0, 0, 0, 0], "detect_roi": [0, 0, 0, 0],
"detect_minscore": 0.35, "detect_minscore": 0.35,
"car_noplate_detect_min_score": 0.8, "car_noplate_detect_min_score": 0.8,
"pyramidal_search_enabled": False,
"pyramidal_search_sensitivity": 0.38, "pyramidal_search_enabled": True,
"pyramidal_search_sensitivity": 0.38, # default 0.28
"pyramidal_search_minscore": 0.8, "pyramidal_search_minscore": 0.8,
"pyramidal_search_min_image_size_inpixels": 800, "pyramidal_search_min_image_size_inpixels": 800,
"recogn_rectify_enabled": True,
"recogn_rectify_enabled": True, # heavy on cpu
"recogn_minscore": 0.4, "recogn_minscore": 0.4,
"recogn_score_type": "min" "recogn_score_type": "min"
} }
IMAGE_TYPES_MAPPING = { IMAGE_TYPES_MAPPING = {
'RGB': ultimateAlprSdk.ULTALPR_SDK_IMAGE_TYPE_RGB24, 'RGB': ultimateAlprSdk.ULTALPR_SDK_IMAGE_TYPE_RGB24,
@ -77,42 +52,41 @@ IMAGE_TYPES_MAPPING = {
'L': ultimateAlprSdk.ULTALPR_SDK_IMAGE_TYPE_Y 'L': ultimateAlprSdk.ULTALPR_SDK_IMAGE_TYPE_Y
} }
config = json.dumps(JSON_CONFIG)
def start_backend_loop():
global boot_time, counter
while True:
load_engine()
# loop for about an hour or 3000 requests then reload the engine (fix for trial license)
while counter < 3000 and time.time() - boot_time < 3600:
# every 120 sec
if int(time.time()) % 120 == 0:
if not is_engine_loaded():
unload_engine()
load_engine()
time.sleep(1)
unload_engine()
# Reset counter and boot_time to restart the loop
with counter_lock:
counter = 0
boot_time = time.time()
def is_engine_loaded():
# hacky way to check if the engine is loaded cause the SDK doesn't provide a method for it
return ultimateAlprSdk.UltAlprSdkEngine_requestRuntimeLicenseKey().isOK()
def load_engine(): def load_engine():
result = ultimateAlprSdk.UltAlprSdkEngine_init(config) 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(): if not result.isOK():
raise RuntimeError("Init failed: %s" % result.phrase()) raise RuntimeError("Init failed: %s" % result.phrase())
while counter < 3000:
sleep(1)
unload_engine()
load_engine()
def unload_engine(): def unload_engine():
result = ultimateAlprSdk.UltAlprSdkEngine_deInit() result = ultimateAlprSdk.UltAlprSdkEngine_deInit()
@ -122,121 +96,57 @@ def unload_engine():
def process_image(image: Image) -> str: def process_image(image: Image) -> str:
global counter global counter
with counter_lock:
counter += 1 counter += 1
width, height = image.size width, height = image.size
image_type = IMAGE_TYPES_MAPPING.get(image.mode, None)
if image_type is None: if image.mode in IMAGE_TYPES_MAPPING:
raise ValueError(f"Invalid mode: {image.mode}") image_type = IMAGE_TYPES_MAPPING[image.mode]
else:
raise ValueError("Invalid mode: %s" % image.mode)
result = ultimateAlprSdk.UltAlprSdkEngine_process( result = ultimateAlprSdk.UltAlprSdkEngine_process(
image_type, image.tobytes(), width, height, 0, 1 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(): if not result.isOK():
raise RuntimeError(f"Process failed: {result.phrase()}") raise RuntimeError("Process failed: %s" % result.phrase())
else:
return result.json() return result.json()
def create_rest_server_flask(): def create_rest_server_flask():
app = Flask(__name__, template_folder=os.path.join(bundle_dir, 'templates')) app = Flask(__name__)
@app.route('/v1/image/alpr', methods=['POST']) @app.route('/v1/<string:domain>/<string:module>', methods=['POST'])
def alpr(): def alpr(domain, module):
""" # Only care about the ALPR endpoint
The function receives an image and processes it using the ultimateALPR SDK. if domain == 'image' and module == 'alpr':
Parameters:
- upload: The image to be processed
- grid_size: The number of cells to split the image into (e.g. 3)
- wanted_cells: The cells to process in the grid separated by commas (e.g. 1,2,3,4) (max: grid_size²)
"""
interference = time.time()
try:
if 'upload' not in request.files: if 'upload' not in request.files:
return jsonify({'error': 'No image found'}), 400 return jsonify({'error': 'No image found'})
grid_size = int(request.form.get('grid_size', 3)) image = request.files['upload']
wanted_cells = request.form.get('wanted_cells') if image.filename == '':
if wanted_cells: return jsonify({'error': 'No selected file'})
wanted_cells = [int(cell) for cell in wanted_cells.split(',')]
else:
wanted_cells = list(range(1, grid_size * grid_size + 1))
image_file = request.files['upload'] image = Image.open(image)
if image_file.filename == '':
return jsonify({'error': 'No selected file'}), 400
image = Image.open(image_file)
result = process_image(image) result = process_image(image)
result = convert_to_cpai_compatible(result) result = convert_to_cpai_compatible(result)
if not result['predictions']:
logger.debug("No plate found, attempting grid split")
predictions_found = find_best_plate_with_grid_split(image, grid_size, wanted_cells)
if predictions_found:
result['predictions'].append(max(predictions_found, key=lambda x: x['confidence']))
if result['predictions']:
isolated_plate_image = isolate_plate_in_image(image, result['predictions'][0])
result['image'] = f"data:image/png;base64,{image_to_base64(isolated_plate_image, compress=True)}"
process_ms = round((time.time() - interference) * 1000, 2)
result.update({'processMs': process_ms, 'inferenceMs': process_ms})
return jsonify(result) return jsonify(result)
except Exception as e:
logger.error(f"Error processing image: {e}")
logger.error(traceback.format_exc())
return jsonify({'error': 'Error processing image'}), 500
@app.route('/v1/image/alpr_grid_debug', methods=['POST'])
def alpr_grid_debug():
"""
The function receives an image and returns it with the grid overlayed on it (for debugging purposes).
Parameters:
- upload: The image to be processed
- grid_size: The number of cells to split the image into (e.g. 3)
- wanted_cells: The cells to process in the grid separated by commas (e.g. 1,2,3,4) (max: grid_size²)
Returns:
- The image with the grid overlayed on it
"""
try:
if 'upload' not in request.files:
return jsonify({'error': 'No image found'}), 400
grid_size = int(request.form.get('grid_size', 3))
wanted_cells = request.form.get('wanted_cells')
if wanted_cells:
wanted_cells = [int(cell) for cell in wanted_cells.split(',')]
else: else:
wanted_cells = list(range(1, grid_size * grid_size + 1)) return jsonify({'error': 'Endpoint not implemented'}), 404
image_file = request.files['upload']
if image_file.filename == '':
return jsonify({'error': 'No selected file'}), 400
image = Image.open(image_file)
image = draw_grid_and_cell_numbers_on_image(image, grid_size, wanted_cells)
image_base64 = image_to_base64(image, compress=True)
return jsonify({"image": f"data:image/png;base64,{image_base64}"})
except Exception as e:
logger.error(f"Error processing image: {e}")
logger.error(traceback.format_exc())
return jsonify({'error': 'Error processing image'}), 500
@app.route('/')
def index():
return render_template('index.html')
return app return app
def convert_to_cpai_compatible(result): def convert_to_cpai_compatible(result):
result = json.loads(result) result = json.loads(result)
response = { response = {
'success': "true", 'success': "true",
'processMs': result['duration'], 'processMs': result['duration'],
@ -254,131 +164,34 @@ def convert_to_cpai_compatible(result):
'timestamp': '' 'timestamp': ''
} }
for plate in result.get('plates', []): if 'plates' in result:
plates = result['plates']
for plate in plates:
warpedBox = plate['warpedBox'] warpedBox = plate['warpedBox']
x_coords = warpedBox[0::2] x_coords = warpedBox[0::2]
y_coords = warpedBox[1::2] y_coords = warpedBox[1::2]
x_min, x_max = min(x_coords), max(x_coords) x_min = min(x_coords)
y_min, y_max = min(y_coords), max(y_coords) x_max = max(x_coords)
y_min = min(y_coords)
y_max = max(y_coords)
response['predictions'].append({ response['predictions'].append({
'confidence': plate['confidences'][0] / 100, 'confidence': plate['confidence'] / 100,
'label': f"Plate: {plate['text']}", 'label': "Plate: " + plate['text'],
'plate': plate['text'], 'plate': plate['text'],
'x_min': x_min, 'x_min': x_min,
'x_max': x_max, 'x_max': x_max,
'y_min': y_min, 'y_min': y_min,
'y_max': y_max 'y_max': y_max
}) })
return response return response
def draw_grid_and_cell_numbers_on_image(image: Image, grid_size: int = 3, wanted_cells: list = None) -> Image:
if grid_size < 1:
grid_size = 1
if wanted_cells is None:
wanted_cells = list(range(1, grid_size * grid_size + 1))
width, height = image.size
cell_width = width // grid_size
cell_height = height // grid_size
draw = ImageDraw.Draw(image)
font = ImageFont.truetype(os.path.join(bundle_dir, 'assets', 'fonts', 'GlNummernschildEng-XgWd.ttf'),
image.size[0] // 10)
for cell_index in range(1, grid_size * grid_size + 1):
row = (cell_index - 1) // grid_size
col = (cell_index - 1) % grid_size
left = col * cell_width
upper = row * cell_height
right = left + cell_width
lower = upper + cell_height
if cell_index in wanted_cells:
draw.rectangle([left, upper, right, lower], outline="red", width=4)
draw.text((left + 5, upper + 5), str(cell_index), fill="red", font=font)
return image
def find_best_plate_with_grid_split(image: Image, grid_size: int = 3, wanted_cells: list = None):
if grid_size < 1:
logger.debug("Grid size < 1, skipping split")
return []
if wanted_cells is None:
wanted_cells = list(range(1, grid_size * grid_size + 1))
predictions_found = []
width, height = image.size
cell_width = width // grid_size
cell_height = height // grid_size
for cell_index in range(1, grid_size * grid_size + 1):
row = (cell_index - 1) // grid_size
col = (cell_index - 1) % grid_size
left = col * cell_width
upper = row * cell_height
right = left + cell_width
lower = upper + cell_height
if cell_index in wanted_cells:
cell_image = image.crop((left, upper, right, lower))
result_cell = json.loads(process_image(cell_image))
for plate in result_cell.get('plates', []):
warpedBox = plate['warpedBox']
x_coords = warpedBox[0::2]
y_coords = warpedBox[1::2]
x_min = min(x_coords) + left
x_max = max(x_coords) + left
y_min = min(y_coords) + upper
y_max = max(y_coords) + upper
predictions_found.append({
'confidence': plate['confidences'][0] / 100,
'label': f"Plate: {plate['text']}",
'plate': plate['text'],
'x_min': x_min,
'x_max': x_max,
'y_min': y_min,
'y_max': y_max
})
return predictions_found
def isolate_plate_in_image(image: Image, plate: dict) -> Image:
x_min, x_max = plate['x_min'], plate['x_max']
y_min, y_max = plate['y_min'], plate['y_max']
offset = 10
cropped_image = image.crop((max(0, x_min - offset), max(0, y_min - offset), min(image.size[0], x_max + offset),
min(image.size[1], y_max + offset)))
resized_image = cropped_image.resize((int(cropped_image.size[0] * 3), int(cropped_image.size[1] * 3)),
resample=Image.Resampling.LANCZOS)
return resized_image
def image_to_base64(img: Image, compress=False):
"""Convert a Pillow image to a base64-encoded string."""
buffered = io.BytesIO()
if compress:
img = img.resize((img.size[0] // 2, img.size[1] // 2))
img.save(buffered, format="WEBP", quality=35, lossless=False)
else:
img.save(buffered, format="WEBP")
return base64.b64encode(buffered.getvalue()).decode('utf-8')
if __name__ == '__main__': if __name__ == '__main__':
engine_thread = threading.Thread(target=start_backend_loop, daemon=True) engine = threading.Thread(target=load_engine, daemon=True)
engine_thread.start() engine.start()
app = create_rest_server_flask() app = create_rest_server_flask()
app.run(host='0.0.0.0', port=5000) app.run(host='0.0.0.0', port=5000)

View File

@ -1,14 +1 @@
#!/bin/bash pyinstaller --noconfirm --onefile --console --add-data libs:. --add-data assets:assets --name alpr_api "alpr_api.py"
VERSION=1.5.0
rm -rf buildenv build dist *.spec
python3.10 -m venv buildenv
source buildenv/bin/activate
python3.10 -m pip install --upgrade pip pyinstaller
python3.10 -m pip install ./wheel/ultimateAlprSdk-3.0.0-cp310-cp310-linux_x86_64.whl
pip install -r requirements.txt
pyinstaller --noconfirm --onefile --console --add-data libs:. --add-data assets:assets --add-data static:static --add-data templates:templates --name easy-local-alpr-$VERSION-openvinocpu_linux_x86_64 "alpr_api.py"
deactivate
rm -rf buildenv

View File

@ -1,184 +1,58 @@
#!/bin/bash # 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
deactivate 2>/dev/null mkdir temp-sdk
mv ultimateALPR-SDK*/* ./temp-sdk
rm -R ultimateALPR-SDK*
# Function to create virtual environment, install the wheel, and copy assets and libs # create env to build ultimatealpr-sdk for python
install_and_setup() { python3.10 -m venv venv
echo "Creating virtual environment at the root..." source venv/bin/activate
python3.10 -m venv "$ROOT_DIR/venv" >/dev/null 2>&1 pip install setuptools wheel Cython
if [ $? -ne 0 ]; then
echo "Failed to create virtual environment."
exit 1
fi
echo "Activating virtual environment..." cd temp-sdk
source "$ROOT_DIR/venv/bin/activate"
echo "Installing the wheel..." # move folders to simplify build
pip install "$BUILD_DIR"/ultimateAlprSdk-*.whl >/dev/null 2>&1 mkdir -p binaries/linux/x86_64/c++
if [ $? -ne 0 ]; then cp c++/* binaries/linux/x86_64/c++
echo "Failed to install the wheel." cp python/* binaries/linux/x86_64/
exit 1
fi
echo "Deactivating virtual environment..." # edit setup.py to simplify build
deactivate cd binaries/linux/x86_64/
echo "Copying assets and libs folders to the root directory..."
cp -r "$BUILD_DIR/assets" "$ROOT_DIR"
cp -r "$BUILD_DIR/libs" "$ROOT_DIR"
if [ -f "$ROOT_DIR/requirements.txt" ]; then
echo "Installing requirements..."
source "$ROOT_DIR/venv/bin/activate"
pip install -r "$ROOT_DIR/requirements.txt" >/dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Failed to install requirements."
exit 1
fi
deactivate
fi
rm -rf "$BUILD_DIR"
echo "Virtual environment created and wheel installed successfully."
echo "Assets and libs folders copied to the root directory."
echo "Setup completed."
}
# Function to prompt user for auto setup choice
prompt_auto_setup() {
read -r -p "Do you want to automatically create a new virtual environment, install the wheel and copy the assets and libs? (y/n): " choice
case "$choice" in
y|Y ) install_and_setup;;
n|N ) echo "Setup completed.";;
* ) echo "Invalid choice. Please run the script again and choose y or n.";;
esac
}
# Variables
ROOT_DIR=$(pwd)
BUILD_DIR="$ROOT_DIR/tmp-build-env"
SDK_ZIP_URL="https://github.com/DoubangoTelecom/ultimateALPR-SDK/archive/8130c76140fe8edc60fe20f875796121a8d22fed.zip"
SDK_ZIP="$BUILD_DIR/temp-sdk.zip"
SDK_DIR="$BUILD_DIR/temp-sdk"
BIN_DIR="$SDK_DIR/binaries/linux/x86_64"
# Create build environment
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR" || exit
# Clone SDK
echo "Downloading SDK..."
if [ -f "$SDK_ZIP" ]; then
echo "SDK zip already exists."
rm -R "$SDK_DIR"
else
wget "$SDK_ZIP_URL" -O "$SDK_ZIP" >/dev/null 2>&1
fi
if [ $? -ne 0 ]; then
echo "Failed to download SDK."
exit 1
fi
echo "Unzipping SDK..."
unzip "$SDK_ZIP" >/dev/null 2>&1
rm "$SDK_ZIP"
mkdir "$SDK_DIR"
mv ultimateALPR-SDK*/* "$SDK_DIR"
rm -r ultimateALPR-SDK*
# Create environment to build ultimatealpr-sdk for Python
echo "Creating virtual environment for building SDK..."
python3.10 -m venv "$BUILD_DIR/venv" >/dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Failed to create virtual environment."
exit 1
fi
echo "Activating virtual environment..."
source "$BUILD_DIR/venv/bin/activate"
echo "Installing build dependencies..."
pip install setuptools wheel Cython >/dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Failed to install build dependencies."
exit 1
fi
# Move folders to simplify build
mkdir -p "$BIN_DIR/c++"
mv "$SDK_DIR/c++"/* "$BIN_DIR/c++"
mv "$SDK_DIR/python"/* "$BIN_DIR/"
# Edit setup.py to simplify build
echo "Editing setup.py for simplified build..."
cd "$BIN_DIR" || exit
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|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|include_dirs=\['../../../c++'\]|include_dirs=['c++']|g" setup.py
sed -i "s|library_dirs=\['.'\]|library_dirs=['libs']|g" setup.py sed -i "s|library_dirs=\['.'\]|library_dirs=['libs']|g" setup.py
# Move all .so files into libs folder # move all .so files into libs folder
mkdir "$BIN_DIR/libs" mkdir libs
mv "$BIN_DIR/"*.so* "$BIN_DIR/libs" mv *.so libs/
mv *.so.* libs/
# Download TensorFlow # build the wheel
read -r -p "Do you want TensorFlow for CPU or GPU? (cpu/gpu): " tf_choice python setup.py bdist_wheel -v
mkdir -p "$BIN_DIR/tensorflow"
if [ "$tf_choice" == "gpu" ]; then
echo "Downloading TensorFlow GPU..."
wget https://storage.googleapis.com/tensorflow/libtensorflow/libtensorflow-gpu-linux-x86_64-2.6.0.tar.gz >/dev/null 2>&1 # Use 2.6 for newer GPU support
if [ $? -ne 0 ]; then
echo "Failed to download TensorFlow GPU."
exit 1
fi
echo "Extracting TensorFlow GPU..."
tar -xf libtensorflow-gpu-linux-x86_64-2.6.0.tar.gz -C "$BIN_DIR/tensorflow" >/dev/null 2>&1
mv "$BIN_DIR/tensorflow/lib/libtensorflow.so.1" "$BIN_DIR/libs/libtensorflow.so.1" # move the built whl and the libs back to root dir
mv "$BIN_DIR/tensorflow/lib/libtensorflow_framework.so.2.6.0" "$BIN_DIR/libs/libtensorflow_framework.so.2" mv dist/* ../../../../
else mv libs ../../../../
echo "Downloading TensorFlow CPU..."
#wget https://storage.googleapis.com/tensorflow/libtensorflow/libtensorflow-cpu-linux-x86_64-2.6.0.tar.gz >/dev/null 2>&1
wget https://storage.googleapis.com/tensorflow/libtensorflow/libtensorflow-cpu-linux-x86_64-1.14.0.tar.gz >/dev/null 2>&1 # Use 1.14 as it's smaller in size
if [ $? -ne 0 ]; then
echo "Failed to download TensorFlow CPU."
exit 1
fi
echo "Extracting TensorFlow CPU..."
tar -xf libtensorflow-cpu-linux-x86_64-1.14.0.tar.gz -C "$BIN_DIR/tensorflow" >/dev/null 2>&1
mv "$BIN_DIR/tensorflow/lib/"* "$BIN_DIR/libs/" # move the assets to root dir
fi cd ../../../
mv assets ../assets
# Build the wheel ## install the whl
echo "Building the wheel..." #cd ..
python "$BIN_DIR/setup.py" bdist_wheel -v >/dev/null 2>&1 #pip install *.whl
if [ $? -ne 0 ]; then #rm *.whl
echo "Failed to build the wheel."
exit 1
fi
# Move the built wheel and the libs back to the root directory cd ../
mv "$BIN_DIR/dist/"*.whl "$BUILD_DIR"
mv "$BIN_DIR/libs" "$BUILD_DIR"
mv "$BIN_DIR/plugins.xml" "$BUILD_DIR/libs"
# Move the assets to the root directory # remove sdk
mv "$SDK_DIR/assets" "$BUILD_DIR/assets" rm -R temp-sdk
# Deactivate and clean up the build virtual environment echo "UltimateALPR SDK built and setup successfully"
echo "Deactivating and cleaning up virtual environment..." echo "You can now install the wheel using 'pip install ultimateAlprSdk-*.whl'"
deactivate
cd "$ROOT_DIR" || exit
rm -rf "$BUILD_DIR/venv"
rm -rf "$SDK_DIR"
# Inform the user of the successful build
echo "UltimateALPR SDK built and setup successfully."
echo "You can now create a virtual environment, install the wheel and copy the assets and libs and start developing. Say 'y' to the next prompt to do this automatically (recommended)."
echo "Tip: Look at the assets folder as you might not need all the models depending on your platform/use case."
# Prompt user for auto setup choice
prompt_auto_setup

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -1,323 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Easy Local ALPR - API</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap" rel="stylesheet">
<style>
body {
background-color: #f0f2f5;
background-image: radial-gradient(#7c7c7c 1px, rgba(0, 0, 0, 0) 1px);
background-size: 20px 20px;
font-family: 'Google Sans', sans-serif;
}
.grid-cell {
border: 2px solid #e5e7eb; /* Tailwind gray-200 */
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 0.5rem; /* Rounded corners */
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease; /* Smooth transition */
padding-top: 25%; /* More compact rectangular shape */
position: relative;
overflow: hidden;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); /* Subtle shadow for modern look */
}
.grid-cell span {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: 500; /* Semi-bold text */
}
.grid-cell.selected {
background-color: #1f2937; /* Tailwind gray-800 */
color: white;
border-color: #1f2937; /* Match border color with background */
}
.grid-cell:hover {
background-color: #9ca3af; /* Tailwind gray-400 for hover effect */
color: white;
border-color: #9ca3af; /* Match border color with hover effect */
}
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(50px, 1fr));
gap: 8px; /* Adjust gap between cells */
margin-top: 1rem;
}
</style>
</head>
<body class="bg-neutral-100 dark:bg-neutral-900 dark:text-white flex items-center justify-center min-h-screen p-4">
<div class="absolute top-4 left-4 z-50">
<img alt="Logo" class="h-12 dark:hidden" id="logo" src="{{ url_for('static', filename='logo_black.webp') }}">
<img alt="Logo" class="h-12 hidden dark:block" id="logoDark"
src="{{ url_for('static', filename='logo_white.webp') }}">
</div>
<div class="bg-white dark:bg-neutral-800 p-6 rounded-lg shadow-lg w-full max-w-xl mt-16">
<h1 class="text-2xl font-bold mb-4 text-center dark:text-gray-200">Select Service</h1>
<form class="space-y-4" id="serviceForm">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="service">Choose a
service:</label>
<select class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white dark:bg-neutral-800 dark:border-neutral-700 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
id="service" name="service" onchange="updateFormFields()">
<option value="alpr">Plate Recognition (ALPR)</option>
<option value="alpr_grid_debug">Grid Size Helper</option>
</select>
</div>
<div class="service-fields hidden" id="alprFields">
<div>
<label for="upload_alpr" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Choose an
image:</label>
<div class="mt-1 flex items-center">
<input type="file" id="upload_alpr" name="upload" accept="image/*" class="hidden"
onchange="updateFileName();">
<label for="upload_alpr"
class="cursor-pointer inline-flex items-center justify-center px-4 py-2 border border-gray-400 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-neutral-800 hover:bg-neutral-50 dark:hover:bg-neutral-600">Select
file</label>
<span id="fileName_alpr" class="ml-2 text-sm text-gray-600 dark:text-gray-300"></span>
</div>
</div>
<div>
<label for="grid_size_alpr" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Grid
Size:</label>
<input type="number" id="grid_size_alpr" name="grid_size" value="3"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-neutral-800 dark:border-neutral-700">
</div>
<div>
<label for="wanted_cells_alpr" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Wanted
Cells:</label>
<div id="gridContainer_alpr" class="grid-container"></div>
<input type="hidden" id="wanted_cells_alpr" name="wanted_cells">
</div>
<input id="plate_image_alpr" name="plate_image_alpr" type="hidden" value="true">
</div>
<div class="service-fields hidden" id="alpr_grid_debugFields">
<div>
<label for="upload_alpr_grid_debug" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Choose
an image:</label>
<div class="mt-1 flex items-center">
<input type="file" id="upload_alpr_grid_debug" name="upload" accept="image/*" class="hidden"
onchange="updateFileName();">
<label for="upload_alpr_grid_debug"
class="cursor-pointer inline-flex items-center justify-center px-4 py-2 border border-gray-400 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-neutral-800 hover:bg-neutral-50 dark:hover:bg-neutral-600">Select
file</label>
<span id="fileName_alpr_grid_debug" class="ml-2 text-sm text-gray-600 dark:text-gray-300"></span>
</div>
</div>
<div>
<label for="grid_size_alpr_grid_debug"
class="block text-sm font-medium text-gray-700 dark:text-gray-300">Grid Size:</label>
<input type="number" id="grid_size_alpr_grid_debug" name="grid_size" value="3"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-neutral-800 dark:border-neutral-700">
</div>
<div>
<label for="wanted_cells_alpr_grid_debug"
class="block text-sm font-medium text-gray-700 dark:text-gray-300">Wanted Cells:</label>
<div id="gridContainer_alpr_grid_debug" class="grid-container"></div>
<input type="hidden" id="wanted_cells_alpr_grid_debug" name="wanted_cells">
</div>
</div>
<div id="imagePreview" class="mt-4 hidden">
<label id="imagePreviewLabel"
class="block text sm font-medium text-gray-700 dark:text-gray-300">Preview:</label>
<img id="previewImage" src="#" alt="Preview" class="max-w-full h-auto rounded-lg">
</div>
<button class="w-full py-2 px-4 bg-black text-white font-semibold rounded-md shadow-sm hover:bg-neutral-900 dark:bg-neutral-900 dark:hover:bg-neutral-950"
id="submitButton" type="submit">Submit
</button>
</form>
<div class="mt-6">
<h2 class="text-xl font-semibold mb-2 dark:text-gray-200">
Response
<span class="text-sm font-normal" id="timer"></span>
</h2>
<pre class="bg-neutral-100 dark:bg-neutral-900 p-4 border rounded-lg text-sm text-gray-900 dark:text-gray-200 overflow-x-auto"
id="responseBox"></pre>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
function updateFormFields() {
const service = document.getElementById('service').value;
localStorage.setItem('selectedService', service);
document.querySelectorAll('.service-fields').forEach(field => {
field.classList.add('hidden');
field.querySelectorAll('input, select').forEach(field => field.disabled = true);
});
const selectedServiceFields = document.getElementById(service + 'Fields');
selectedServiceFields.classList.remove('hidden');
selectedServiceFields.querySelectorAll('input, select').forEach(field => field.disabled = false);
['responseBox', 'timer', 'fileName_' + service, 'previewImage', 'imagePreview', 'upload_' + service]
.forEach(id => {
const element = document.getElementById(id);
if (element) {
if (element.tagName === 'DIV') element.classList.add('hidden');
if (element.tagName === 'INPUT') element.value = '';
if (element.tagName === 'SPAN' || element.tagName === 'PRE') element.textContent = '';
if (element.tagName === 'IMG') element.src = '';
}
});
updateGrid(service);
}
function initializeForm() {
const savedService = localStorage.getItem('selectedService');
if (savedService) {
document.getElementById('service').value = savedService;
updateFormFields();
}
}
function toggleLogo() {
const logo = document.getElementById('logo');
const logoDark = document.getElementById('logoDark');
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
logo.style.display = 'none';
logoDark.style.display = 'block';
} else {
logo.style.display = 'block';
logoDark.style.display = 'none';
}
}
function updateFileName() {
const service = document.getElementById('service').value;
const input = document.getElementById('upload_' + service);
const fileName = document.getElementById('fileName_' + service);
const imagePreview = document.getElementById('imagePreview');
const previewImage = document.getElementById('previewImage');
const imagePreviewLabel = document.getElementById('imagePreviewLabel');
fileName.textContent = input.files[0] ? input.files[0].name : '';
imagePreviewLabel.textContent = 'Preview:';
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = (e) => {
previewImage.src = e.target.result;
imagePreview.classList.remove('hidden');
}
reader.readAsDataURL(input.files[0]);
}
}
function updateGrid(service) {
const gridSize = parseInt(document.getElementById('grid_size_' + service).value);
const gridContainer = document.getElementById('gridContainer_' + service);
gridContainer.innerHTML = '';
gridContainer.style.gridTemplateColumns = `repeat(${gridSize}, minmax(0, 1fr))`;
const wantedCellsInput = document.getElementById('wanted_cells_' + service);
const selectedCells = wantedCellsInput.value ? wantedCellsInput.value.split(',').map(Number) : [];
for (let i = 0; i < gridSize * gridSize; i++) {
const cell = document.createElement('div');
cell.classList.add('grid-cell');
const cellSpan = document.createElement('span');
cellSpan.textContent = i + 1;
cell.appendChild(cellSpan);
if (selectedCells.includes(i + 1)) cell.classList.add('selected');
cell.addEventListener('click', () => {
cell.classList.toggle('selected');
updateWantedCells(service);
});
gridContainer.appendChild(cell);
}
}
function updateWantedCells(service) {
const gridContainer = document.getElementById('gridContainer_' + service);
const selectedCells = [];
gridContainer.querySelectorAll('.grid-cell.selected').forEach(cell => {
selectedCells.push(cell.textContent);
});
document.getElementById('wanted_cells_' + service).value = selectedCells.join(',');
}
$(document).ready(function () {
initializeForm();
toggleLogo();
window.matchMedia("(prefers-color-scheme: dark)").addEventListener('change', toggleLogo);
$('#grid_size_alpr, #grid_size_alpr_grid_debug').on('input', function () {
updateGrid(document.getElementById('service').value);
});
$('#serviceForm').on('submit', function (e) {
e.preventDefault();
const service = $('#service').val();
const formData = new FormData(this);
var url;
if (service === 'alpr') {
url = '/v1/image/alpr';
type = 'POST';
} else if (service === 'alpr_grid_debug') {
url = '/v1/image/alpr_grid_debug';
type = 'POST';
}
$('#submitButton').prop('disabled', true).html('<svg class="animate-spin h-5 w-5 text-white mx-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M12 2a10 10 0 00-8 4.9l1.5 1A8 8 0 0112 4V2z"></path></svg>');
const startTime = Date.now();
$.ajax({
url: url,
type: type,
data: formData,
processData: false,
contentType: false,
success: function (data) {
const endTime = Date.now();
const elapsedTime = endTime - startTime;
$('#responseBox').text(JSON.stringify(data, null, 2));
$('#timer').text(`(${elapsedTime} ms)`);
$('#submitButton').prop('disabled', false).text('Submit');
if (data.image) {
$('#previewImage').attr('src', data.image);
$('#imagePreview').removeClass('hidden');
if (service === 'alpr') $('#imagePreviewLabel').text('Identified plate image:');
} else {
updateFileName();
}
},
error: function (xhr) {
const endTime = Date.now();
const elapsedTime = endTime - startTime;
const err = JSON.parse(xhr.responseText);
$('#responseBox').text(JSON.stringify(err, null, 2));
$('#timer').text(`(${elapsedTime} ms)`);
$('#submitButton').prop('disabled', false).text('Submit');
}
});
});
});
</script>
</body>
</html>