add grid_debug endpoint, previews, try/except to avoid errors

This commit is contained in:
Mathieu Broillet 2024-08-04 12:31:51 +02:00
parent 6976f690ff
commit 0d7351d651
Signed by: mathieu
GPG Key ID: C4A6176ABC6B2DFC
2 changed files with 378 additions and 133 deletions

View File

@ -1,12 +1,15 @@
import base64
import io
import json import json
import logging import logging
import os import os
import sys import sys
import threading import threading
import time import time
import traceback
import ultimateAlprSdk import ultimateAlprSdk
from PIL import Image from PIL import Image, ImageDraw, ImageFont
from flask import Flask, request, jsonify, render_template from flask import Flask, request, jsonify, render_template
# Setup logging # Setup logging
@ -149,7 +152,6 @@ def create_rest_server_flask():
@app.route('/v1/image/alpr', methods=['POST']) @app.route('/v1/image/alpr', methods=['POST'])
def alpr(): def alpr():
""" """
This function is called when a POST request is made to the /v1/image/alpr endpoint.
The function receives an image and processes it using the ultimateALPR SDK. The function receives an image and processes it using the ultimateALPR SDK.
Parameters: Parameters:
@ -159,34 +161,91 @@ def create_rest_server_flask():
""" """
interference = time.time() interference = time.time()
if 'upload' not in request.files: try:
return jsonify({'error': 'No image found'}), 400 if 'upload' not in request.files:
return jsonify({'error': 'No image found'}), 400
grid_size = int(request.form.get('grid_size', 3)) grid_size = int(request.form.get('grid_size', 3))
wanted_cells = request.form.get('wanted_cells') wanted_cells = request.form.get('wanted_cells')
if wanted_cells: if wanted_cells:
wanted_cells = [int(cell) for cell in wanted_cells.split(',')] wanted_cells = [int(cell) for cell in wanted_cells.split(',')]
else: else:
wanted_cells = list(range(1, grid_size * grid_size + 1)) wanted_cells = list(range(1, grid_size * grid_size + 1))
image = request.files['upload'] image = request.files['upload']
if image.filename == '': if image.filename == '':
return jsonify({'error': 'No selected file'}), 400 return jsonify({'error': 'No selected file'}), 400
image = Image.open(image) image = Image.open(image)
result = process_image(image) result = process_image(image)
result = convert_to_cpai_compatible(result) result = convert_to_cpai_compatible(result)
if not result['predictions']: if not result['predictions']:
logger.debug("No plate found in the image, attempting to split the image") logger.debug("No plate found in the image, attempting to split the image")
predictions_found = find_best_plate_with_split(image, grid_size, wanted_cells) predictions_found = find_best_plate_with_grid_split(image, grid_size, wanted_cells)
if predictions_found: if predictions_found:
result['predictions'].append(max(predictions_found, key=lambda x: x['confidence'])) result['predictions'].append(max(predictions_found, key=lambda x: x['confidence']))
result['processMs'] = round((time.time() - interference) * 1000, 2) # Add the isolated plate image to the result
result['inferenceMs'] = result['processMs'] if result['predictions']:
return jsonify(result) 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)}"
result['processMs'] = round((time.time() - interference) * 1000, 2)
result['inferenceMs'] = result['processMs']
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. 4)
- 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:
wanted_cells = list(range(1, grid_size * grid_size + 1))
image = request.files['upload']
if image.filename == '':
return jsonify({'error': 'No selected file'}), 400
image = Image.open(image)
image = draw_grid_and_cell_numbers_on_image(image, grid_size, wanted_cells)
image_base64 = image_to_base64(image, compress=True)
result = {
"image": f"data:image/png;base64,{image_base64}"
}
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('/') @app.route('/')
def index(): def index():
@ -239,7 +298,41 @@ def convert_to_cpai_compatible(result):
return response return response
def find_best_plate_with_split(image: Image, grid_size: int = 3, wanted_cells: list = None): 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: if wanted_cells is None:
wanted_cells = list(range(1, grid_size * grid_size + 1)) wanted_cells = list(range(1, grid_size * grid_size + 1))
@ -284,6 +377,36 @@ def find_best_plate_with_split(image: Image, grid_size: int = 3, wanted_cells: l
return predictions_found return predictions_found
def isolate_plate_in_image(image: Image, plate: dict) -> Image:
x_min = plate['x_min']
x_max = plate['x_max']
y_min = plate['y_min']
y_max = plate['y_max']
offset = 10
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)))
image = image.resize((int(image.size[0] * 3), int(image.size[1] * 3)), resample=Image.Resampling.LANCZOS)
return 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((int(img.size[0] / 2), int(img.size[1] / 2)))
img.save(buffered, format="WEBP", quality=35, lossless=False)
else:
img.save(buffered, format="WEBP")
print(buffered.__sizeof__())
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_thread = threading.Thread(target=start_backend_loop, daemon=True)
engine_thread.start() engine_thread.start()

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Easy Local ALPR - API</title> <title>Easy Local ALPR - API</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<!-- Include Google Sans font --> <!-- Include Google Sans font -->
@ -14,144 +14,266 @@
background-size: 20px 20px; background-size: 20px 20px;
font-family: 'Google Sans', sans-serif; font-family: 'Google Sans', sans-serif;
} }
.loading-circle {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #000;
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
input[type="text"], input[type="number"] {
background-color: white;
color: black;
}
input[type="text"].dark, input[type="number"].dark {
background-color: #3b3b3b;
color: white;
}
</style> </style>
</head> </head>
<body class="bg-neutral-100 dark:bg-neutral-900 dark:text-white flex items-center justify-center min-h-screen p-4"> <body class="bg-neutral-100 dark:bg-neutral-900 dark:text-white flex items-center justify-center min-h-screen p-4">
<!-- Logo --> <!-- Logo -->
<div class="absolute top-4 left-4 z-50"> <div class="absolute top-4 left-4 z-50">
<img id="logo" src="{{ url_for('static', filename='logo_black.webp') }}" alt="Logo" class="h-12 dark:hidden"> <img alt="Logo" class="h-12 dark:hidden" id="logo" src="{{ url_for('static', filename='logo_black.webp') }}">
<img id="logoDark" src="{{ url_for('static', filename='logo_white.webp') }}" alt="Logo" class="h-12 hidden dark:block"> <img alt="Logo" class="h-12 hidden dark:block" id="logoDark"
</div> 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-md mt-16"> <div class="bg-white dark:bg-neutral-800 p-6 rounded-lg shadow-lg w-full max-w-md mt-16">
<h1 class="text-2xl font-bold mb-4 text-center dark:text-gray-200">Upload Image for ALPR</h1> <h1 class="text-2xl font-bold mb-4 text-center dark:text-gray-200">Select Service</h1>
<form id="uploadForm" enctype="multipart/form-data" class="space-y-4"> <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>
<!-- Plate Recognition (ALPR) Fields -->
<div class="service-fields hidden" id="alprFields">
<div> <div>
<label for="upload" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Choose an image:</label> <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"> <div class="mt-1 flex items-center">
<input type="file" id="upload" name="upload" accept="image/*" class="hidden" onchange="updateFileName()"> <input type="file" id="upload_alpr" name="upload" accept="image/*" class="hidden"
<label for="upload" 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"> 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 Select file
</label> </label>
<span id="fileName" class="ml-2 text-sm text-gray-600 dark:text-gray-300"></span> <span id="fileName_alpr" class="ml-2 text-sm text-gray-600 dark:text-gray-300"></span>
</div> </div>
</div> </div>
<div> <div>
<label for="grid_size" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Grid Size:</label> <label for="grid_size_alpr" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Grid
<input type="number" id="grid_size" 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"> 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 dark:bg-neutral-800 dark:border-neutral-700">
</div> </div>
<div> <div>
<label for="wanted_cells" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Wanted Cells:</label> <label for="wanted_cells_alpr" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Wanted
<input type="text" id="wanted_cells" name="wanted_cells" 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"> Cells:</label>
<input type="text" id="wanted_cells_alpr" name="wanted_cells" value=""
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 dark:bg-neutral-800 dark:border-neutral-700">
</div> </div>
<div id="imagePreview" class="mt-4 hidden"> <!-- Hidden field to show plate image from the response in webui -->
<img id="previewImage" src="#" alt="Preview" class="max-w-full h-auto rounded-lg"> <input id="plate_image_alpr" name="plate_image_alpr" type="hidden" value="true">
</div>
<button id="submitButton" type="submit" 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 relative flex justify-center items-center">
<span>Upload</span>
<div id="loadingCircle" class="loading-circle absolute hidden"></div>
</button>
</form>
<div class="mt-6">
<h2 class="text-xl font-semibold mb-2 dark:text-gray-200">Response</h2>
<pre id="responseBox" 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"></pre>
</div> </div>
<! -- Grid Size Helper Fields -->
<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 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>
<input type="text" id="wanted_cells_alpr_grid_debug" name="wanted_cells" value=""
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 dark:bg-neutral-800 dark:border-neutral-700">
</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>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script> <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script> <script>
function updateFileName() { function updateFormFields() {
var input = document.getElementById('upload'); var service = document.getElementById('service').value;
var fileName = document.getElementById('fileName');
var imagePreview = document.getElementById('imagePreview');
var previewImage = document.getElementById('previewImage');
fileName.textContent = input.files[0] ? input.files[0].name : ''; localStorage.setItem('selectedService', service); // Save the selected service
if (input.files && input.files[0]) { elementsIdToEmpty = ['responseBox', 'timer', 'fileName_' + service, 'previewImage', 'imagePreview', 'upload_' + service];
var reader = new FileReader(); elementsIdToEmpty.forEach(element => {
element = document.getElementById(element);
console.log(element.tagName);
reader.onload = function (e) { switch (element.tagName) {
previewImage.src = e.target.result; case 'DIV':
imagePreview.classList.remove('hidden'); element.classList.add('hidden');
} break;
case 'INPUT':
reader.readAsDataURL(input.files[0]); element.value = '';
break;
case 'SPAN': // fallthrough
case 'PRE':
element.textContent = '';
break;
case 'IMG':
element.src = '';
break;
} }
});
var allServicesFields = document.getElementById('serviceForm').querySelectorAll('.service-fields');
allServicesFields.forEach(field => {
field.classList.add('hidden');
field.querySelectorAll('input, select').forEach(field => field.disabled = true);
});
var selectedServiceFields = document.getElementById(service + 'Fields');
selectedServiceFields.classList.remove('hidden');
selectedServiceFields.querySelectorAll('input, select').forEach(field => field.disabled = false);
}
function initializeForm() {
var savedService = localStorage.getItem('selectedService');
if (savedService) {
document.getElementById('service').value = savedService;
updateFormFields();
} }
}
const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)"); const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
function toggleLogo() {
const logo = document.getElementById('logo');
const logoDark = document.getElementById('logoDark');
const upload = document.getElementById('upload');
const gridSize = document.getElementById('grid_size');
const wantedCells = document.getElementById('wanted_cells');
if (prefersDarkScheme.matches) { function toggleLogo() {
logo.style.display = 'none'; const logo = document.getElementById('logo');
logoDark.style.display = 'block'; const logoDark = document.getElementById('logoDark');
upload.classList.add('dark'); if (prefersDarkScheme.matches) {
gridSize.classList.add('dark'); logo.style.display = 'none';
wantedCells.classList.add('dark'); logoDark.style.display = 'block';
} else { } else {
logo.style.display = 'block'; logo.style.display = 'block';
logoDark.style.display = 'none'; logoDark.style.display = 'none';
upload.classList.remove('dark'); }
gridSize.classList.remove('dark'); }
wantedCells.classList.remove('dark');
toggleLogo();
prefersDarkScheme.addEventListener('change', toggleLogo);
function updateFileName() {
var service = document.getElementById('service').value;
var input = document.getElementById('upload_' + service);
var fileName = document.getElementById('fileName' + '_' + service);
var imagePreview = document.getElementById('imagePreview');
var previewImage = document.getElementById('previewImage');
var imagePreviewLabel = document.getElementById('imagePreviewLabel');
fileName.textContent = input.files[0] ? input.files[0].name : '';
imagePreviewLabel.textContent = 'Preview:';
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
previewImage.src = e.target.result;
imagePreview.classList.remove('hidden');
} }
reader.readAsDataURL(input.files[0]);
} }
}
toggleLogo();
prefersDarkScheme.addEventListener('change', toggleLogo);
$(document).ready(function () { $(document).ready(function () {
$('#uploadForm').on('submit', function (e) { initializeForm(); // Initialize the form on page load
e.preventDefault();
var formData = new FormData(this);
$('#submitButton').prop('disabled', true);
$('#loadingCircle').removeClass('hidden');
$.ajax({ $('#serviceForm').on('submit', function (e) {
url: '/v1/image/alpr', e.preventDefault();
type: 'POST', var service = $('#service').val();
data: formData, var formData = new FormData(this);
processData: false,
contentType: false, var url;
success: function (data) { if (service === 'alpr') {
$('#responseBox').text(JSON.stringify(data, null, 2)); url = '/v1/image/alpr';
}, type = 'POST';
error: function (xhr, status, error) { } else if (service === 'alpr_grid_debug') {
var err = JSON.parse(xhr.responseText); url = '/v1/image/alpr_grid_debug';
$('#responseBox').text(JSON.stringify(err, null, 2)); type = 'POST';
}, }
complete: function () {
$('#submitButton').prop('disabled', false); var $submitButton = $('#submitButton');
$('#loadingCircle').addClass('hidden'); $submitButton.prop('disabled', true);
$submitButton.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>');
var startTime = Date.now();
$.ajax({
url: url,
type: type,
data: formData,
processData: false,
contentType: false,
success: function (data) {
var endTime = Date.now();
var elapsedTime = endTime - startTime;
$('#responseBox').text(JSON.stringify(data, null, 2));
$('#timer').text(`(${elapsedTime} ms)`);
$submitButton.prop('disabled', false);
$submitButton.text('Submit');
if (data.image) {
$('#previewImage').attr('src', data.image);
$('#imagePreview').removeClass('hidden');
if (service === 'alpr') {
$('#imagePreviewLabel')[0].textContent = 'Identified plate image:';
}
} else {
updateFileName();
} }
}); },
error: function (xhr, status, error) {
var endTime = Date.now();
var elapsedTime = endTime - startTime;
var err = JSON.parse(xhr.responseText);
$('#responseBox').text(JSON.stringify(err, null, 2));
$('#timer').text(`(${elapsedTime} ms)`);
$submitButton.prop('disabled', false);
$submitButton.text('Submit');
}
}); });
}); });
</script> });
</script>
</body> </body>
</html> </html>