265 lines
7.4 KiB
JavaScript
265 lines
7.4 KiB
JavaScript
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(/</g, ' ').trim(),
|
|
givenNames: line2.substring(0, 30).replace(/</g, ' ').trim(),
|
|
documentNumber: line2.substring(30, 36).trim(),
|
|
}
|
|
|
|
console.log(`[MRZ_DECODE] MRZ décodée: ${result.surname} ${result.givenNames}`)
|
|
return result
|
|
} catch (error) {
|
|
console.error(`[MRZ_DECODE] Erreur décodage MRZ:`, error.message)
|
|
return null
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
isCNIDocument,
|
|
extractMRZ,
|
|
segmentCNIZones,
|
|
extractCNIZone,
|
|
enhanceCNIPreprocessing,
|
|
processCNIWithZones,
|
|
decodeMRZ,
|
|
}
|