const sharp = require('sharp') const path = require('path') const fs = require('fs') /** * Améliorateur OCR spécialisé pour les Cartes Nationales d'Identité françaises * Utilise des techniques avancées de preprocessing et de segmentation */ // Fonction pour détecter si une image est probablement une CNI async function isCNIDocument(inputPath) { try { const image = sharp(inputPath) const metadata = await image.metadata() // Vérifications pour une CNI française const isPortrait = metadata.height > metadata.width const hasGoodResolution = metadata.width > 800 && metadata.height > 500 const aspectRatio = metadata.width / metadata.height const isCNIRatio = aspectRatio > 0.6 && aspectRatio < 0.7 // Ratio typique d'une CNI console.log( `[CNI_DETECT] ${path.basename(inputPath)} - Portrait: ${isPortrait}, Résolution: ${metadata.width}x${metadata.height}, Ratio: ${aspectRatio.toFixed(2)}`, ) return isPortrait && hasGoodResolution && isCNIRatio } catch (error) { console.error(`[CNI_DETECT] Erreur détection CNI:`, error.message) return false } } // Fonction pour extraire la zone MRZ (Machine Readable Zone) d'une CNI async function extractMRZ(inputPath) { try { const image = sharp(inputPath) const metadata = await image.metadata() // La MRZ est généralement en bas de la CNI const mrzHeight = Math.floor(metadata.height * 0.15) // 15% de la hauteur const mrzTop = metadata.height - mrzHeight console.log(`[MRZ] Extraction de la zone MRZ: ${mrzTop},${mrzHeight}`) const mrzImage = await image .extract({ left: 0, top: mrzTop, width: metadata.width, height: mrzHeight, }) .grayscale() .normalize() .sharpen() .threshold(128) .png() .toBuffer() return mrzImage } catch (error) { console.error(`[MRZ] Erreur extraction MRZ:`, error.message) return null } } // Fonction pour segmenter une CNI en zones spécifiques async function segmentCNIZones(inputPath) { try { const image = sharp(inputPath) const metadata = await image.metadata() const zones = { // Zone du nom (généralement en haut à gauche) nameZone: { left: Math.floor(metadata.width * 0.05), top: Math.floor(metadata.height * 0.25), width: Math.floor(metadata.width * 0.4), height: Math.floor(metadata.height * 0.15), }, // Zone du prénom firstNameZone: { left: Math.floor(metadata.width * 0.05), top: Math.floor(metadata.height * 0.35), width: Math.floor(metadata.width * 0.4), height: Math.floor(metadata.height * 0.15), }, // Zone de la date de naissance birthDateZone: { left: Math.floor(metadata.width * 0.05), top: Math.floor(metadata.height * 0.45), width: Math.floor(metadata.width * 0.3), height: Math.floor(metadata.height * 0.1), }, // Zone du numéro de CNI idNumberZone: { left: Math.floor(metadata.width * 0.05), top: Math.floor(metadata.height * 0.55), width: Math.floor(metadata.width * 0.4), height: Math.floor(metadata.height * 0.1), }, } console.log(`[CNI_SEGMENT] Segmentation en ${Object.keys(zones).length} zones`) return zones } catch (error) { console.error(`[CNI_SEGMENT] Erreur segmentation:`, error.message) return null } } // Fonction pour extraire une zone spécifique d'une CNI async function extractCNIZone(inputPath, zone, zoneName) { try { const image = sharp(inputPath) const zoneImage = await image .extract(zone) .grayscale() .normalize() .sharpen({ sigma: 2, m1: 0.5, m2: 2, x1: 2, y2: 10 }) .modulate({ brightness: 1.2, contrast: 1.5 }) .threshold(140) .png() .toBuffer() console.log(`[CNI_ZONE] Zone ${zoneName} extraite: ${zone.width}x${zone.height}`) return zoneImage } catch (error) { console.error(`[CNI_ZONE] Erreur extraction zone ${zoneName}:`, error.message) return null } } // Fonction pour améliorer le preprocessing spécifiquement pour les CNI async function enhanceCNIPreprocessing(inputPath) { try { console.log(`[CNI_ENHANCE] Amélioration CNI pour: ${path.basename(inputPath)}`) const image = sharp(inputPath) const metadata = await image.metadata() // Configuration spécialisée pour les CNI const enhancedImage = await image .resize({ width: 2000, height: Math.floor(2000 * (metadata.height / metadata.width)), fit: 'fill', kernel: sharp.kernel.lanczos3, }) .grayscale() .normalize() .modulate({ brightness: 1.3, contrast: 1.8, saturation: 0, }) .sharpen({ sigma: 1.5, m1: 0.5, m2: 3, x1: 2, y2: 20, }) .median(3) .threshold(135) .png() .toBuffer() console.log(`[CNI_ENHANCE] Image améliorée: ${enhancedImage.length} bytes`) return enhancedImage } catch (error) { console.error(`[CNI_ENHANCE] Erreur amélioration CNI:`, error.message) return null } } // Fonction pour traiter une CNI avec segmentation par zones async function processCNIWithZones(inputPath) { try { console.log(`[CNI_PROCESS] Traitement CNI par zones: ${path.basename(inputPath)}`) // Vérifier si c'est une CNI const isCNI = await isCNIDocument(inputPath) if (!isCNI) { console.log(`[CNI_PROCESS] Document non reconnu comme CNI`) return null } // Segmenter en zones const zones = await segmentCNIZones(inputPath) if (!zones) { console.log(`[CNI_PROCESS] Échec de la segmentation`) return null } const results = { isCNI: true, zones: {}, mrz: null, } // Extraire chaque zone for (const [zoneName, zone] of Object.entries(zones)) { const zoneImage = await extractCNIZone(inputPath, zone, zoneName) if (zoneImage) { results.zones[zoneName] = zoneImage } } // Extraire la MRZ const mrzImage = await extractMRZ(inputPath) if (mrzImage) { results.mrz = mrzImage } console.log(`[CNI_PROCESS] CNI traitée: ${Object.keys(results.zones).length} zones + MRZ`) return results } catch (error) { console.error(`[CNI_PROCESS] Erreur traitement CNI:`, error.message) return null } } // Fonction pour décoder la MRZ (Machine Readable Zone) function decodeMRZ(mrzText) { try { if (!mrzText || mrzText.length < 88) { return null } // Format MRZ de la CNI française (2 lignes de 36 caractères) const lines = mrzText.split('\n').filter((line) => line.trim().length > 0) if (lines.length < 2) { return null } const line1 = lines[0].trim() const line2 = lines[1].trim() const result = { documentType: line1.substring(0, 2), country: line1.substring(2, 5), surname: line1.substring(5, 36).replace(/