Compare commits

...

40 Commits
v1.0.0 ... main

Author SHA1 Message Date
145452b0ae
revert shared libraries stripping, messes with tensorflow and use tensorflow v1 instead of v2 on cpu (smaller size) 2024-08-05 11:32:18 +02:00
59a54945ff
improve setup script, reduce libs .so size (strip) 2024-08-04 20:36:34 +02:00
87e2d6dd7b
add grid UI for wanted_cells 2024-08-04 19:49:41 +02:00
e026c61ea3
add logo 2024-08-04 19:10:36 +02:00
64791828d1
add grey logo 2024-08-04 18:46:55 +02:00
b15b05c157
update readme 2024-08-04 18:46:29 +02:00
b8c04b133d
update readme 2024-08-04 18:44:03 +02:00
7d31c06a9f
update readme 2024-08-04 18:43:40 +02:00
8bc5187d5e
update readme 2024-08-04 18:41:20 +02:00
224d566d65
update readme 2024-08-04 13:23:52 +02:00
8d5c55fb88
some refactor 2024-08-04 13:09:19 +02:00
9cf457511e
some refactor 2024-08-04 12:58:59 +02:00
0d7351d651
add grid_debug endpoint, previews, try/except to avoid errors 2024-08-04 12:31:51 +02:00
6976f690ff
improve structure/readability, add config file 2024-08-01 12:03:57 +02:00
d81d955a8b
fix recursion error mess 2024-08-01 11:47:01 +02:00
a4d297fc33
improve engine reload system 2024-07-30 11:31:12 +02:00
cd0caed325
restart engien after 6 hours 2024-07-26 10:26:09 +02:00
e63ff4a220
update readme 2024-07-22 22:52:24 +02:00
cb0c4145de
update readme 2024-07-22 22:49:22 +02:00
4e9e956589
add grid size parameter 2024-07-22 22:38:54 +02:00
29e1d98294
add preview 2024-07-20 23:08:09 +02:00
899d558c2d
fix tensorflow setup libs issue 2024-07-19 22:34:22 +02:00
4d9621920f
change html page title 2024-07-19 21:43:47 +02:00
c692f88fce
improve setup script and update readme accordingly 2024-07-19 21:42:47 +02:00
a9807154bc
improve response webui 2024-07-19 21:42:16 +02:00
c124d3c45b
updated readme 2024-07-19 19:12:24 +02:00
1648f7b452
fixed openvino and made it default 2024-07-18 11:55:50 +02:00
645ae397b4
fix template error in bundled onefile exec 2024-07-17 23:00:09 +02:00
aa0cf11dd8 Merge pull request 'fix detached head' (#2) from detached2 into main
Reviewed-on: #2
2024-07-17 20:48:13 +00:00
0479a48ae2 Merge pull request 'detached' (#1) from detached into main
Reviewed-on: #1
2024-07-17 20:47:53 +00:00
42b1571ee2
improve splitting 2024-07-17 22:46:00 +02:00
b1b762ce9c
improve readability 2024-07-17 22:31:22 +02:00
549aba963a
add splitting system (3x3) if nothing found 2024-07-17 22:24:32 +02:00
1aa018f7d5
improved webui 2024-07-17 22:02:50 +02:00
b21b24cc17
added nice webui on / 2024-07-17 21:14:01 +02:00
70d5a9d543
rename build exec 2024-07-17 19:20:11 +02:00
e364a7fb59
Merge remote-tracking branch 'origin/main' 2024-07-17 19:19:32 +02:00
c6f0817d16
fix confidence 2024-07-17 19:19:26 +02:00
579946ae61 Actualiser README.md 2024-07-17 17:05:46 +00:00
66b839e78f
add prebuitl wheel 2024-07-17 18:59:20 +02:00
13 changed files with 936 additions and 184 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
.git-assets/logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

167
README.md
View File

@ -1,28 +1,65 @@
# Easy local ALPR (Automatic License Plate Recognition) ![logo](.git-assets/logo.webp)
![ALPR](.git-assets/preview-webui.webp)
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 # Easy Local ALPR (Automatic License Plate Recognition)
information. The server is created using Flask and the ultimateALPR SDK is used to process the images. This project is a simple local ALPR (Automatic License Plate Recognition) server that uses the [ultimateALPR-SDK](https://github.com/DoubangoTelecom/ultimateALPR-SDK) to
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]
> 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: > 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 last character of the license plate is masked with an asterisk > For any commercial use, you will need to take a look at their licensing terms.
> - 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.** > **I am not affiliated with ultimateALPR-SDK in any way, and I am not responsible for any misuse of the software.**
> [!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).
> POST: http://localhost:32168/v1/vision/alpr The server listens on port 5000 and has a few endpoints documented below, the most important one being [``/v1/image/alpr``](#v1visionalpr).
### /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.
@ -32,31 +69,82 @@ as a drop-in replacement for the [CodeProject AI ALPR API](https://www.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
charset, you need to set the charset in the JSON_CONFIG variable and rebuild the executable with the according When using the built executable, only the **latin** charset models are bundled by default. If you want to use a
models found [here](https://github.com/DoubangoTelecom/ultimateALPR-SDK/tree/master/assets) different charset, you need to set the charset in the JSON_CONFIG variable and rebuild the executable with the
To build the executable, you can use the ``build_alpr_api.sh`` script, which will create an executable named ``alpr_api`` in according models found [here](https://github.com/DoubangoTelecom/ultimateALPR-SDK/tree/master/assets)
the ``dist`` folder. 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 ## Setup development environment
### Install ultimateALPR SDK ### Use automatic setup script
#### 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 > [!IMPORTANT]
Then you need to copy the ``assets`` and ``libs`` folders to the same directory as the script. > Make sure to install the package python3-dev (APT) python3-devel (RPM) before running the build and setup 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.
If you built the wheel in the previous step, you can copy the ``assets`` and ``libs`` folders from the ``tmp`` directory. The end structure should look like this:
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
@ -71,8 +159,23 @@ The 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,14 +1,25 @@
import base64
import io
import json import json
import logging
import os import os
import sys import sys
import threading import threading
from time import sleep import time
import traceback
import ultimateAlprSdk import ultimateAlprSdk
from PIL import Image from PIL import Image, ImageDraw, ImageFont
from flask import Flask, request, jsonify from flask import Flask, request, jsonify, render_template
# 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!
@ -19,29 +30,43 @@ 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.
""" """
# Defines the default JSON configuration. More information at https://www.doubango.org/SDKs/anpr/docs/Configuration_options.html # Load configuration
CONFIG_PATH = os.path.join(bundle_dir,
'config.json') # TODO: store config file outside of bundle (to remove need for compilation by users)
if os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH, 'r') as config_file:
JSON_CONFIG = json.load(config_file)
else:
JSON_CONFIG = { JSON_CONFIG = {
"debug_level": "info", "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_enabled": True, "pyramidal_search_sensitivity": 0.38,
"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"
} }
@ -52,40 +77,41 @@ IMAGE_TYPES_MAPPING = {
'L': ultimateAlprSdk.ULTALPR_SDK_IMAGE_TYPE_Y 'L': ultimateAlprSdk.ULTALPR_SDK_IMAGE_TYPE_Y
} }
config = json.dumps(JSON_CONFIG)
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") def start_backend_loop():
JSON_CONFIG["charset"] = "latin" global boot_time, counter
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)) while True:
if not result.isOK(): load_engine()
raise RuntimeError("Init failed: %s" % result.phrase())
while counter < 3000:
sleep(1)
# 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() unload_engine()
load_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():
result = ultimateAlprSdk.UltAlprSdkEngine_init(config)
if not result.isOK():
raise RuntimeError("Init failed: %s" % result.phrase())
def unload_engine(): def unload_engine():
@ -96,57 +122,121 @@ 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.mode in IMAGE_TYPES_MAPPING: if image_type is None:
image_type = IMAGE_TYPES_MAPPING[image.mode] raise ValueError(f"Invalid mode: {image.mode}")
else:
raise ValueError("Invalid mode: %s" % image.mode)
result = ultimateAlprSdk.UltAlprSdkEngine_process( result = ultimateAlprSdk.UltAlprSdkEngine_process(
image_type, image_type, image.tobytes(), width, height, 0, 1
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("Process failed: %s" % result.phrase()) raise RuntimeError(f"Process failed: {result.phrase()}")
else:
return result.json() return result.json()
def create_rest_server_flask(): def create_rest_server_flask():
app = Flask(__name__) app = Flask(__name__, template_folder=os.path.join(bundle_dir, 'templates'))
@app.route('/v1/<string:domain>/<string:module>', methods=['POST']) @app.route('/v1/image/alpr', methods=['POST'])
def alpr(domain, module): def alpr():
# Only care about the ALPR endpoint """
if domain == 'image' and module == 'alpr': The function receives an image and processes it using the ultimateALPR SDK.
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'}) return jsonify({'error': 'No image found'}), 400
image = request.files['upload'] grid_size = int(request.form.get('grid_size', 3))
if image.filename == '': wanted_cells = request.form.get('wanted_cells')
return jsonify({'error': 'No selected file'}) if wanted_cells:
wanted_cells = [int(cell) for cell in wanted_cells.split(',')]
else:
wanted_cells = list(range(1, grid_size * grid_size + 1))
image = Image.open(image) image_file = request.files['upload']
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:
return jsonify({'error': 'Endpoint not implemented'}), 404 wanted_cells = list(range(1, grid_size * grid_size + 1))
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'],
@ -164,21 +254,92 @@ def convert_to_cpai_compatible(result):
'timestamp': '' 'timestamp': ''
} }
if 'plates' in result: for plate in result.get('plates', []):
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 = min(x_coords) x_min, x_max = min(x_coords), max(x_coords)
x_max = max(x_coords) y_min, y_max = min(y_coords), max(y_coords)
y_min = min(y_coords)
y_max = max(y_coords)
response['predictions'].append({ response['predictions'].append({
'confidence': plate['confidence'] / 100, 'confidence': plate['confidences'][0] / 100,
'label': "Plate: " + plate['text'], '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 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'], 'plate': plate['text'],
'x_min': x_min, 'x_min': x_min,
'x_max': x_max, 'x_max': x_max,
@ -186,12 +347,38 @@ def convert_to_cpai_compatible(result):
'y_max': y_max 'y_max': y_max
}) })
return response 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 = threading.Thread(target=load_engine, daemon=True) engine_thread = threading.Thread(target=start_backend_loop, daemon=True)
engine.start() engine_thread.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 +1,14 @@
pyinstaller --noconfirm --onefile --console --add-data libs:. --add-data assets:assets --name alpr_api "alpr_api.py" #!/bin/bash
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,58 +1,184 @@
# clone sdk #!/bin/bash
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 deactivate 2>/dev/null
mv ultimateALPR-SDK*/* ./temp-sdk
rm -R ultimateALPR-SDK*
# create env to build ultimatealpr-sdk for python # Function to create virtual environment, install the wheel, and copy assets and libs
python3.10 -m venv venv install_and_setup() {
source venv/bin/activate echo "Creating virtual environment at the root..."
pip install setuptools wheel Cython python3.10 -m venv "$ROOT_DIR/venv" >/dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Failed to create virtual environment."
exit 1
fi
cd temp-sdk echo "Activating virtual environment..."
source "$ROOT_DIR/venv/bin/activate"
# move folders to simplify build echo "Installing the wheel..."
mkdir -p binaries/linux/x86_64/c++ pip install "$BUILD_DIR"/ultimateAlprSdk-*.whl >/dev/null 2>&1
cp c++/* binaries/linux/x86_64/c++ if [ $? -ne 0 ]; then
cp python/* binaries/linux/x86_64/ echo "Failed to install the wheel."
exit 1
fi
# edit setup.py to simplify build echo "Deactivating virtual environment..."
cd binaries/linux/x86_64/ deactivate
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 libs mkdir "$BIN_DIR/libs"
mv *.so libs/ mv "$BIN_DIR/"*.so* "$BIN_DIR/libs"
mv *.so.* libs/
# build the wheel # Download TensorFlow
python setup.py bdist_wheel -v read -r -p "Do you want TensorFlow for CPU or GPU? (cpu/gpu): " tf_choice
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
# move the built whl and the libs back to root dir mv "$BIN_DIR/tensorflow/lib/libtensorflow.so.1" "$BIN_DIR/libs/libtensorflow.so.1"
mv dist/* ../../../../ mv "$BIN_DIR/tensorflow/lib/libtensorflow_framework.so.2.6.0" "$BIN_DIR/libs/libtensorflow_framework.so.2"
mv libs ../../../../ else
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
# move the assets to root dir mv "$BIN_DIR/tensorflow/lib/"* "$BIN_DIR/libs/"
cd ../../../ fi
mv assets ../assets
## install the whl # Build the wheel
#cd .. echo "Building the wheel..."
#pip install *.whl python "$BIN_DIR/setup.py" bdist_wheel -v >/dev/null 2>&1
#rm *.whl if [ $? -ne 0 ]; then
echo "Failed to build the wheel."
exit 1
fi
cd ../ # Move the built wheel and the libs back to the root directory
mv "$BIN_DIR/dist/"*.whl "$BUILD_DIR"
mv "$BIN_DIR/libs" "$BUILD_DIR"
mv "$BIN_DIR/plugins.xml" "$BUILD_DIR/libs"
# remove sdk # Move the assets to the root directory
rm -R temp-sdk mv "$SDK_DIR/assets" "$BUILD_DIR/assets"
echo "UltimateALPR SDK built and setup successfully" # Deactivate and clean up the build virtual environment
echo "You can now install the wheel using 'pip install ultimateAlprSdk-*.whl'" echo "Deactivating and cleaning up virtual environment..."
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

BIN
static/logo_black.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
static/logo_white.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

323
templates/index.html Normal file
View File

@ -0,0 +1,323 @@
<!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>