diff --git a/lib/page/ai_detection.dart b/lib/page/ai_detection.dart index 661086a..37b0afa 100644 --- a/lib/page/ai_detection.dart +++ b/lib/page/ai_detection.dart @@ -6,13 +6,12 @@ import 'package:flutter/services.dart'; import 'package:image/image.dart' as img; import 'package:logger/logger.dart'; import 'package:sudoku/ml/yolov8/yolov8_output.dart'; -import 'package:sudoku/page/ai_detection_painter.dart'; +import 'package:sudoku/page/ai_detection_main.dart'; import 'package:sudoku/util/image_util.dart'; -import 'package:sudoku_dart/sudoku_dart.dart'; Logger log = Logger(); -class AIDetectPaintPage extends StatefulWidget { +class AIDetectPaintPage extends StatelessWidget { final ui.Image image; final Uint8List imageBytes; final YoloV8Output output; @@ -23,22 +22,6 @@ class AIDetectPaintPage extends StatefulWidget { required this.output, }); - @override - _AIDetectPainPageState createState() => _AIDetectPainPageState(); -} - -class _AIDetectPainPageState extends State { - var detectPuzzles; - var detectSolution; - - @override - void initState() { - super.initState(); - // 初始化检测 puzzle and solution - detectPuzzles = List.generate(81, (index) => -1); - detectSolution = List.generate(81, (index) => -1); - } - @override Widget build(BuildContext context) { _init() async { @@ -48,10 +31,10 @@ class _AIDetectPainPageState extends State { final screenHeight = screenSize.height.toInt(); // origin image size - final originImageWidth = widget.image.width; - final originImageHeight = widget.image.height; + final originImageWidth = image.width; + final originImageHeight = image.height; - final uiBytes = await widget.image.toByteData(); + final uiBytes = await image.toByteData(); final _min = min(screenWidth, screenHeight); final widthScale = 0.9 * _min / originImageWidth; @@ -67,11 +50,12 @@ class _AIDetectPainPageState extends State { height: (heightScale * originImageHeight).round(), ); final uiResizeImg = await ImageUtil.convertImageToFlutterUi(resizeImg); - final hasDetectionSudoku = widget.output.boxes.isNotEmpty; + final hasDetectionSudoku = output.boxes.isNotEmpty; + final List detectRefs = List.generate(81, (_) => null); if (hasDetectionSudoku) { // begin calculate sudoku rows and cols - final boxes = widget.output.boxes; + final boxes = output.boxes; // 计算单元格大小 final colBlock = originImageHeight ~/ 9; @@ -92,21 +76,24 @@ class _AIDetectPainPageState extends State { var colIndex = (x ~/ rowBlock) + ((x % rowBlock > rowBlockN) ? 1 : 0); var rowIndex = (y ~/ colBlock) + ((y % colBlock > colBlockN) ? 1 : 0); - var index = (rowIndex * 9 + colIndex).toInt(); + int index = (rowIndex * 9 + colIndex).toInt(); - detectPuzzles[index] = box.classId; + detectRefs[index] = DetectRef( + index: index, + value: box.classId, + box: box, + ); }); } return ( - (screenWidth, screenHeight), - (originImageWidth, originImageHeight), (widthScale, heightScale), uiResizeImg, + detectRefs, ); } - return FutureBuilder<((int, int), (int, int), (double, double), ui.Image)>( + return FutureBuilder<((double, double), ui.Image, List)>( future: _init(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { @@ -114,123 +101,27 @@ class _AIDetectPainPageState extends State { log.e(snapshot.error); } final ( - (screenWidth, screenHeight), - (originImageWidth, originImageHeight), (widthScale, heightScale), uiResizeImg, + detectRefs, ) = snapshot.requireData; - // 主画面控件 - var _mainWidget; - var hasDetectionSudoku = widget.output.boxes.isNotEmpty; - - if (!hasDetectionSudoku) { - _mainWidget = const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.block, - size: 128, - color: Colors.white, - shadows: [ui.Shadow(blurRadius: 1.68)], - ), - Center( - child: Text("Not Detected", - style: TextStyle( - fontSize: 36, - color: Colors.white, - shadows: [ui.Shadow(blurRadius: 1.68)], - )), - ), - ], - ); - } else { - final _gridWidget = GridView.builder( - padding: EdgeInsets.zero, - physics: NeverScrollableScrollPhysics(), - shrinkWrap: false, - itemCount: 81, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 9), - itemBuilder: ((BuildContext context, int index) { - var cellColor = - detectPuzzles[index] != -1 ? Colors.yellow : Colors.white; - var cellText = ""; - if (detectSolution[index] != -1) { - cellText = detectSolution[index].toString(); - } - if (detectPuzzles[index] != -1) { - cellText = detectPuzzles[index].toString(); - } - return Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.amberAccent, width: 1.5), - ), - child: Text( - cellText, - style: TextStyle( - shadows: [ui.Shadow(blurRadius: 1.68)], - fontSize: 30, - color: cellColor), - ), - ); - }), - ); - - _mainWidget = _gridWidget; - } - - var _drawWidget = CustomPaint( - child: _mainWidget, - painter: AIDetectionPainter( - image: uiResizeImg, - output: widget.output, - offset: ui.Offset(0, 0), - widthScale: widthScale, - heightScale: heightScale, - ), + return AIDetectionMainWidget( + detectRefs: detectRefs, + image: uiResizeImg, + imageBytes: imageBytes, + widthScale: widthScale, + heightScale: heightScale, + output: output, ); - - var _btnWidget = Offstage( - offstage: !hasDetectionSudoku, - child: IconButton( - icon: Icon(Icons.visibility), - iconSize: 36, - onPressed: _solveSudoku, - ), - ); - - var _bodyWidget = Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center( - child: SizedBox( - width: uiResizeImg.width.toDouble(), - height: uiResizeImg.height.toDouble(), - child: _drawWidget), - ), - Center(child: _btnWidget), - ], - ); - - return Scaffold( - appBar: AppBar(title: Text("Detection Result")), - body: _bodyWidget); } - return Center(child: CircularProgressIndicator()); + return Center( + child: CircularProgressIndicator( + color: Colors.amberAccent, + backgroundColor: Colors.white, + ), + ); }); } - - _solveSudoku() async { - log.d("解题中"); - try { - var sudoku = Sudoku(detectPuzzles); - setState(() { - detectSolution = sudoku.solution; - }); - } catch (e) { - log.e(e); - } - } } diff --git a/lib/page/ai_detection_main.dart b/lib/page/ai_detection_main.dart new file mode 100644 index 0000000..dfae75a --- /dev/null +++ b/lib/page/ai_detection_main.dart @@ -0,0 +1,262 @@ +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:logger/logger.dart'; +import 'package:sudoku/ml/yolov8/yolov8_output.dart'; +import 'package:sudoku/page/ai_detection_painter.dart'; +import 'package:sudoku_dart/sudoku_dart.dart'; + +Logger log = Logger(); + +/// just define magic -1 to SUDOKU_EMPTY_DIGIT , make code easier to read and know +const int SUDOKU_EMPTY_DIGIT = -1; + +/// Detect Ref +/// +/// +class DetectRef { + /// puzzle index + final int index; + + /// puzzle value of index + final int value; + + /// puzzle value of index from detect box + final YoloV8DetectionBox box; + + const DetectRef({ + required this.index, + required this.value, + required this.box, + }); +} + +class AIDetectionMainWidget extends StatefulWidget { + final List detectRefs; + final ui.Image image; + final Uint8List imageBytes; + final double widthScale; + final double heightScale; + final YoloV8Output output; + + const AIDetectionMainWidget({ + Key? key, + required this.detectRefs, + required this.image, + required this.imageBytes, + required this.widthScale, + required this.heightScale, + required this.output, + }) : super(key: key); + + @override + State createState() => _AIDetectionMainWidgetState(); +} + +class _AIDetectionMainWidgetState extends State { + + /// amendable cell edit on this "amendPuzzle" + /// + /// when amendPuzzle[index] != SUDOKU_EMPTY_DIGIT (-1) , grid cell of index will show value with blue color text + late List amendPuzzle; + + late List solution; + late String solveMessage; + int? selectedBox = null; + + @override + void initState() { + super.initState(); + + amendPuzzle = _emptyMatrix(); + solution = _emptyMatrix(); + solveMessage = ""; + } + + _emptyMatrix() { + return List.generate(81, (_) => SUDOKU_EMPTY_DIGIT); + } + + _solveSudoku() { + log.d("solve sudoku puzzle"); + try { + // merge puzzle from detectRefs and amendPuzzle + final List puzzle = _emptyMatrix(); + for (var index = 0; index < puzzle.length; ++index) { + DetectRef? detectRef = widget.detectRefs[index]; + if (amendPuzzle[index] != SUDOKU_EMPTY_DIGIT) { + puzzle[index] = amendPuzzle[index]; + } else if (detectRef != null && detectRef.value != SUDOKU_EMPTY_DIGIT) { + puzzle[index] = detectRef.value; + } + } + + final sudoku = Sudoku(puzzle); + setState(() { + solution = sudoku.solution; + solveMessage = ""; + }); + } catch (e) { + // seem this puzzle can't be solve because is wrong puzzle + log.e(e); + if (e.runtimeType == StateError) { + final errorMessage = (e as StateError).message; + setState(() { + solution = _emptyMatrix(); + solveMessage = errorMessage; + }); + } + } + } + + @override + Widget build(BuildContext context) { + final detectRefs = widget.detectRefs; + final uiImage = widget.image; + final widthScale = widget.widthScale; + final heightScale = widget.heightScale; + final output = widget.output; + + // 主画面控件 + var _mainWidget; + var hasDetectionSudoku = output.boxes.isNotEmpty; + + if (!hasDetectionSudoku) { + _mainWidget = const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.block, + size: 128, + color: Colors.white, + shadows: [ui.Shadow(blurRadius: 1.68)], + ), + Center( + child: Text("Not Detected", + style: TextStyle( + fontSize: 36, + color: Colors.white, + shadows: [ui.Shadow(blurRadius: 1.68)], + )), + ), + ], + ); + } else { + final _gridWidget = GridView.builder( + padding: EdgeInsets.zero, + physics: NeverScrollableScrollPhysics(), + shrinkWrap: false, + itemCount: 81, + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 9), + itemBuilder: ((BuildContext context, int index) { + DetectRef? detectRef = detectRefs[index]; + + // default cell text/style/border + var cellTextColor = detectRef != null && detectRef.value != SUDOKU_EMPTY_DIGIT + ? Colors.yellow + : Colors.white; + var cellText = ""; + var cellBorder = Border.all(color: Colors.amber, width: 1.5); + + if (amendPuzzle[index] != SUDOKU_EMPTY_DIGIT) { + // 修正的谜题 + cellText = amendPuzzle[index].toString(); + cellTextColor = Colors.blue; + } else if (detectRef != null && detectRef.value != SUDOKU_EMPTY_DIGIT) { + // 检测关联的谜题 + cellText = detectRef.value.toString(); + cellTextColor = Colors.yellow; + } else if (solution[index] != SUDOKU_EMPTY_DIGIT) { + // solutions + cellText = solution[index].toString(); + cellTextColor = Colors.white; + } + + if (index == selectedBox) { + // if choose cell , change the border + cellBorder = Border.all(color: Colors.blue, width: 2.0); + } + + var _cellContainer = Container( + decoration: BoxDecoration( + border: cellBorder, + ), + child: Text( + cellText, + style: TextStyle( + shadows: [ui.Shadow(blurRadius: 3.68)], + fontSize: 30, + color: cellTextColor), + ), + ); + + return InkWell( + child: _cellContainer, + onTap: () { + setState(() { + if (selectedBox == null) { + selectedBox = index; + } else if (selectedBox == index) { + selectedBox = null; + } else { + selectedBox = index; + + // @TODO here should show dialog to input amend value from user + } + }); + }, + ); + }), + ); + + _mainWidget = _gridWidget; + } + + var _drawWidget = CustomPaint( + child: _mainWidget, + painter: AIDetectionPainter( + image: uiImage, + output: output, + offset: ui.Offset(0, 0), + widthScale: widthScale, + heightScale: heightScale, + ), + ); + + var _btnWidget = Offstage( + offstage: !hasDetectionSudoku, + child: IconButton( + icon: Icon(Icons.visibility), + iconSize: 36, + onPressed: _solveSudoku, + ), + ); + + var _bodyWidget = Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // show solve message output + Center( + child: Text( + solveMessage, + style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red), + ), + ), + Center( + child: SizedBox( + width: uiImage.width.toDouble(), + height: uiImage.height.toDouble(), + child: _drawWidget), + ), + Center(child: _btnWidget), + ], + ); + + return Scaffold( + appBar: AppBar(title: Text("Detection Result")), + body: _bodyWidget, + ); + } +} diff --git a/lib/page/ai_detection_painter.dart b/lib/page/ai_detection_painter.dart index bf0043c..a65af07 100644 --- a/lib/page/ai_detection_painter.dart +++ b/lib/page/ai_detection_painter.dart @@ -21,6 +21,7 @@ class AIDetectionPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { var imgPaint = Paint(); + imgPaint.color = ui.Color.fromRGBO(128, 128, 128, 0.6); canvas.drawImage(image, offset, imgPaint); var boxPaint = ui.Paint()