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 logging
import os
import sys
import threading
import time
import traceback
import ultimateAlprSdk
from PIL import Image
from PIL import Image, ImageDraw, ImageFont
from flask import Flask, request, jsonify, render_template
# Setup logging
@ -149,7 +152,6 @@ def create_rest_server_flask():
@app.route('/v1/image/alpr', methods=['POST'])
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.
Parameters:
@ -159,6 +161,7 @@ def create_rest_server_flask():
"""
interference = time.time()
try:
if 'upload' not in request.files:
return jsonify({'error': 'No image found'}), 400
@ -179,14 +182,70 @@ def create_rest_server_flask():
if not result['predictions']:
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:
result['predictions'].append(max(predictions_found, key=lambda x: x['confidence']))
# Add the isolated plate image to the result
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)}"
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('/')
def index():
@ -239,7 +298,41 @@ def convert_to_cpai_compatible(result):
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:
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
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__':
engine_thread = threading.Thread(target=start_backend_loop, daemon=True)
engine_thread.start()

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<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>
<script src="https://cdn.tailwindcss.com"></script>
<!-- Include Google Sans font -->
@ -14,78 +14,190 @@
background-size: 20px 20px;
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>
</head>
<body class="bg-neutral-100 dark:bg-neutral-900 dark:text-white flex items-center justify-center min-h-screen p-4">
<!-- Logo -->
<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 id="logoDark" src="{{ url_for('static', filename='logo_white.webp') }}" alt="Logo" class="h-12 hidden dark:block">
<!-- Logo -->
<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-md 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="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>
<form id="uploadForm" enctype="multipart/form-data" class="space-y-4">
<!-- Plate Recognition (ALPR) Fields -->
<div class="service-fields hidden" id="alprFields">
<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">
<input type="file" id="upload" name="upload" accept="image/*" class="hidden" onchange="updateFileName()">
<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">
<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" 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>
<label for="grid_size" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Grid Size:</label>
<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">
<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 dark:bg-neutral-800 dark:border-neutral-700">
</div>
<div>
<label for="wanted_cells" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Wanted Cells:</label>
<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">
<label for="wanted_cells_alpr" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Wanted
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>
<!-- Hidden field to show plate image from the response in webui -->
<input id="plate_image_alpr" name="plate_image_alpr" type="hidden" value="true">
</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 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 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</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>
<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() {
var service = document.getElementById('service').value;
localStorage.setItem('selectedService', service); // Save the selected service
elementsIdToEmpty = ['responseBox', 'timer', 'fileName_' + service, 'previewImage', 'imagePreview', 'upload_' + service];
elementsIdToEmpty.forEach(element => {
element = document.getElementById(element);
console.log(element.tagName);
switch (element.tagName) {
case 'DIV':
element.classList.add('hidden');
break;
case 'INPUT':
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)");
function toggleLogo() {
const logo = document.getElementById('logo');
const logoDark = document.getElementById('logoDark');
if (prefersDarkScheme.matches) {
logo.style.display = 'none';
logoDark.style.display = 'block';
} else {
logo.style.display = 'block';
logoDark.style.display = 'none';
}
}
toggleLogo();
prefersDarkScheme.addEventListener('change', toggleLogo);
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
function updateFileName() {
var input = document.getElementById('upload');
var fileName = document.getElementById('fileName');
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();
@ -99,59 +211,69 @@
}
}
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) {
logo.style.display = 'none';
logoDark.style.display = 'block';
upload.classList.add('dark');
gridSize.classList.add('dark');
wantedCells.classList.add('dark');
} else {
logo.style.display = 'block';
logoDark.style.display = 'none';
upload.classList.remove('dark');
gridSize.classList.remove('dark');
wantedCells.classList.remove('dark');
}
}
toggleLogo();
prefersDarkScheme.addEventListener('change', toggleLogo);
$(document).ready(function () {
$('#uploadForm').on('submit', function (e) {
initializeForm(); // Initialize the form on page load
$('#serviceForm').on('submit', function (e) {
e.preventDefault();
var service = $('#service').val();
var formData = new FormData(this);
$('#submitButton').prop('disabled', true);
$('#loadingCircle').removeClass('hidden');
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';
}
var $submitButton = $('#submitButton');
$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: '/v1/image/alpr',
type: 'POST',
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));
},
complete: function () {
$('#submitButton').prop('disabled', false);
$('#loadingCircle').addClass('hidden');
$('#timer').text(`(${elapsedTime} ms)`);
$submitButton.prop('disabled', false);
$submitButton.text('Submit');
}
});
});
});
</script>
</script>
</body>
</html>