Files
cai-foligno-tools/ai-kyc/kyc.html

1446 lines
57 KiB
HTML

<!doctype html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Scansiona Documenti</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(to bottom, #eff6ff, #dbeafe);
min-height: 100vh;
padding: 1rem;
}
.container {
max-width: 500px;
margin: 0 auto;
}
.card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
margin-bottom: 1rem;
}
.header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.5rem;
}
.header svg {
width: 32px;
height: 32px;
color: #2563eb;
}
h1 {
font-size: 1.5rem;
color: #1f2937;
}
.subtitle {
font-size: 0.875rem;
color: #6b7280;
}
label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
margin-bottom: 0.5rem;
}
.doc-type-btn {
width: 100%;
padding: 1rem;
text-align: left;
border: 2px solid #e5e7eb;
border-radius: 8px;
background: white;
cursor: pointer;
margin-bottom: 0.5rem;
transition: all 0.2s;
}
.doc-type-btn:hover {
border-color: #2563eb;
background: #eff6ff;
}
.doc-type-btn strong {
display: block;
color: #1f2937;
margin-bottom: 0.25rem;
}
.doc-type-btn small {
color: #6b7280;
font-size: 0.75rem;
}
.capture-area {
border: 2px dashed #d1d5db;
border-radius: 8px;
padding: 2rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 1.5rem;
}
.capture-area:hover:not(.disabled) {
border-color: #2563eb;
background: #eff6ff;
}
.capture-area.disabled {
background: #f9fafb;
cursor: not-allowed;
opacity: 0.6;
}
.capture-area svg {
width: 48px;
height: 48px;
color: #9ca3af;
margin-bottom: 0.5rem;
}
.capture-area.disabled svg {
color: #d1d5db;
}
.capture-area p {
font-size: 0.875rem;
color: #6b7280;
}
.preview-container {
position: relative;
margin-bottom: 1.5rem;
}
.preview-container img {
width: 100%;
border-radius: 8px;
border: 2px solid #10b981;
}
.preview-container button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: #ef4444;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.875rem;
cursor: pointer;
}
.preview-container button:hover {
background: #dc2626;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.section-header svg {
width: 20px;
height: 20px;
color: #10b981;
}
.btn {
width: 100%;
padding: 0.875rem;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 0.5rem;
}
.btn-primary {
background: #2563eb;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #1d4ed8;
}
.btn-primary:disabled {
background: #d1d5db;
cursor: not-allowed;
}
.btn-secondary {
background: #e5e7eb;
color: #374151;
}
.btn-secondary:hover {
background: #d1d5db;
}
.btn-text {
background: none;
border: none;
color: #2563eb;
font-size: 0.875rem;
cursor: pointer;
padding: 0.5rem;
}
.btn-text:hover {
color: #1d4ed8;
}
.message {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.875rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.875rem;
}
.message svg {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.message.success {
background: #d1fae5;
color: #065f46;
}
.message.error {
background: #fee2e2;
color: #991b1b;
}
.message.info {
background: #dbeafe;
color: #1e40af;
}
.hidden {
display: none;
}
input[type="file"] {
display: none;
}
.processing-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.processing-overlay.hidden {
display: none !important;
}
.processing-content {
background: white;
padding: 2rem;
border-radius: 12px;
text-align: center;
max-width: 90%;
}
.spinner {
border: 4px solid #e5e7eb;
border-top: 4px solid #2563eb;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.detection-preview {
margin-top: 1rem;
max-width: 400px;
border-radius: 8px;
overflow: hidden;
}
.detection-preview img {
width: 100%;
height: auto;
}
.detection-status {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: 1rem;
color: #10b981;
font-weight: 500;
}
.detection-status svg {
width: 24px;
height: 24px;
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="card">
<div class="header">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<div>
<h1>Scansiona Documenti</h1>
<p class="subtitle">
Scansiona i tuoi documenti in modo sicuro
</p>
</div>
</div>
</div>
<!-- Document Type Selection -->
<div id="docTypeSection" class="card">
<h2
style="
font-size: 1.125rem;
margin-bottom: 1rem;
color: #1f2937;
"
>
Seleziona il tipo di documento
</h2>
<button
class="doc-type-btn"
onclick="selectDocType('carta_identita', true)"
>
<strong>Carta d'Identità</strong>
<small>Fronte e retro richiesti</small>
</button>
<button
class="doc-type-btn"
onclick="selectDocType('passaporto', false)"
>
<strong>Passaporto</strong>
<small>Solo fronte</small>
</button>
<button
class="doc-type-btn"
onclick="selectDocType('patente', true)"
>
<strong>Patente di Guida</strong>
<small>Fronte e retro richiesti</small>
</button>
<button
class="doc-type-btn"
onclick="selectDocType('permesso_soggiorno', true)"
>
<strong>Permesso di Soggiorno</strong>
<small>Fronte e retro richiesti</small>
</button>
<button
class="doc-type-btn"
onclick="selectDocType('tessera_sanitaria', false)"
>
<strong>Tessera Sanitaria</strong>
<small>Solo fronte</small>
</button>
</div>
<!-- Scanner Section -->
<div id="scannerSection" class="card hidden">
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
"
>
<h2
id="selectedDocTitle"
style="font-size: 1.125rem; color: #1f2937"
></h2>
<button class="btn-text" onclick="changeDocument()">
Cambia documento
</button>
</div>
<!-- Front Image -->
<div>
<div class="section-header">
<label>Fronte documento</label>
<svg
id="frontCheck"
class="hidden"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div
id="frontCaptureArea"
class="capture-area"
onclick="document.getElementById('frontInput').click()"
>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<p>Tocca per scansionare il fronte</p>
</div>
<div id="frontPreview" class="preview-container hidden">
<img id="frontImage" src="" alt="Fronte documento" />
<button onclick="resetFront()">Rifai</button>
</div>
<input
type="file"
id="frontInput"
accept="image/*"
capture="environment"
onchange="handleImageCapture(this, 'front')"
/>
</div>
<!-- Back Image -->
<div id="backSection" class="hidden">
<div class="section-header">
<label>Retro documento</label>
<svg
id="backCheck"
class="hidden"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div id="backCaptureArea" class="capture-area disabled">
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<p id="backCaptureText">Scansiona prima il fronte</p>
</div>
<div id="backPreview" class="preview-container hidden">
<img id="backImage" src="" alt="Retro documento" />
<button onclick="resetBack()">Rifai</button>
</div>
<input
type="file"
id="backInput"
accept="image/*"
capture="environment"
onchange="handleImageCapture(this, 'back')"
/>
</div>
<!-- Message -->
<div id="messageBox" class="hidden"></div>
<!-- Action Buttons -->
<button
id="submitBtn"
class="btn btn-primary"
onclick="submitDocument()"
disabled
>
Invia Documento
</button>
<button class="btn btn-secondary" onclick="resetScanner()">
Ricomincia
</button>
</div>
</div>
<!-- Processing Overlay -->
<div id="processingOverlay" class="processing-overlay hidden">
<div class="processing-content">
<div class="spinner"></div>
<p
id="processingTitle"
style="color: #1f2937; font-weight: 500"
>
Elaborazione in corso...
</p>
<p
id="processingSubtitle"
style="
color: #6b7280;
font-size: 0.875rem;
margin-top: 0.5rem;
"
>
Rilevamento bordi documento
</p>
<div
id="detectionPreview"
class="detection-preview hidden"
></div>
<div id="detectionStatus" class="detection-status hidden"></div>
</div>
</div>
<script src="https://docs.opencv.org/4.5.2/opencv.js"></script>
<script>
const WEBHOOK_URL =
"https://n8n-prod.commandware.com/webhook/cai_kyc_upload";
let cvReady = false;
// Attendi che OpenCV sia pronto
function waitForOpenCV() {
return new Promise((resolve) => {
if (typeof cv !== "undefined" && cv.Mat) {
cvReady = true;
console.log("✅ OpenCV caricato");
resolve();
} else {
setTimeout(() => waitForOpenCV().then(resolve), 100);
}
});
}
// Inizializza OpenCV all'avvio
waitForOpenCV();
let state = {
documentType: "",
documentLabel: "",
needsBack: false,
frontImage: null,
backImage: null,
};
const docTypes = {
carta_identita: "Carta d'Identità",
passaporto: "Passaporto",
patente: "Patente di Guida",
permesso_soggiorno: "Permesso di Soggiorno",
tessera_sanitaria: "Tessera Sanitaria",
};
function selectDocType(type, needsBack) {
state.documentType = type;
state.documentLabel = docTypes[type];
state.needsBack = needsBack;
document
.getElementById("docTypeSection")
.classList.add("hidden");
document
.getElementById("scannerSection")
.classList.remove("hidden");
document.getElementById("selectedDocTitle").textContent =
state.documentLabel;
if (needsBack) {
document
.getElementById("backSection")
.classList.remove("hidden");
} else {
document
.getElementById("backSection")
.classList.add("hidden");
}
}
function changeDocument() {
resetScanner();
state.documentType = "";
state.documentLabel = "";
state.needsBack = false;
document
.getElementById("scannerSection")
.classList.add("hidden");
document
.getElementById("docTypeSection")
.classList.remove("hidden");
}
function showProcessing(
title = "Elaborazione in corso...",
subtitle = "Rilevamento bordi documento",
) {
document.getElementById("processingTitle").textContent = title;
document.getElementById("processingSubtitle").textContent =
subtitle;
document
.getElementById("detectionPreview")
.classList.add("hidden");
document
.getElementById("detectionStatus")
.classList.add("hidden");
document
.getElementById("processingOverlay")
.classList.remove("hidden");
}
function hideProcessing() {
document
.getElementById("processingOverlay")
.classList.add("hidden");
}
function showDetectionPreview(imageData, detected) {
const preview = document.getElementById("detectionPreview");
const status = document.getElementById("detectionStatus");
preview.innerHTML = `<img src="${imageData}" alt="Anteprima rilevamento">`;
preview.classList.remove("hidden");
if (detected) {
status.innerHTML = `
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>Documento rilevato!</span>
`;
status.style.color = "#10b981";
} else {
status.innerHTML = `
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>Uso immagine originale</span>
`;
status.style.color = "#f59e0b";
}
status.classList.remove("hidden");
}
// Funzione avanzata per rilevare e ritagliare il documento
function detectAndCropDocument(imageSrc) {
return new Promise((resolve) => {
if (!cvReady) {
console.log(
"OpenCV non pronto, uso immagine originale",
);
resolve(imageSrc);
return;
}
try {
const img = new Image();
img.onload = function () {
try {
// Crea canvas e carica l'immagine
const canvas = document.createElement("canvas");
const maxDim = 1600; // Aumentato per maggiore qualità
let width = img.width;
let height = img.height;
if (width > maxDim || height > maxDim) {
if (width > height) {
height = (height / width) * maxDim;
width = maxDim;
} else {
width = (width / height) * maxDim;
height = maxDim;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
// Converti in Mat
let src = cv.imread(canvas);
let gray = new cv.Mat();
let blurred = new cv.Mat();
let edges = new cv.Mat();
let dilated = new cv.Mat();
let contours = new cv.MatVector();
let hierarchy = new cv.Mat();
// MIGLIORAMENTO 1: Pipeline di preprocessing più robusta
cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY, 0);
// Equalizzazione istogramma per migliorare il contrasto
cv.equalizeHist(gray, gray);
// Filtro bilaterale per ridurre il rumore mantenendo i bordi
cv.bilateralFilter(
gray,
blurred,
9,
75,
75,
cv.BORDER_DEFAULT,
);
// MIGLIORAMENTO 2: Edge detection adattivo con più tentativi
let documentFound = false;
let bestContour = null;
let bestApprox = null;
let bestArea = 0;
// Prova con diversi threshold di Canny
const cannyParams = [
[30, 100], // Soglia bassa - per documenti con poco contrasto
[50, 150], // Soglia media - standard
[75, 200], // Soglia alta - per sfondi molto contrastati
];
for (let [low, high] of cannyParams) {
let tempEdges = new cv.Mat();
let tempDilated = new cv.Mat();
let tempContours = new cv.MatVector();
let tempHierarchy = new cv.Mat();
cv.Canny(
blurred,
tempEdges,
low,
high,
3,
false,
);
// Dilata per chiudere gap nei bordi
let kernel = cv.getStructuringElement(
cv.MORPH_RECT,
new cv.Size(5, 5),
);
cv.dilate(tempEdges, tempDilated, kernel);
kernel.delete();
cv.findContours(
tempDilated,
tempContours,
tempHierarchy,
cv.RETR_EXTERNAL,
cv.CHAIN_APPROX_SIMPLE,
);
console.log(
`Canny [${low},${high}]: trovati ${tempContours.size()} contorni`,
);
// MIGLIORAMENTO 3: Criteri più intelligenti per la selezione del contorno
let minArea = width * height * 0.15; // Ridotto al 15%
let maxArea = width * height * 0.95; // Max 95%
for (
let i = 0;
i < tempContours.size();
i++
) {
let cnt = tempContours.get(i);
let area = cv.contourArea(cnt, false);
if (area < minArea || area > maxArea)
continue;
let peri = cv.arcLength(cnt, true);
let approx = new cv.Mat();
// Prova diverse tolleranze di approssimazione
for (let epsilon of [
0.02, 0.03, 0.04, 0.05,
]) {
cv.approxPolyDP(
cnt,
approx,
epsilon * peri,
true,
);
// Cerchiamo un quadrilatero
if (approx.rows === 4) {
// Verifica che sia un quadrilatero "ragionevole"
if (
isValidQuadrilateral(
approx,
width,
height,
)
) {
// Calcola i punti del contorno
let points = [];
for (let j = 0; j < 4; j++) {
points.push({
x: approx.data32S[j * 2],
y: approx.data32S[j * 2 + 1],
});
}
// Calcola centralità
let centralityScore = calculateCentralityScore(
points,
width,
height,
);
// Score combinato: 70% area, 30% centralità
let normalizedArea = area / (width * height);
let combinedScore =
(normalizedArea * 0.7) +
(centralityScore * 0.3);
// Usa score combinato invece di solo area
let currentBestScore = bestArea / (width * height) * 0.7;
if (bestApprox) {
let bestPoints = [];
for (let j = 0; j < 4; j++) {
bestPoints.push({
x: bestApprox.data32S[j * 2],
y: bestApprox.data32S[j * 2 + 1],
});
}
let bestCentrality = calculateCentralityScore(
bestPoints,
width,
height,
);
currentBestScore += bestCentrality * 0.3;
}
if (combinedScore > currentBestScore) {
console.log(
`📍 Nuovo miglior contorno - Area: ${area.toFixed(0)}, ` +
`Centralità: ${(centralityScore * 100).toFixed(1)}%, ` +
`Score: ${(combinedScore * 100).toFixed(1)}%`
);
bestArea = area;
if (bestApprox)
bestApprox.delete();
bestApprox =
approx.clone();
bestContour =
cnt.clone();
documentFound = true;
}
}
break;
}
}
approx.delete();
}
tempEdges.delete();
tempDilated.delete();
tempContours.delete();
tempHierarchy.delete();
// Se abbiamo trovato un buon documento, fermiamoci
if (
documentFound &&
bestArea > width * height * 0.3
) {
console.log(
`✅ Documento trovato con parametri Canny [${low},${high}]`,
);
break;
}
}
let result = src.clone();
if (documentFound && bestApprox) {
console.log(
`✅ Documento rilevato! Area: ${bestArea.toFixed(0)} px²`,
);
// Estrai i 4 punti
let points = [];
for (let i = 0; i < 4; i++) {
points.push({
x: bestApprox.data32S[i * 2],
y: bestApprox.data32S[i * 2 + 1],
});
}
// MIGLIORAMENTO 4: Ordinamento più robusto dei punti
let ordered = orderPoints(points);
// Calcola dimensioni del documento con proporzioni corrette
let widthTop = distance(
ordered[0],
ordered[1],
);
let widthBottom = distance(
ordered[3],
ordered[2],
);
let maxWidth = Math.max(
widthTop,
widthBottom,
);
let heightLeft = distance(
ordered[0],
ordered[3],
);
let heightRight = distance(
ordered[1],
ordered[2],
);
let maxHeight = Math.max(
heightLeft,
heightRight,
);
// Punti di origine e destinazione
let srcPoints = cv.matFromArray(
4,
1,
cv.CV_32FC2,
[
ordered[0].x,
ordered[0].y,
ordered[1].x,
ordered[1].y,
ordered[2].x,
ordered[2].y,
ordered[3].x,
ordered[3].y,
],
);
let dstPoints = cv.matFromArray(
4,
1,
cv.CV_32FC2,
[
0,
0,
maxWidth,
0,
maxWidth,
maxHeight,
0,
maxHeight,
],
);
// Applica trasformazione prospettica
let transform = cv.getPerspectiveTransform(
srcPoints,
dstPoints,
);
let warped = new cv.Mat();
let dsize = new cv.Size(
maxWidth,
maxHeight,
);
cv.warpPerspective(
src,
warped,
transform,
dsize,
cv.INTER_LINEAR,
cv.BORDER_CONSTANT,
new cv.Scalar(255, 255, 255, 255),
);
// MIGLIORAMENTO 5: Post-processing per migliorare la qualità
let enhanced = new cv.Mat();
cv.cvtColor(
warped,
enhanced,
cv.COLOR_RGBA2RGB,
0,
);
// Applica sharpening
let sharpened = sharpenImage(enhanced);
result.delete();
result = sharpened;
enhanced.delete();
warped.delete();
srcPoints.delete();
dstPoints.delete();
transform.delete();
if (bestApprox) bestApprox.delete();
if (bestContour) bestContour.delete();
} else {
console.log(
"⚠️ Nessun documento rilevato, uso immagine originale",
);
}
// Converti il risultato in base64 con qualità alta
let outputCanvas =
document.createElement("canvas");
cv.imshow(outputCanvas, result);
let croppedImage = outputCanvas.toDataURL(
"image/jpeg",
0.95,
);
// Mostra anteprima del rilevamento
showDetectionPreview(
croppedImage,
documentFound,
);
// Aspetta 1.5 secondi per mostrare l'anteprima
setTimeout(() => {
// Cleanup
src.delete();
gray.delete();
blurred.delete();
result.delete();
resolve(croppedImage);
}, 1500);
} catch (error) {
console.error("Errore nel processing:", error);
resolve(imageSrc);
}
};
img.onerror = function () {
console.error("Errore nel caricamento immagine");
resolve(imageSrc);
};
img.src = imageSrc;
} catch (error) {
console.error("Errore generale:", error);
resolve(imageSrc);
}
});
}
// Funzioni di supporto per il rilevamento documenti
function distance(p1, p2) {
return Math.sqrt(
Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2),
);
}
function orderPoints(points) {
// Ordina i punti in modo consistente: TL, TR, BR, BL
let center = {
x: points.reduce((sum, p) => sum + p.x, 0) / 4,
y: points.reduce((sum, p) => sum + p.y, 0) / 4,
};
let ordered = points
.map((p) => ({
...p,
angle: Math.atan2(p.y - center.y, p.x - center.x),
}))
.sort((a, b) => a.angle - b.angle);
// Trova il punto in alto a sinistra (quello con y minore tra i primi due)
if (ordered[0].y > ordered[3].y) {
ordered = [ordered[3], ordered[0], ordered[1], ordered[2]];
}
return ordered;
}
function isValidQuadrilateral(approx, imgWidth, imgHeight) {
// Verifica che il quadrilatero sia valido
let points = [];
for (let i = 0; i < 4; i++) {
points.push({
x: approx.data32S[i * 2],
y: approx.data32S[i * 2 + 1],
});
}
// Calcola il rapporto d'aspetto
let ordered = orderPoints(points);
let width = Math.max(
distance(ordered[0], ordered[1]),
distance(ordered[3], ordered[2]),
);
let height = Math.max(
distance(ordered[0], ordered[3]),
distance(ordered[1], ordered[2]),
);
let aspectRatio =
Math.max(width, height) / Math.min(width, height);
// I documenti di identità hanno aspect ratio tra 1.3 e 1.8 circa
// Permettiamo un range più ampio per flessibilità
if (aspectRatio > 3.0 || aspectRatio < 1.2) {
return false;
}
// MIGLIORAMENTO: Rifiuta i contorni con punti troppo vicini ai bordi
// Il documento dovrebbe essere fotografato al centro, non ai margini
let margin = Math.min(imgWidth, imgHeight) * 0.05; // 5% dei bordi
let pointsOnBorder = 0;
for (let p of points) {
if (
p.x < margin ||
p.y < margin ||
p.x > imgWidth - margin ||
p.y > imgHeight - margin
) {
pointsOnBorder++;
}
}
// Se più di 1 punto è sul bordo, probabilmente è un falso positivo
if (pointsOnBorder > 1) {
console.log(`❌ Quadrilatero scartato: ${pointsOnBorder} punti troppo vicini ai bordi`);
return false;
}
return true;
}
// Calcola quanto un contorno è "centrale" nell'immagine
function calculateCentralityScore(points, imgWidth, imgHeight) {
// Calcola il centro del contorno
let centerX = points.reduce((sum, p) => sum + p.x, 0) / points.length;
let centerY = points.reduce((sum, p) => sum + p.y, 0) / points.length;
// Centro dell'immagine
let imgCenterX = imgWidth / 2;
let imgCenterY = imgHeight / 2;
// Distanza dal centro (normalizzata)
let maxDist = Math.sqrt(Math.pow(imgWidth / 2, 2) + Math.pow(imgHeight / 2, 2));
let dist = Math.sqrt(
Math.pow(centerX - imgCenterX, 2) +
Math.pow(centerY - imgCenterY, 2)
);
// Score: 1.0 = perfettamente centrato, 0.0 = molto decentrato
let centralityScore = 1.0 - (dist / maxDist);
return centralityScore;
}
function sharpenImage(src) {
// Applica filtro di sharpening per migliorare la nitidezza
let kernel = cv.matFromArray(
3,
3,
cv.CV_32FC1,
[0, -1, 0, -1, 5, -1, 0, -1, 0],
);
let dst = new cv.Mat();
cv.filter2D(src, dst, cv.CV_8U, kernel);
kernel.delete();
return dst;
}
async function handleImageCapture(input, side) {
const file = input.files[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
showMessage("error", "Seleziona un'immagine valida");
return;
}
showProcessing();
const reader = new FileReader();
reader.onload = async function (e) {
const imageData = e.target.result;
// Attendi OpenCV e processa l'immagine
await waitForOpenCV();
const processedImage =
await detectAndCropDocument(imageData);
hideProcessing();
if (side === "front") {
state.frontImage = processedImage;
document.getElementById("frontImage").src =
processedImage;
document
.getElementById("frontCaptureArea")
.classList.add("hidden");
document
.getElementById("frontPreview")
.classList.remove("hidden");
document
.getElementById("frontCheck")
.classList.remove("hidden");
showMessage(
"success",
"Fronte acquisito con successo!",
);
if (state.needsBack) {
document
.getElementById("backCaptureArea")
.classList.remove("disabled");
document.getElementById("backCaptureArea").onclick =
function () {
document
.getElementById("backInput")
.click();
};
document.getElementById(
"backCaptureText",
).textContent = "Tocca per scansionare il retro";
}
} else {
state.backImage = processedImage;
document.getElementById("backImage").src =
processedImage;
document
.getElementById("backCaptureArea")
.classList.add("hidden");
document
.getElementById("backPreview")
.classList.remove("hidden");
document
.getElementById("backCheck")
.classList.remove("hidden");
showMessage("success", "Retro acquisito con successo!");
}
updateSubmitButton();
};
reader.readAsDataURL(file);
}
function resetFront() {
state.frontImage = null;
document.getElementById("frontInput").value = "";
document
.getElementById("frontCaptureArea")
.classList.remove("hidden");
document.getElementById("frontPreview").classList.add("hidden");
document.getElementById("frontCheck").classList.add("hidden");
if (state.needsBack) {
resetBack();
document
.getElementById("backCaptureArea")
.classList.add("disabled");
document.getElementById("backCaptureArea").onclick = null;
document.getElementById("backCaptureText").textContent =
"Scansiona prima il fronte";
}
updateSubmitButton();
}
function resetBack() {
state.backImage = null;
document.getElementById("backInput").value = "";
document
.getElementById("backCaptureArea")
.classList.remove("hidden");
document.getElementById("backPreview").classList.add("hidden");
document.getElementById("backCheck").classList.add("hidden");
updateSubmitButton();
}
function resetScanner() {
resetFront();
hideMessage();
}
function updateSubmitButton() {
const hasFront = state.frontImage !== null;
const hasBack = !state.needsBack || state.backImage !== null;
const submitBtn = document.getElementById("submitBtn");
submitBtn.disabled = !(hasFront && hasBack);
}
function showMessage(type, text) {
const messageBox = document.getElementById("messageBox");
messageBox.className = `message ${type}`;
const icons = {
success:
'<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>',
error: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>',
info: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>',
};
messageBox.innerHTML =
icons[type] + "<span>" + text + "</span>";
messageBox.classList.remove("hidden");
if (type === "success") {
setTimeout(hideMessage, 3000);
}
}
function hideMessage() {
document.getElementById("messageBox").classList.add("hidden");
}
async function submitDocument() {
if (!state.frontImage) {
showMessage(
"error",
"Scansiona almeno il fronte del documento",
);
return;
}
if (state.needsBack && !state.backImage) {
showMessage(
"error",
"Questo documento richiede anche il retro",
);
return;
}
const submitBtn = document.getElementById("submitBtn");
submitBtn.disabled = true;
submitBtn.textContent = "Invio in corso...";
try {
const payload = {
documentType: state.documentType,
documentLabel: state.documentLabel,
timestamp: new Date().toISOString(),
images: {
front: state.frontImage,
back: state.backImage,
},
};
const response = await fetch(WEBHOOK_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (response.ok) {
showMessage(
"success",
"Documento inviato con successo!",
);
setTimeout(() => {
changeDocument();
}, 2000);
} else {
throw new Error("Errore HTTP: " + response.status);
}
} catch (error) {
console.error("Errore invio:", error);
showMessage(
"error",
"Errore nell'invio del documento. Riprova.",
);
submitBtn.disabled = false;
submitBtn.textContent = "Invia Documento";
}
}
</script>
</body>
</html>