feat: reintegration (en cours) de ml kit lecteur de doc
This commit is contained in:
@@ -17,6 +17,7 @@ import 'widgets/image_source_button.dart';
|
||||
import '../../services/image_crop_service.dart';
|
||||
import '../../services/opencv_target_service.dart';
|
||||
import '../../services/parallelism_service.dart'; // NOUVEAU
|
||||
import '../../services/document_scanner_service.dart'; // NOUVEAU : scanner doc redresse la cible
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
@@ -36,10 +37,13 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
|
||||
// NOUVEAU : Service IMU de parallélisme
|
||||
final ParallelismService _parallelismService = ParallelismService(
|
||||
alignThreshold: 15.0, // Seuil pour passer au vert
|
||||
misalignThreshold: 22.0, // Seuil pour repasser à l'orange
|
||||
alignThreshold: 25.0, // Seuil pour passer au vert (permissif)
|
||||
misalignThreshold: 32.0, // Seuil pour repasser à l'orange
|
||||
);
|
||||
|
||||
// NOUVEAU : Scanner de documents Google (redresse la cible de face)
|
||||
final DocumentScannerService _docScanner = DocumentScannerService();
|
||||
|
||||
String? _selectedImagePath;
|
||||
bool _isLoading = false;
|
||||
|
||||
@@ -53,8 +57,9 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
late AnimationController _scanAnimationController;
|
||||
late Animation<double> _scanAnimation;
|
||||
|
||||
// Détection OpenCV (inchangée)
|
||||
// Détection OpenCV (cible circulaire) — on garde le résultat COMPLET
|
||||
bool? _alignmentStatus;
|
||||
TargetDetectionResult? _targetResult; // centre + rayon de la cible
|
||||
Timer? _detectionTimer;
|
||||
bool _isAnalyzingFrame = false;
|
||||
|
||||
@@ -109,25 +114,39 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// NOUVEAU : Calcule la couleur et le message depuis les données IMU
|
||||
// Calcule la couleur et le message depuis l'IMU + la détection de cible
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Couleur du cadre selon le parallélisme détecté par l'IMU.
|
||||
Color get _frameColor {
|
||||
if (_parallelismData == null) return const Color(0xFF00FF00);
|
||||
return _parallelismData!.isAligned ? const Color(0xFF00FF00) : Colors.orange;
|
||||
/// La cible circulaire est-elle détectée et raisonnablement cadrée ?
|
||||
bool get _targetReady {
|
||||
final t = _targetResult;
|
||||
if (t == null || !t.success) return false;
|
||||
final bool centered =
|
||||
t.centerX > 0.15 && t.centerX < 0.85 && t.centerY > 0.15 && t.centerY < 0.85;
|
||||
final bool bigEnough = t.radius > 0.12;
|
||||
return centered && bigEnough;
|
||||
}
|
||||
|
||||
/// Message d'aide selon le parallélisme détecté.
|
||||
/// Couleur du cadre : dépend UNIQUEMENT du parallélisme IMU.
|
||||
Color get _frameColor {
|
||||
final bool imuAligned =
|
||||
_parallelismData == null || _parallelismData!.isAligned;
|
||||
return imuAligned ? const Color(0xFF00FF00) : Colors.orange;
|
||||
}
|
||||
|
||||
/// Message d'aide selon le parallélisme IMU.
|
||||
String get _alignmentMessage {
|
||||
if (_parallelismData == null) {
|
||||
if (_parallelismData == null ||
|
||||
_parallelismData!.status == ParallelismStatus.unknown) {
|
||||
return 'ALIGNEZ LA CIBLE DANS LE CADRE';
|
||||
}
|
||||
if (_parallelismData!.status == ParallelismStatus.aligned) {
|
||||
return 'PARALLÈLE OK — PRÊT À PHOTOGRAPHIER';
|
||||
|
||||
if (_parallelismData!.isAligned) {
|
||||
return _targetReady
|
||||
? 'PARFAIT — CIBLE DÉTECTÉE, PRÊT'
|
||||
: 'PARALLÈLE OK — PRÊT À SCANNER';
|
||||
}
|
||||
|
||||
// Message directif selon l'axe le plus dévié
|
||||
final double pitch = _parallelismData!.pitchDegrees;
|
||||
final double roll = _parallelismData!.rollDegrees;
|
||||
|
||||
@@ -143,7 +162,7 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Initialisation de la caméra (inchangée, sauf ajout IMU)
|
||||
// Initialisation de la caméra
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
Future<void> _initLiveCamera() async {
|
||||
final status = await Permission.camera.request();
|
||||
@@ -168,7 +187,7 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
if (_cameras != null && _cameras!.isNotEmpty) {
|
||||
_cameraController = CameraController(
|
||||
_cameras![0],
|
||||
ResolutionPreset.max,
|
||||
ResolutionPreset.veryHigh, // ~1080p : suffisant et bien plus rapide que max
|
||||
enableAudio: false,
|
||||
imageFormatGroup: ImageFormatGroup.jpeg,
|
||||
);
|
||||
@@ -179,6 +198,7 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
_isCameraInitialized = true;
|
||||
_showLiveCamera = true;
|
||||
_alignmentStatus = null;
|
||||
_targetResult = null;
|
||||
_parallelismData = null; // NOUVEAU : reset IMU
|
||||
});
|
||||
|
||||
@@ -193,7 +213,7 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Détection OpenCV périodique (inchangée)
|
||||
// Détection OpenCV périodique (cercle de cible en direct)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
void _startAlignmentDetection() {
|
||||
_detectionTimer?.cancel();
|
||||
@@ -204,8 +224,9 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
_cameraController!.startImageStream((CameraImage cameraImage) async {
|
||||
if (_isAnalyzingFrame) return;
|
||||
final now = DateTime.now();
|
||||
// Cadence ~1 s : assez réactif pour suivre la cible sans saturer le CPU.
|
||||
if (_lastAnalysis != null &&
|
||||
now.difference(_lastAnalysis!).inSeconds < 3) return;
|
||||
now.difference(_lastAnalysis!).inMilliseconds < 1000) return;
|
||||
_lastAnalysis = now;
|
||||
_isAnalyzingFrame = true;
|
||||
|
||||
@@ -229,6 +250,8 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
|
||||
if (mounted && _showLiveCamera) {
|
||||
setState(() {
|
||||
_targetResult = result.success ? result : null;
|
||||
|
||||
if (!result.success) {
|
||||
_alignmentStatus = null;
|
||||
} else {
|
||||
@@ -258,6 +281,7 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
}
|
||||
} catch (_) {}
|
||||
_alignmentStatus = null;
|
||||
_targetResult = null;
|
||||
}
|
||||
|
||||
img.Image? _convertCameraImage(CameraImage cameraImage) {
|
||||
@@ -418,11 +442,9 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Vue caméra live — MODIFIÉE pour utiliser les données IMU
|
||||
// Vue caméra live (aide au cadrage : parallélisme + cercle de détection)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
Widget _buildLiveCameraView() {
|
||||
// Priorité : IMU (parallélisme physique) prime sur OpenCV (centrage)
|
||||
// Si l'IMU dit "mal aligné" → orange, sinon → vert
|
||||
final Color frameColor = _frameColor;
|
||||
final String alignmentMessage = _alignmentMessage;
|
||||
|
||||
@@ -439,6 +461,21 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
),
|
||||
),
|
||||
|
||||
// 1.bis Cercle dessiné autour de la cible circulaire détectée
|
||||
if (_targetResult != null && _targetResult!.success)
|
||||
Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 3 / 4,
|
||||
child: CustomPaint(
|
||||
painter: _TargetCirclePainter(
|
||||
target: _targetResult!,
|
||||
color: _targetReady ? frameColor : Colors.white,
|
||||
highlighted: _targetReady,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 2. Cadre de visée avec coins et mire centrale
|
||||
Center(
|
||||
child: Container(
|
||||
@@ -555,14 +592,14 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
),
|
||||
),
|
||||
|
||||
// 5. Bouton déclencheur
|
||||
// 5. Bouton déclencheur → lance le scanner de documents Google
|
||||
Positioned(
|
||||
bottom: 40,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: GestureDetector(
|
||||
onTap: _takePictureManually,
|
||||
onTap: _scanWithDocumentScanner,
|
||||
child: Container(
|
||||
height: 80,
|
||||
width: 80,
|
||||
@@ -578,6 +615,7 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.document_scanner, color: Colors.black54),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -691,34 +729,51 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Capture manuelle (inchangée, sauf ajout stop IMU)
|
||||
// NOUVEAU : Déclic → lance le scanner de documents Google qui redresse
|
||||
// automatiquement la cible (feuille rectangulaire) de face.
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
Future<void> _takePictureManually() async {
|
||||
if (_cameraController == null || !_cameraController!.value.isInitialized) {
|
||||
return;
|
||||
}
|
||||
Future<void> _scanWithDocumentScanner() async {
|
||||
// On stoppe la caméra custom : le scanner Google ouvre sa propre caméra.
|
||||
_stopAlignmentDetection();
|
||||
_stopParallelismDetection();
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_showLiveCamera = false;
|
||||
});
|
||||
|
||||
// Libère la caméra custom pour éviter tout conflit d'accès au capteur.
|
||||
try {
|
||||
try {
|
||||
await _cameraController!.setZoomLevel(1.0);
|
||||
} catch (_) {}
|
||||
await _cameraController?.dispose();
|
||||
_cameraController = null;
|
||||
_isCameraInitialized = false;
|
||||
} catch (_) {}
|
||||
|
||||
_stopAlignmentDetection();
|
||||
_stopParallelismDetection(); // NOUVEAU
|
||||
try {
|
||||
final result = await _docScanner.scanTarget();
|
||||
|
||||
final XFile photo = await _cameraController!.takePicture();
|
||||
if (!result.success) {
|
||||
// L'utilisateur a annulé le scan : on ne fait rien de plus.
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_selectedImagePath = photo.path;
|
||||
_showLiveCamera = false;
|
||||
_selectedImagePath = result.imagePath;
|
||||
});
|
||||
|
||||
_analyzeImage();
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du clic photo: $e');
|
||||
debugPrint('Erreur scanner document: $e');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Le scan a échoué. Réessayez.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setState(() => _isLoading = false);
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,4 +871,62 @@ class _CaptureScreenState extends State<CaptureScreen>
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Painter qui dessine un cercle autour de la cible circulaire détectée.
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
class _TargetCirclePainter extends CustomPainter {
|
||||
final TargetDetectionResult target;
|
||||
final Color color;
|
||||
final bool highlighted;
|
||||
|
||||
_TargetCirclePainter({
|
||||
required this.target,
|
||||
required this.color,
|
||||
required this.highlighted,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final double cx = target.centerX * size.width;
|
||||
final double cy = target.centerY * size.height;
|
||||
final double r = target.radius * math.min(size.width, size.height);
|
||||
|
||||
final Paint stroke = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = highlighted ? 3.5 : 2.0
|
||||
..color = color;
|
||||
|
||||
canvas.drawCircle(Offset(cx, cy), r, stroke);
|
||||
|
||||
final Paint cross = Paint()
|
||||
..strokeWidth = 2.0
|
||||
..color = color;
|
||||
const double k = 10;
|
||||
canvas.drawLine(Offset(cx - k, cy), Offset(cx + k, cy), cross);
|
||||
canvas.drawLine(Offset(cx, cy - k), Offset(cx, cy + k), cross);
|
||||
|
||||
if (highlighted) {
|
||||
final tp = TextPainter(
|
||||
text: TextSpan(
|
||||
text: ' CIBLE ',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
backgroundColor: color,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
tp.paint(canvas, Offset(cx - tp.width / 2, (cy - r - 20).clamp(0.0, size.height)));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_TargetCirclePainter old) =>
|
||||
old.target != target ||
|
||||
old.color != color ||
|
||||
old.highlighted != highlighted;
|
||||
}
|
||||
Reference in New Issue
Block a user