44// A captcha input dialog.
55
66import 'dart:typed_data' ;
7+ import 'dart:math' ;
78
89import 'package:flutter/material.dart' ;
910import 'package:flutter_i18n/flutter_i18n.dart' ;
1011import 'package:watermeter/page/public_widget/toast.dart' ;
1112import 'package:image/image.dart' as img;
1213import 'package:tflite_flutter/tflite_flutter.dart' ;
1314
15+ enum DigitCaptchaType { payment, zfw }
16+
1417class DigitCaptchaClientProvider {
15- static const String _interpreterAssetName = 'assets/captcha-solver.tflite' ;
18+ // Ref: https://github.com/stalomeow/captcha-solver
19+
20+ static String _getInterpreterAssetName (DigitCaptchaType type) {
21+ return 'assets/captcha-solver-${type .name .toLowerCase ()}.tflite' ;
22+ }
23+
24+ static double _lerp (double a, double b, double t) {
25+ return a + (b - a) * t;
26+ }
27+
28+ static num _sampleMin (img.Image image, List <int > bb, double u, double v) {
29+ int x = _lerp (bb[0 ] * 1.0 , bb[2 ] - 1.0 , u).floor ();
30+ int y = _lerp (bb[1 ] * 1.0 , bb[3 ] - 1.0 , v).floor ();
31+ num px = min (image.getPixelClamped (x, y + 0 ).r,
32+ image.getPixelClamped (x + 1 , y + 0 ).r);
33+ num py = min (image.getPixelClamped (x, y + 1 ).r,
34+ image.getPixelClamped (x + 1 , y + 1 ).r);
35+ return min (px, py);
36+ }
37+
38+ static List <int > _getbbox (img.Image image) {
39+ int left = image.width;
40+ int upper = image.height;
41+ int right = 0 ; // Exclusive
42+ int lower = 0 ; // Exclusive
1643
17- static Future <String > infer (List <int > imageData) async {
18- // Ref: https://github.com/stalomeow/captcha-solver
44+ for (int x = 0 ; x < image.width; x++ ) {
45+ for (int y = 0 ; y < image.height; y++ ) {
46+ num p = image.getPixel (x, y).r;
1947
48+ // Binarization
49+ if (p < 0.98 ) {
50+ continue ;
51+ }
52+
53+ left = min (left, x);
54+ upper = min (upper, y);
55+ right = max (right, x + 1 );
56+ lower = max (lower, y + 1 );
57+ }
58+ }
59+
60+ // Expand the bounding box by 1 pixel
61+ left = max (0 , left - 1 );
62+ upper = max (0 , upper - 1 );
63+ right = min (image.width, right + 1 );
64+ lower = min (image.height, lower + 1 );
65+
66+ return [left, upper, right, lower];
67+ }
68+
69+ static img.Image ? _getImage (DigitCaptchaType type, List <int > imageData) {
2070 img.Image image = img.decodeImage (Uint8List .fromList (imageData))! ;
2171 image = img.grayscale (image);
2272 image = image.convert (
2373 format: img.Format .float32, numChannels: 1 ); // 0-256 to 0-1
2474
75+ if (type == DigitCaptchaType .zfw) {
76+ // Invert the image
77+ for (int x = 0 ; x < image.width; x++ ) {
78+ for (int y = 0 ; y < image.height; y++ ) {
79+ image.setPixelR (x, y, 1.0 - image.getPixel (x, y).r);
80+ }
81+ }
82+
83+ List <int > bb = _getbbox (image);
84+
85+ // The numbers are too close
86+ if (bb[2 ] - bb[0 ] < 44 ) {
87+ return null ;
88+ }
89+
90+ // Align with the size of payment captcha
91+ img.Image result = new img.Image (
92+ width: 200 , height: 80 , format: img.Format .float32, numChannels: 1 );
93+ for (int x = 0 ; x < result.width; x++ ) {
94+ for (int y = 0 ; y < result.height; y++ ) {
95+ double u = x * 1.0 / result.width;
96+ double v = y * 1.0 / result.height;
97+ num r = _sampleMin (image, bb, u, v);
98+ result.setPixelR (x, y, r);
99+ }
100+ }
101+ image = result;
102+ }
103+
104+ return image;
105+ }
106+
107+ static int _argmax (List <double > list) {
108+ int result = 0 ;
109+ for (int i = 1 ; i < list.length; i++ ) {
110+ if (list[i] > list[result]) {
111+ result = i;
112+ }
113+ }
114+ return result;
115+ }
116+
117+ static int _getClassCount (DigitCaptchaType type) {
118+ if (type == DigitCaptchaType .payment) {
119+ return 9 ; // The payment captcha only contains number 1-9
120+ }
121+ return 10 ;
122+ }
123+
124+ static int _getClassLabel (DigitCaptchaType type, int klass) {
125+ if (type == DigitCaptchaType .payment) {
126+ return klass + 1 ; // The payment captcha only contains number 1-9
127+ }
128+ return klass;
129+ }
130+
131+ static Future <String ?> infer (
132+ DigitCaptchaType type, List <int > imageData) async {
133+ img.Image ? image = _getImage (type, imageData);
134+
135+ if (image == null ) {
136+ return null ;
137+ }
138+
25139 int dim2 = image.height;
26140 int dim3 = image.width ~ / 4 ;
141+ int classCount = _getClassCount (type);
27142
28143 var input = List .filled (dim2 * dim3, 0.0 )
29144 .reshape <double >([1 , dim2, dim3, 1 ]) as List <List <List <List <double >>>>;
30- var output =
31- List . filled ( 9 , 0.0 ). reshape < double >([ 1 , 9 ]) as List <List <double >>;
145+ var output = List . filled (classCount, 0.0 ). reshape < double >([ 1 , classCount])
146+ as List <List <double >>;
32147
33- final interpreter = await Interpreter .fromAsset (_interpreterAssetName);
148+ final interpreter =
149+ await Interpreter .fromAsset (_getInterpreterAssetName (type));
34150 List <int > nums = [];
35151
36152 // Four numbers
@@ -42,21 +158,11 @@ class DigitCaptchaClientProvider {
42158 }
43159
44160 interpreter.run (input, output);
45- nums.add (_argmax (output[0 ]) + 1 );
161+ nums.add (_getClassLabel (type, _argmax (output[0 ])) );
46162 }
47163
48164 return nums.join ('' );
49165 }
50-
51- static int _argmax (List <double > list) {
52- int result = 0 ;
53- for (int i = 1 ; i < list.length; i++ ) {
54- if (list[i] > list[result]) {
55- result = i;
56- }
57- }
58- return result;
59- }
60166}
61167
62168class CaptchaInputDialog extends StatelessWidget {
0 commit comments