- enhancedOcr.js: patterns génériques pour noms corrompus au lieu de CANTU/NICOLAS spécifiques - server.js: détection générique des patterns de noms avec regex flexibles - test-cni-direct.cjs: tests génériques pour identités au lieu de noms spécifiques - Amélioration de la robustesse du système OCR pour tous types de noms
294 lines
10 KiB
JavaScript
294 lines
10 KiB
JavaScript
const sharp = require('sharp')
|
|
const path = require('path')
|
|
const fs = require('fs')
|
|
const { execSync } = require('child_process')
|
|
const {
|
|
isCNIDocument,
|
|
enhanceCNIPreprocessing,
|
|
processCNIWithZones,
|
|
decodeMRZ,
|
|
} = require('./cniOcrEnhancer')
|
|
|
|
/**
|
|
* OCR amélioré avec support spécialisé pour les CNI
|
|
* Combine Tesseract avec des techniques de preprocessing avancées
|
|
*/
|
|
|
|
// Fonction pour exécuter Tesseract avec stratégies multiples et choisir le meilleur
|
|
async function runTesseractOCR(imageBuffer, options = {}) {
|
|
const tempInput = path.join(__dirname, `temp_input_${Date.now()}.png`)
|
|
const tempOutputBase = path.join(__dirname, `temp_output_${Date.now()}`)
|
|
fs.writeFileSync(tempInput, imageBuffer)
|
|
|
|
const strategies = []
|
|
const baseLang = options.language || 'fra'
|
|
const basePsm = options.psm || '6'
|
|
const baseOem = options.oem || '3'
|
|
|
|
// Stratégies génériques
|
|
strategies.push({ lang: baseLang, psm: basePsm, oem: baseOem })
|
|
strategies.push({ lang: baseLang, psm: '3', oem: baseOem })
|
|
strategies.push({ lang: baseLang, psm: '13', oem: baseOem })
|
|
|
|
// Si on cible MRZ/OCRB
|
|
if ((options.language || '').includes('eng') || options.mrz) {
|
|
// OCRB peut ne pas être installé; on tente eng+ocrb puis eng seul
|
|
strategies.push({ lang: 'ocrb+eng', psm: '6', oem: baseOem })
|
|
strategies.push({ lang: 'ocrb+eng', psm: '8', oem: baseOem })
|
|
strategies.push({ lang: 'eng', psm: '6', oem: baseOem })
|
|
strategies.push({ lang: 'eng', psm: '8', oem: baseOem })
|
|
}
|
|
|
|
// Stratégies spécialisées pour CNI (noms français)
|
|
if (options.cni || options.frenchNames) {
|
|
strategies.push({ lang: 'fra', psm: '6', oem: baseOem })
|
|
strategies.push({ lang: 'fra', psm: '8', oem: baseOem })
|
|
strategies.push({ lang: 'fra', psm: '13', oem: baseOem })
|
|
// Stratégie hybride pour les noms
|
|
strategies.push({ lang: 'fra+eng', psm: '6', oem: baseOem })
|
|
strategies.push({ lang: 'fra+eng', psm: '8', oem: baseOem })
|
|
}
|
|
|
|
let best = { text: '', score: -1, meta: null }
|
|
for (let i = 0; i < strategies.length; i += 1) {
|
|
const s = strategies[i]
|
|
const tempOutput = `${tempOutputBase}_${i}`
|
|
const cmd = `tesseract "${tempInput}" "${tempOutput}" -l ${s.lang} --psm ${s.psm} --oem ${s.oem}`
|
|
try {
|
|
execSync(cmd, { stdio: 'pipe' })
|
|
const t = fs.readFileSync(`${tempOutput}.txt`, 'utf8')
|
|
const text = t.trim()
|
|
// Heuristique de score: longueur utile et présence de caractères alphanumériques
|
|
const alpha = (text.match(/[A-Za-z0-9]/g) || []).length
|
|
const score = alpha + (text.includes('<<') ? 20 : 0)
|
|
if (score > best.score) best = { text, score, meta: s }
|
|
try { fs.unlinkSync(`${tempOutput}.txt`) } catch {}
|
|
} catch (e) {
|
|
// Essai suivant
|
|
}
|
|
}
|
|
|
|
try { fs.unlinkSync(tempInput) } catch {}
|
|
|
|
return {
|
|
text: best.text,
|
|
confidence: best.score > 0 ? 0.85 : 0.6,
|
|
method: 'tesseract_multi',
|
|
used: best.meta,
|
|
}
|
|
}
|
|
|
|
// Fonction pour extraire le texte d'une image avec améliorations CNI
|
|
async function extractTextFromImageEnhanced(inputPath) {
|
|
try {
|
|
console.log(`[ENHANCED_OCR] Début extraction: ${path.basename(inputPath)}`)
|
|
|
|
// Vérifier si c'est une CNI
|
|
const isCNI = await isCNIDocument(inputPath)
|
|
console.log(`[ENHANCED_OCR] CNI détectée: ${isCNI}`)
|
|
|
|
if (isCNI) {
|
|
return await extractTextFromCNI(inputPath)
|
|
} else {
|
|
return await extractTextFromStandardDocument(inputPath)
|
|
}
|
|
} catch (error) {
|
|
console.error(`[ENHANCED_OCR] Erreur extraction:`, error.message)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// Fonction spécialisée pour l'extraction de texte des CNI
|
|
async function extractTextFromCNI(inputPath) {
|
|
try {
|
|
console.log(`[CNI_OCR] Traitement CNI: ${path.basename(inputPath)}`)
|
|
|
|
// Améliorer le preprocessing pour les CNI
|
|
const enhancedImage = await enhanceCNIPreprocessing(inputPath)
|
|
if (!enhancedImage) {
|
|
throw new Error('Échec du preprocessing CNI')
|
|
}
|
|
|
|
// Traitement par zones
|
|
const cniZones = await processCNIWithZones(inputPath)
|
|
|
|
let combinedText = ''
|
|
let mrzData = null
|
|
|
|
// Extraire le texte de l'image améliorée avec stratégies CNI
|
|
const mainText = await runTesseractOCR(enhancedImage, {
|
|
language: 'fra',
|
|
psm: '6', // Mode uniforme de bloc de texte
|
|
cni: true, // Activer les stratégies spécialisées CNI
|
|
frenchNames: true, // Activer les stratégies pour noms français
|
|
})
|
|
combinedText += mainText.text + '\n'
|
|
|
|
// Si on a des zones, traiter chaque zone séparément
|
|
if (cniZones && cniZones.zones) {
|
|
for (const [zoneName, zoneImage] of Object.entries(cniZones.zones)) {
|
|
try {
|
|
const zoneText = await runTesseractOCR(zoneImage, {
|
|
language: 'fra',
|
|
psm: '8', // Mode mot unique
|
|
cni: true, // Activer les stratégies spécialisées CNI
|
|
frenchNames: true, // Activer les stratégies pour noms français
|
|
})
|
|
combinedText += `[${zoneName.toUpperCase()}] ${zoneText.text}\n`
|
|
console.log(`[CNI_OCR] Zone ${zoneName}: ${zoneText.text}`)
|
|
} catch (zoneError) {
|
|
console.warn(`[CNI_OCR] Erreur zone ${zoneName}:`, zoneError.message)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Traiter la MRZ si disponible
|
|
if (cniZones && cniZones.mrz) {
|
|
try {
|
|
const mrzText = await runTesseractOCR(cniZones.mrz, {
|
|
language: 'ocrb+eng',
|
|
psm: '6',
|
|
mrz: true,
|
|
})
|
|
combinedText += `[MRZ] ${mrzText.text}\n`
|
|
|
|
// Décoder la MRZ
|
|
mrzData = decodeMRZ(mrzText.text)
|
|
if (mrzData) {
|
|
combinedText += `[MRZ_DECODED] Nom: ${mrzData.surname}, Prénom: ${mrzData.givenNames}, Numéro: ${mrzData.documentNumber}\n`
|
|
}
|
|
} catch (mrzError) {
|
|
console.warn(`[CNI_OCR] Erreur MRZ:`, mrzError.message)
|
|
}
|
|
}
|
|
|
|
// Post-traitement du texte pour corriger les erreurs communes
|
|
const processedText = postProcessCNIText(combinedText)
|
|
|
|
console.log(`[CNI_OCR] Texte final: ${processedText.length} caractères`)
|
|
|
|
return {
|
|
text: processedText,
|
|
confidence: 0.85, // Confiance élevée pour les CNI traitées
|
|
method: 'cni_enhanced',
|
|
mrzData: mrzData,
|
|
zones: cniZones ? Object.keys(cniZones.zones || {}) : [],
|
|
}
|
|
} catch (error) {
|
|
console.error(`[CNI_OCR] Erreur traitement CNI:`, error.message)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// Fonction pour l'extraction de texte des documents standards
|
|
async function extractTextFromStandardDocument(inputPath) {
|
|
try {
|
|
console.log(`[STANDARD_OCR] Traitement document standard: ${path.basename(inputPath)}`)
|
|
|
|
// Preprocessing standard
|
|
const image = sharp(inputPath)
|
|
const metadata = await image.metadata()
|
|
|
|
const processedImage = await image
|
|
.resize({
|
|
width: Math.min(metadata.width * 2, 2000),
|
|
height: Math.min(metadata.height * 2, 2000),
|
|
fit: 'inside',
|
|
withoutEnlargement: false,
|
|
})
|
|
.grayscale()
|
|
// Sharp attend des bornes entières 1..100 pour lower/upper (percentiles)
|
|
// Ancien réglage (0.1/0.9) provoquait une erreur. On utilise 10/90.
|
|
.normalize({ lower: 10, upper: 90 })
|
|
// Voir commentaire ci-dessus: clamp en 1..100
|
|
.normalize({ lower: 10, upper: 90 })
|
|
.sharpen()
|
|
.png()
|
|
.toBuffer()
|
|
|
|
// OCR standard
|
|
const result = await runTesseractOCR(processedImage, {
|
|
language: 'fra',
|
|
psm: '6',
|
|
})
|
|
|
|
return {
|
|
text: result.text,
|
|
confidence: result.confidence,
|
|
method: 'standard_enhanced',
|
|
}
|
|
} catch (error) {
|
|
console.error(`[STANDARD_OCR] Erreur traitement standard:`, error.message)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// Fonction pour post-traiter le texte des CNI
|
|
function postProcessCNIText(text) {
|
|
try {
|
|
let processedText = text
|
|
|
|
// Corrections communes pour les CNI
|
|
const corrections = [
|
|
// Corrections de caractères corrompus
|
|
{ from: /RÉPUBLIQUE FRANCATSEN/g, to: 'RÉPUBLIQUE FRANÇAISE' },
|
|
{ from: /CARTE NATIONALE DIDENTITE/g, to: "CARTE NATIONALE D'IDENTITÉ" },
|
|
{ from: /Ne :/g, to: 'N° :' },
|
|
{ from: /Fe - 0/g, to: 'Féminin' },
|
|
{ from: /Mele:/g, to: 'Mâle:' },
|
|
{ from: /IDFRA[A-Z]+CCKKLLLLK[A-Z]*/g, to: 'IDFRA' }, // Nettoyer les caractères parasites après IDFRA
|
|
|
|
// Corrections génériques pour les noms corrompus
|
|
{ from: /([A-Z]{2,})CCKKLLLLK/g, to: '$1' }, // Supprimer les caractères parasites après les noms
|
|
{ from: /([A-Z]{2,})<+([A-Z]{2,})/g, to: '$1<<$2' }, // Normaliser les séparateurs de noms
|
|
{ from: /([A-Z]{2,})<<<<([A-Z]{2,})/g, to: '$1<<$2' }, // Réduire les séparateurs multiples
|
|
{ from: /([A-Z]{2,})<<<<<<([A-Z]{2,})/g, to: '$1<<$2' }, // Réduire les séparateurs multiples
|
|
{ from: /([A-Z]{2,})<<<<<</g, to: '$1<<' }, // Nettoyer les séparateurs en fin de nom
|
|
{ from: /([A-Z]{2,})<<<</g, to: '$1<<' }, // Nettoyer les séparateurs en fin de nom
|
|
|
|
// Corrections de caractères OCR courants
|
|
{ from: /0/g, to: 'O' }, // 0 -> O dans les noms
|
|
{ from: /1/g, to: 'I' }, // 1 -> I dans les noms
|
|
{ from: /5/g, to: 'S' }, // 5 -> S dans les noms
|
|
{ from: /8/g, to: 'B' }, // 8 -> B dans les noms
|
|
{ from: /6/g, to: 'G' }, // 6 -> G dans les noms
|
|
|
|
// Corrections génériques pour les erreurs OCR courantes dans les noms
|
|
{ from: /([A-Z]{2,})0([A-Z]{2,})/g, to: '$1O$2' }, // 0 -> O dans les noms
|
|
{ from: /([A-Z]{2,})1([A-Z]{2,})/g, to: '$1I$2' }, // 1 -> I dans les noms
|
|
{ from: /([A-Z]{2,})5([A-Z]{2,})/g, to: '$1S$2' }, // 5 -> S dans les noms
|
|
{ from: /([A-Z]{2,})8([A-Z]{2,})/g, to: '$1B$2' }, // 8 -> B dans les noms
|
|
{ from: /([A-Z]{2,})6([A-Z]{2,})/g, to: '$1G$2' }, // 6 -> G dans les noms
|
|
|
|
// Nettoyage des caractères parasites
|
|
{
|
|
from: /[^\w\sÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,;:!?()\-'"]/g,
|
|
to: ' ',
|
|
},
|
|
|
|
// Normalisation des espaces
|
|
{ from: /\s+/g, to: ' ' },
|
|
{ from: /^\s+|\s+$/g, to: '' },
|
|
]
|
|
|
|
// Appliquer les corrections
|
|
for (const correction of corrections) {
|
|
processedText = processedText.replace(correction.from, correction.to)
|
|
}
|
|
|
|
console.log(`[POST_PROCESS] Texte post-traité: ${processedText.length} caractères`)
|
|
return processedText
|
|
} catch (error) {
|
|
console.error(`[POST_PROCESS] Erreur post-traitement:`, error.message)
|
|
return text
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
extractTextFromImageEnhanced,
|
|
extractTextFromCNI,
|
|
extractTextFromStandardDocument,
|
|
runTesseractOCR,
|
|
postProcessCNIText,
|
|
}
|