680 lines
21 KiB
Dart
680 lines
21 KiB
Dart
/// Service de correction de distorsion d'objectif.
|
|
///
|
|
/// Utilise la calibration des cercles de la cible pour calculer et appliquer
|
|
/// une transformation qui corrige la distorsion de l'objectif. L'image est
|
|
/// transformée pour que les cercles calibrés deviennent parfaitement circulaires.
|
|
library;
|
|
|
|
import 'dart:io';
|
|
import 'dart:math' as math;
|
|
import 'package:image/image.dart' as img;
|
|
import 'package:opencv_dart/opencv_dart.dart' as cv;
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
/// Paramètres de distorsion calculés à partir de la calibration
|
|
class DistortionParameters {
|
|
/// Ratio d'aplatissement horizontal (1.0 = pas de correction)
|
|
final double scaleX;
|
|
|
|
/// Ratio d'aplatissement vertical (1.0 = pas de correction)
|
|
final double scaleY;
|
|
|
|
/// Angle de rotation de l'axe principal (en radians)
|
|
final double rotation;
|
|
|
|
/// Centre de la distorsion en coordonnées normalisées (0-1)
|
|
final double centerX;
|
|
final double centerY;
|
|
|
|
/// Ratio de circularité détecté (1.0 = cercle parfait)
|
|
final double circularityRatio;
|
|
|
|
const DistortionParameters({
|
|
required this.scaleX,
|
|
required this.scaleY,
|
|
required this.rotation,
|
|
required this.centerX,
|
|
required this.centerY,
|
|
required this.circularityRatio,
|
|
});
|
|
|
|
/// Paramètres par défaut (pas de correction)
|
|
static const identity = DistortionParameters(
|
|
scaleX: 1.0,
|
|
scaleY: 1.0,
|
|
rotation: 0.0,
|
|
centerX: 0.5,
|
|
centerY: 0.5,
|
|
circularityRatio: 1.0,
|
|
);
|
|
|
|
/// La correction est-elle nécessaire ?
|
|
bool get needsCorrection => circularityRatio < 0.95;
|
|
|
|
@override
|
|
String toString() {
|
|
return 'DistortionParameters(scaleX: ${scaleX.toStringAsFixed(3)}, '
|
|
'scaleY: ${scaleY.toStringAsFixed(3)}, '
|
|
'rotation: ${(rotation * 180 / math.pi).toStringAsFixed(1)}°, '
|
|
'circularity: ${(circularityRatio * 100).toStringAsFixed(1)}%)';
|
|
}
|
|
}
|
|
|
|
/// Service pour détecter et corriger la distorsion d'objectif
|
|
class DistortionCorrectionService {
|
|
/// Calcule les paramètres de distorsion à partir de la calibration des cercles
|
|
///
|
|
/// [targetCenterX], [targetCenterY] : Centre de la cible calibré (0-1)
|
|
/// [targetRadius] : Rayon de la cible calibré
|
|
/// [imageAspectRatio] : Ratio largeur/hauteur de l'image
|
|
///
|
|
/// Cette méthode analyse la forme attendue (cercle) vs la forme observée
|
|
/// pour déterminer les paramètres de correction.
|
|
DistortionParameters calculateDistortionFromCalibration({
|
|
required double targetCenterX,
|
|
required double targetCenterY,
|
|
required double targetRadius,
|
|
required double imageAspectRatio,
|
|
}) {
|
|
// En théorie, si la cible est un cercle parfait et que l'utilisateur
|
|
// a calibré les anneaux pour qu'ils correspondent à ce qu'il voit,
|
|
// on peut déduire la distorsion.
|
|
|
|
// Pour l'instant, on utilise une approche simplifiée basée sur l'aspect ratio
|
|
// et la position du centre par rapport au centre de l'image.
|
|
|
|
// Calcul de l'excentricité basée sur la position du centre
|
|
final offsetX = targetCenterX - 0.5;
|
|
final offsetY = targetCenterY - 0.5;
|
|
final offsetDistance = math.sqrt(offsetX * offsetX + offsetY * offsetY);
|
|
|
|
// Plus le centre est éloigné du centre de l'image, plus la distorsion est probable
|
|
// Estimation simplifiée de la distorsion radiale
|
|
final distortionFactor = 1.0 + offsetDistance * 0.2;
|
|
|
|
// Calculer l'angle de l'axe principal de déformation
|
|
final angle = math.atan2(offsetY, offsetX);
|
|
|
|
// Si l'image n'est pas carrée, tenir compte de l'aspect ratio
|
|
double scaleX = 1.0;
|
|
double scaleY = 1.0;
|
|
|
|
if (imageAspectRatio > 1.0) {
|
|
// Image plus large que haute
|
|
scaleY = distortionFactor;
|
|
} else if (imageAspectRatio < 1.0) {
|
|
// Image plus haute que large
|
|
scaleX = distortionFactor;
|
|
}
|
|
|
|
final circularityRatio = 1.0 / distortionFactor;
|
|
|
|
return DistortionParameters(
|
|
scaleX: scaleX,
|
|
scaleY: scaleY,
|
|
rotation: angle,
|
|
centerX: targetCenterX,
|
|
centerY: targetCenterY,
|
|
circularityRatio: circularityRatio,
|
|
);
|
|
}
|
|
|
|
/// Calcule les paramètres de distorsion en comparant un cercle théorique
|
|
/// avec les points de calibration fournis par l'utilisateur
|
|
///
|
|
/// [calibrationPoints] : Points sur l'ellipse visible (coordonnées 0-1)
|
|
/// [expectedRadius] : Rayon du cercle théorique
|
|
/// [centerX], [centerY] : Centre du cercle théorique
|
|
DistortionParameters calculateDistortionFromPoints({
|
|
required List<({double x, double y})> calibrationPoints,
|
|
required double expectedRadius,
|
|
required double centerX,
|
|
required double centerY,
|
|
}) {
|
|
if (calibrationPoints.length < 4) {
|
|
return DistortionParameters.identity;
|
|
}
|
|
|
|
// Calculer les distances de chaque point au centre
|
|
final distances = <double>[];
|
|
final angles = <double>[];
|
|
|
|
for (final point in calibrationPoints) {
|
|
final dx = point.x - centerX;
|
|
final dy = point.y - centerY;
|
|
distances.add(math.sqrt(dx * dx + dy * dy));
|
|
angles.add(math.atan2(dy, dx));
|
|
}
|
|
|
|
// Trouver les axes majeur et mineur de l'ellipse
|
|
double maxDist = 0, minDist = double.infinity;
|
|
double maxAngle = 0;
|
|
|
|
for (int i = 0; i < distances.length; i++) {
|
|
if (distances[i] > maxDist) {
|
|
maxDist = distances[i];
|
|
maxAngle = angles[i];
|
|
}
|
|
if (distances[i] < minDist) {
|
|
minDist = distances[i];
|
|
}
|
|
}
|
|
|
|
// Calculer les facteurs de correction
|
|
// On veut que maxDist et minDist deviennent égaux à expectedRadius
|
|
final scaleX = expectedRadius / maxDist;
|
|
final scaleY = expectedRadius / minDist;
|
|
final circularityRatio = minDist / maxDist;
|
|
|
|
return DistortionParameters(
|
|
scaleX: scaleX.clamp(0.5, 2.0),
|
|
scaleY: scaleY.clamp(0.5, 2.0),
|
|
rotation: maxAngle,
|
|
centerX: centerX,
|
|
centerY: centerY,
|
|
circularityRatio: circularityRatio,
|
|
);
|
|
}
|
|
|
|
/// Applique la correction de distorsion à une image
|
|
///
|
|
/// [imagePath] : Chemin de l'image source
|
|
/// [params] : Paramètres de distorsion calculés
|
|
///
|
|
/// Retourne le chemin de l'image corrigée
|
|
Future<String> applyCorrection(
|
|
String imagePath,
|
|
DistortionParameters params,
|
|
) async {
|
|
final file = File(imagePath);
|
|
final bytes = await file.readAsBytes();
|
|
final image = img.decodeImage(bytes);
|
|
|
|
if (image == null) {
|
|
throw Exception('Impossible de décoder l\'image');
|
|
}
|
|
|
|
final correctedImage = _transformImage(image, params);
|
|
|
|
// Sauvegarder l'image corrigée
|
|
final tempDir = await getTemporaryDirectory();
|
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
|
final outputPath = '${tempDir.path}/corrected_$timestamp.jpg';
|
|
|
|
final outputFile = File(outputPath);
|
|
await outputFile.writeAsBytes(img.encodeJpg(correctedImage, quality: 95));
|
|
|
|
return outputPath;
|
|
}
|
|
|
|
/// Transforme l'image pour corriger la distorsion
|
|
img.Image _transformImage(img.Image source, DistortionParameters params) {
|
|
final width = source.width;
|
|
final height = source.height;
|
|
|
|
// Créer une nouvelle image de même taille
|
|
final result = img.Image(width: width, height: height);
|
|
|
|
// Centre de transformation en pixels
|
|
final cx = params.centerX * width;
|
|
final cy = params.centerY * height;
|
|
|
|
// Précalculer cos/sin de la rotation
|
|
final cosR = math.cos(params.rotation);
|
|
final sinR = math.sin(params.rotation);
|
|
|
|
// Pour chaque pixel de l'image destination, trouver le pixel source correspondant
|
|
for (int y = 0; y < height; y++) {
|
|
for (int x = 0; x < width; x++) {
|
|
// Coordonnées relatives au centre
|
|
final dx = x - cx;
|
|
final dy = y - cy;
|
|
|
|
// Rotation inverse
|
|
final rx = dx * cosR + dy * sinR;
|
|
final ry = -dx * sinR + dy * cosR;
|
|
|
|
// Mise à l'échelle inverse (pour trouver d'où vient le pixel)
|
|
final sx = rx / params.scaleX;
|
|
final sy = ry / params.scaleY;
|
|
|
|
// Rotation dans l'autre sens
|
|
final fx = sx * cosR - sy * sinR;
|
|
final fy = sx * sinR + sy * cosR;
|
|
|
|
// Coordonnées source
|
|
final srcX = fx + cx;
|
|
final srcY = fy + cy;
|
|
|
|
// Interpolation bilinéaire
|
|
final pixel = _bilinearInterpolate(source, srcX, srcY);
|
|
result.setPixel(x, y, pixel);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Interpolation bilinéaire pour un échantillonnage de qualité
|
|
img.Color _bilinearInterpolate(img.Image image, double x, double y) {
|
|
final x0 = x.floor();
|
|
final y0 = y.floor();
|
|
final x1 = x0 + 1;
|
|
final y1 = y0 + 1;
|
|
|
|
// Vérifier les limites
|
|
if (x0 < 0 || y0 < 0 || x1 >= image.width || y1 >= image.height) {
|
|
// Retourner le pixel le plus proche pour les zones hors limites
|
|
return image.getPixel(
|
|
x.round().clamp(0, image.width - 1),
|
|
y.round().clamp(0, image.height - 1),
|
|
);
|
|
}
|
|
|
|
// Poids pour l'interpolation
|
|
final wx = x - x0;
|
|
final wy = y - y0;
|
|
|
|
// Récupérer les 4 pixels voisins
|
|
final p00 = image.getPixel(x0, y0);
|
|
final p10 = image.getPixel(x1, y0);
|
|
final p01 = image.getPixel(x0, y1);
|
|
final p11 = image.getPixel(x1, y1);
|
|
|
|
// Interpoler chaque canal
|
|
final r = _lerp2D(
|
|
p00.r.toDouble(),
|
|
p10.r.toDouble(),
|
|
p01.r.toDouble(),
|
|
p11.r.toDouble(),
|
|
wx,
|
|
wy,
|
|
);
|
|
final g = _lerp2D(
|
|
p00.g.toDouble(),
|
|
p10.g.toDouble(),
|
|
p01.g.toDouble(),
|
|
p11.g.toDouble(),
|
|
wx,
|
|
wy,
|
|
);
|
|
final b = _lerp2D(
|
|
p00.b.toDouble(),
|
|
p10.b.toDouble(),
|
|
p01.b.toDouble(),
|
|
p11.b.toDouble(),
|
|
wx,
|
|
wy,
|
|
);
|
|
final a = _lerp2D(
|
|
p00.a.toDouble(),
|
|
p10.a.toDouble(),
|
|
p01.a.toDouble(),
|
|
p11.a.toDouble(),
|
|
wx,
|
|
wy,
|
|
);
|
|
|
|
return img.ColorRgba8(
|
|
r.round().clamp(0, 255),
|
|
g.round().clamp(0, 255),
|
|
b.round().clamp(0, 255),
|
|
a.round().clamp(0, 255),
|
|
);
|
|
}
|
|
|
|
/// Interpolation linéaire 2D
|
|
double _lerp2D(
|
|
double v00,
|
|
double v10,
|
|
double v01,
|
|
double v11,
|
|
double wx,
|
|
double wy,
|
|
) {
|
|
final top = v00 * (1 - wx) + v10 * wx;
|
|
final bottom = v01 * (1 - wx) + v11 * wx;
|
|
return top * (1 - wy) + bottom * wy;
|
|
}
|
|
|
|
/// Applique une correction de perspective simple basée sur 4 points
|
|
///
|
|
/// Cette méthode est utile quand la photo est prise en angle.
|
|
/// [corners] : Les 4 coins de la cible dans l'ordre (haut-gauche, haut-droite, bas-droite, bas-gauche)
|
|
Future<String> applyPerspectiveCorrection(
|
|
String imagePath,
|
|
List<({double x, double y})> corners,
|
|
) async {
|
|
if (corners.length != 4) {
|
|
throw ArgumentError('4 points de coin sont requis');
|
|
}
|
|
|
|
final file = File(imagePath);
|
|
final bytes = await file.readAsBytes();
|
|
final image = img.decodeImage(bytes);
|
|
|
|
if (image == null) {
|
|
throw Exception('Impossible de décoder l\'image');
|
|
}
|
|
|
|
final width = image.width;
|
|
final height = image.height;
|
|
|
|
// Convertir les coordonnées normalisées en pixels
|
|
final srcCorners = corners
|
|
.map((c) => (x: c.x * width, y: c.y * height))
|
|
.toList();
|
|
|
|
// Calculer la taille du rectangle destination
|
|
// On prend la moyenne des largeurs et hauteurs
|
|
final topWidth = _distance(srcCorners[0], srcCorners[1]);
|
|
final bottomWidth = _distance(srcCorners[3], srcCorners[2]);
|
|
final leftHeight = _distance(srcCorners[0], srcCorners[3]);
|
|
final rightHeight = _distance(srcCorners[1], srcCorners[2]);
|
|
|
|
final dstWidth = ((topWidth + bottomWidth) / 2).round();
|
|
final dstHeight = ((leftHeight + rightHeight) / 2).round();
|
|
|
|
// Créer l'image destination
|
|
final result = img.Image(width: dstWidth, height: dstHeight);
|
|
|
|
// Calculer la matrice de transformation perspective
|
|
final matrix = _computePerspectiveMatrix(srcCorners, [
|
|
(x: 0.0, y: 0.0),
|
|
(x: dstWidth.toDouble(), y: 0.0),
|
|
(x: dstWidth.toDouble(), y: dstHeight.toDouble()),
|
|
(x: 0.0, y: dstHeight.toDouble()),
|
|
]);
|
|
|
|
// Appliquer la transformation
|
|
for (int y = 0; y < dstHeight; y++) {
|
|
for (int x = 0; x < dstWidth; x++) {
|
|
final src = _applyPerspectiveTransform(
|
|
matrix,
|
|
x.toDouble(),
|
|
y.toDouble(),
|
|
);
|
|
|
|
if (src.x >= 0 && src.x < width && src.y >= 0 && src.y < height) {
|
|
final pixel = _bilinearInterpolate(image, src.x, src.y);
|
|
result.setPixel(x, y, pixel);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sauvegarder
|
|
final tempDir = await getTemporaryDirectory();
|
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
|
final outputPath = '${tempDir.path}/perspective_$timestamp.jpg';
|
|
|
|
final outputFile = File(outputPath);
|
|
await outputFile.writeAsBytes(img.encodeJpg(result, quality: 95));
|
|
|
|
return outputPath;
|
|
}
|
|
|
|
double _distance(({double x, double y}) p1, ({double x, double y}) p2) {
|
|
final dx = p2.x - p1.x;
|
|
final dy = p2.y - p1.y;
|
|
return math.sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
/// Calcule la matrice de transformation perspective (homographie)
|
|
List<double> _computePerspectiveMatrix(
|
|
List<({double x, double y})> src,
|
|
List<({double x, double y})> dst,
|
|
) {
|
|
// Résolution du système linéaire pour trouver la matrice 3x3
|
|
// Utilisation de la méthode DLT (Direct Linear Transform)
|
|
|
|
final a = List<List<double>>.generate(8, (_) => List.filled(9, 0.0));
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
final sx = src[i].x;
|
|
final sy = src[i].y;
|
|
final dx = dst[i].x;
|
|
final dy = dst[i].y;
|
|
|
|
a[i * 2] = [-sx, -sy, -1, 0, 0, 0, dx * sx, dx * sy, dx];
|
|
a[i * 2 + 1] = [0, 0, 0, -sx, -sy, -1, dy * sx, dy * sy, dy];
|
|
}
|
|
|
|
// Résolution par SVD simplifiée (on utilise une approximation)
|
|
// Pour une implémentation complète, il faudrait une vraie décomposition SVD
|
|
final h = _solveHomography(a);
|
|
|
|
return h;
|
|
}
|
|
|
|
/// Résout le système linéaire pour trouver la matrice d'homographie 3x3.
|
|
/// Utilise l'élimination de Gauss-Jordan avec pivot partiel pour la stabilité.
|
|
List<double> _solveHomography(List<List<double>> a) {
|
|
// Le système 'a' est de taille 8x9 (8 équations, 9 inconnues).
|
|
// On fixe h8 = 1.0 pour résoudre le système, ce qui nous donne un système 8x8.
|
|
final int n = 8;
|
|
final List<List<double>> matrix = List.generate(
|
|
n,
|
|
(i) => List<double>.from(a[i]),
|
|
);
|
|
|
|
// Vecteur B (les constantes de l'autre côté de l'égalité)
|
|
// Dans DLT, -h8 * dx (ou dy) devient le terme constant.
|
|
final List<double> b = List.generate(n, (i) => -matrix[i][8]);
|
|
|
|
// Élimination de Gauss-Jordan
|
|
for (int i = 0; i < n; i++) {
|
|
// Recherche du pivot (valeur maximale dans la colonne pour limiter les erreurs)
|
|
int pivot = i;
|
|
for (int j = i + 1; j < n; j++) {
|
|
if (matrix[j][i].abs() > matrix[pivot][i].abs()) {
|
|
pivot = j;
|
|
}
|
|
}
|
|
|
|
// Échange des lignes (si nécessaire)
|
|
final List<double> tempRow = matrix[i];
|
|
matrix[i] = matrix[pivot];
|
|
matrix[pivot] = tempRow;
|
|
|
|
final double tempB = b[i];
|
|
b[i] = b[pivot];
|
|
b[pivot] = tempB;
|
|
|
|
// Vérification de la singularité (division par zéro impossible)
|
|
if (matrix[i][i].abs() < 1e-10) {
|
|
return [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Retourne identité si échec
|
|
}
|
|
|
|
// Normalisation de la ligne pivot
|
|
for (int j = i + 1; j < n; j++) {
|
|
final double factor = matrix[j][i] / matrix[i][i];
|
|
b[j] -= factor * b[i];
|
|
for (int k = i; k < n; k++) {
|
|
matrix[j][k] -= factor * matrix[i][k];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Substitution arrière
|
|
final List<double> h = List.filled(9, 0.0);
|
|
for (int i = n - 1; i >= 0; i--) {
|
|
double sum = 0.0;
|
|
for (int j = i + 1; j < n; j++) {
|
|
sum += matrix[i][j] * h[j];
|
|
}
|
|
h[i] = (b[i] - sum) / matrix[i][i];
|
|
}
|
|
|
|
h[8] = 1.0; // Normalisation finale
|
|
return h;
|
|
}
|
|
|
|
({double x, double y}) _applyPerspectiveTransform(
|
|
List<double> h,
|
|
double x,
|
|
double y,
|
|
) {
|
|
final w = h[6] * x + h[7] * y + h[8];
|
|
if (w.abs() < 1e-10) {
|
|
return (x: x, y: y);
|
|
}
|
|
final nx = (h[0] * x + h[1] * y + h[2]) / w;
|
|
final ny = (h[3] * x + h[4] * y + h[5]) / w;
|
|
return (x: nx, y: ny);
|
|
}
|
|
|
|
/// Corrige la perspective en se basant sur la détection de cercles (ellipses)
|
|
/// dans l'image.
|
|
///
|
|
/// Cette méthode tente de détecter l'ellipse la plus proéminente (la cible)
|
|
/// et calcule une transformation pour la rendre parfaitement circulaire.
|
|
Future<String> correctPerspectiveUsingCircles(String imagePath) async {
|
|
try {
|
|
// 1. Charger l'image avec OpenCV
|
|
final src = cv.imread(imagePath, flags: cv.IMREAD_COLOR);
|
|
if (src.isEmpty) throw Exception("Impossible de charger l'image");
|
|
|
|
// 2. Prétraitement
|
|
final gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY);
|
|
final blurred = cv.gaussianBlur(gray, (5, 5), 0);
|
|
|
|
// Canny edge detector avec seuil adaptatif (Otsu)
|
|
final thresh = cv.threshold(
|
|
blurred,
|
|
0,
|
|
255,
|
|
cv.THRESH_BINARY | cv.THRESH_OTSU,
|
|
);
|
|
final edges = cv.canny(blurred, thresh.$1 * 0.5, thresh.$1);
|
|
|
|
// 3. Trouver les contours
|
|
final contoursResult = cv.findContours(
|
|
edges,
|
|
cv.RETR_EXTERNAL,
|
|
cv.CHAIN_APPROX_SIMPLE,
|
|
);
|
|
final contours = contoursResult.$1;
|
|
|
|
if (contours.isEmpty) return imagePath; // Pas de contours trouvés
|
|
|
|
// 4. Trouver le meilleur candidat ellipse
|
|
cv.RotatedRect? bestEllipse;
|
|
double maxArea = 0;
|
|
|
|
for (final contour in contours) {
|
|
if (contour.length < 5)
|
|
continue; // fitEllipse nécessite au moins 5 points
|
|
|
|
final area = cv.contourArea(contour);
|
|
if (area < 1000) continue; // Ignorer les trop petits bruits
|
|
|
|
final ellipse = cv.fitEllipse(contour);
|
|
|
|
// Critère de sélection: on cherche la plus grande ellipse qui est proche d'un cercle
|
|
// Mais comme on veut corriger la distorsion, elle PEUT être aplatie.
|
|
// Donc on prend juste la plus grande ellipse raisonnablement centrée.
|
|
if (area > maxArea) {
|
|
maxArea = area;
|
|
bestEllipse = ellipse;
|
|
}
|
|
}
|
|
|
|
if (bestEllipse == null) return imagePath;
|
|
|
|
// 5. Calculer la transformation perspective
|
|
// L'idée est de mapper les 4 sommets de l'ellipse détectée vers un cercle parfait.
|
|
// Ou plus simplement, mapper le rectangle englobant de l'ellipse vers un carré.
|
|
|
|
// Points source: les 4 coins du rotated rect de l'ellipse
|
|
// Note: opencv_dart RotatedRect points() non dispo directement?
|
|
// On peut utiliser boxPoints(ellipse)
|
|
final boxPoints = cv.boxPoints(bestEllipse);
|
|
// boxPoints returns Mat (4x2 float32)
|
|
|
|
// Extraire les 4 points
|
|
final List<cv.Point> srcPoints = [];
|
|
|
|
for (int i = 0; i < boxPoints.length; i++) {
|
|
// On accède directement au point à l'index i
|
|
final point2f = boxPoints[i];
|
|
|
|
// On convertit les coordonnées float en int pour cv.Point
|
|
srcPoints.add(cv.Point(point2f.x.toInt(), point2f.y.toInt()));
|
|
}
|
|
|
|
// Trier les points pour avoir: TL, TR, BR, BL
|
|
_sortPoints(srcPoints);
|
|
|
|
// Dimensions cibles
|
|
final side = math
|
|
.max(bestEllipse.size.width, bestEllipse.size.height)
|
|
.toInt();
|
|
|
|
final List<cv.Point> dstPoints = [
|
|
cv.Point(0, 0),
|
|
cv.Point(side, 0),
|
|
cv.Point(side, side),
|
|
cv.Point(0, side),
|
|
];
|
|
|
|
// Matrice de perspective
|
|
final M = cv.getPerspectiveTransform(
|
|
cv.VecPoint.fromList(srcPoints),
|
|
cv.VecPoint.fromList(dstPoints),
|
|
);
|
|
|
|
// 6. Warper l'image
|
|
final corrected = cv.warpPerspective(src, M, (side, side));
|
|
|
|
// 7. Sauvegarder
|
|
final tempDir = await getTemporaryDirectory();
|
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
|
final outputPath = '${tempDir.path}/corrected_circle_$timestamp.jpg';
|
|
|
|
cv.imwrite(outputPath, corrected);
|
|
|
|
return outputPath;
|
|
} catch (e) {
|
|
// En cas d'erreur, retourner l'image originale
|
|
print('Erreur correction perspective cercles: $e');
|
|
return imagePath;
|
|
}
|
|
}
|
|
|
|
/// Trie les points dans l'ordre: Top-Left, Top-Right, Bottom-Right, Bottom-Left
|
|
void _sortPoints(List<cv.Point> points) {
|
|
// Calculer le centre de gravité
|
|
double cx = 0;
|
|
double cy = 0;
|
|
for (final p in points) {
|
|
cx += p.x;
|
|
cy += p.y;
|
|
}
|
|
cx /= points.length;
|
|
cy /= points.length;
|
|
|
|
points.sort((a, b) {
|
|
// Trier par angle autour du centre
|
|
final angleA = math.atan2(a.y - cy, a.x - cx);
|
|
final angleB = math.atan2(b.y - cy, b.x - cx);
|
|
return angleA.compareTo(angleB);
|
|
});
|
|
|
|
// Re-trier pour être sûr:
|
|
points.sort((a, b) => (a.y + a.x).compareTo(b.y + b.x));
|
|
final tl = points[0];
|
|
final br = points[3];
|
|
|
|
// Reste tr et bl
|
|
final remaining = [points[1], points[2]];
|
|
remaining.sort((a, b) => a.x.compareTo(b.x));
|
|
final bl = remaining[0];
|
|
final tr = remaining[1];
|
|
|
|
points[0] = tl;
|
|
points[1] = tr;
|
|
points[2] = br;
|
|
points[3] = bl;
|
|
}
|
|
}
|