creek

The AI Image Editor of 2030

lib/services/analyze/font.dart (6950B)


  1import 'dart:io';
  2import 'dart:math';
  3import 'dart:convert';
  4import 'package:flutter/foundation.dart';
  5import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
  6import 'package:onnxruntime/onnxruntime.dart';
  7import 'package:image/image.dart' as img;
  8
  9class FontIdentifierService {
 10  OrtSession? _session;
 11  Map<String, List<double>>? _fontDb;
 12  final _textRecognizer = TextRecognizer(script: TextRecognitionScript.latin);
 13
 14  static const int IMG_SIZE = 64;
 15
 16  Future<void> initialize({
 17    required String modelPath,
 18    required String jsonPath,
 19  }) async {
 20    if (_session != null) return;
 21
 22    try {
 23      debugPrint("Initializing Font Service...");
 24      OrtEnv.instance.init();
 25
 26      // 1. Load ONNX
 27      final sessionOptions = OrtSessionOptions();
 28      _session = OrtSession.fromFile(File(modelPath), sessionOptions);
 29      sessionOptions.release();
 30
 31      // 2. Load JSON DB
 32      final jsonStr = await File(jsonPath).readAsString();
 33      final Map<String, dynamic> rawDb = json.decode(jsonStr);
 34      _fontDb = {};
 35      rawDb.forEach((key, value) {
 36        _fontDb![key] = List<double>.from(value);
 37      });
 38    } catch (e) {
 39      debugPrint("Font Service Init Error: $e");
 40    }
 41  }
 42
 43  Future<Map<String, dynamic>> analyze(
 44    String imagePath, {
 45    String? modelPath,
 46    String? jsonPath,
 47  }) async {
 48    if (modelPath == null || jsonPath == null) {
 49      return {'success': false, 'scores': {}, 'error': 'Paths missing'};
 50    }
 51    await initialize(modelPath: modelPath, jsonPath: jsonPath);
 52    if (_session == null || _fontDb == null) {
 53      return {'success': false, 'scores': {}, 'error': 'Init failed'};
 54    }
 55
 56    try {
 57      // 1. OCR Detection (Native Platform Call)
 58      final inputImage = InputImage.fromFilePath(imagePath);
 59      final recognizedText = await _textRecognizer.processImage(inputImage);
 60
 61      if (recognizedText.blocks.isEmpty) {
 62        return {
 63          'success': true,
 64          'scores': {'No Text Detected': 1.0},
 65          'error': null,
 66        };
 67      }
 68
 69      // 2. Load Image for Processing
 70      final bytes = await File(imagePath).readAsBytes();
 71      final fullImage = img.decodeImage(bytes);
 72      if (fullImage == null) {
 73        return {'success': false, 'scores': {}, 'error': 'Image decode failed'};
 74      }
 75
 76      // Store counts to find dominant font
 77      Map<String, double> fontCounts = {};
 78      int totalBlocks = 0;
 79
 80      // 3. Iterate Text Blocks
 81      for (TextBlock block in recognizedText.blocks) {
 82        for (TextLine line in block.lines) {
 83          // Crop
 84          final rect = line.boundingBox;
 85
 86          // Safety check for bounds
 87          int x = max(0, rect.left.toInt());
 88          int y = max(0, rect.top.toInt());
 89          int w = min(rect.width.toInt(), fullImage.width - x);
 90          int h = min(rect.height.toInt(), fullImage.height - y);
 91
 92          if (w <= 0 || h <= 0) continue;
 93
 94          final crop = img.copyCrop(fullImage, x: x, y: y, width: w, height: h);
 95
 96          // Preprocess & Inference
 97          final floatInput = _preprocessImage(crop);
 98          final embedding = _runInference(floatInput);
 99          final match = _findBestFont(embedding);
100
101          // Aggregate Scores
102          final fontName = match.key;
103          final conf = match.value;
104
105          if (fontCounts.containsKey(fontName)) {
106            fontCounts[fontName] = fontCounts[fontName]! + conf;
107          } else {
108            fontCounts[fontName] = conf;
109          }
110          totalBlocks++;
111        }
112      }
113
114      // Normalize scores
115      fontCounts.updateAll((key, value) => (value / totalBlocks) * 100);
116
117      // Sort
118      var sorted =
119          fontCounts.entries.toList()
120            ..sort((a, b) => b.value.compareTo(a.value));
121      Map<String, dynamic> finalScores = {};
122      for (var entry in sorted) {
123        finalScores[entry.key] = entry.value;
124      }
125
126      return {'success': true, 'scores': finalScores, 'error': null};
127    } catch (e) {
128      debugPrint("Font Analysis Failed: $e");
129      return {'success': false, 'scores': {}, 'error': e.toString()};
130    }
131  }
132
133  /// Convert crop to 64x64 Grayscale Thresholded Float32List
134  Float32List _preprocessImage(img.Image crop) {
135    // Resize to 64x64
136    final resized = img.copyResize(crop, width: IMG_SIZE, height: IMG_SIZE);
137    final pixels = Float32List(IMG_SIZE * IMG_SIZE);
138
139    // 1. Calculate Mean Brightness (Adaptive Threshold)
140    double totalLum = 0;
141    for (final pixel in resized) {
142      totalLum += pixel.r * 0.299 + pixel.g * 0.587 + pixel.b * 0.114;
143    }
144    double mean = totalLum / (IMG_SIZE * IMG_SIZE);
145
146    // 2. Binarize (Inverted: Text=1.0, BG=0.0)
147    int idx = 0;
148    for (int y = 0; y < IMG_SIZE; y++) {
149      for (int x = 0; x < IMG_SIZE; x++) {
150        final p = resized.getPixel(x, y);
151        final lum = p.r * 0.299 + p.g * 0.587 + p.b * 0.114;
152        // If pixel is darker than mean, it's text (1.0)
153        pixels[idx++] = (lum < mean) ? 1.0 : 0.0;
154      }
155    }
156    return pixels;
157  }
158
159  List<double> _runInference(Float32List imageFloats) {
160    OrtValueTensor? imgTensor;
161    OrtValueTensor? charTensor;
162    OrtRunOptions? runOptions;
163    List<OrtValue?>? outputs;
164
165    try {
166      // Input 1: Image [1, 64, 64, 1]
167      imgTensor = OrtValueTensor.createTensorWithDataList(imageFloats, [
168        1,
169        64,
170        64,
171        1,
172      ]);
173
174      // Input 2: Dummy Char [1, 26, 1] (Required by FANNet architecture)
175      final dummyFloats = Float32List(26 * 1);
176      charTensor = OrtValueTensor.createTensorWithDataList(dummyFloats, [
177        1,
178        26,
179        1,
180      ]);
181
182      runOptions = OrtRunOptions();
183
184      // Run
185      outputs = _session!.run(runOptions, {
186        'image_input': imgTensor,
187        'char_input': charTensor,
188      });
189
190      // Output Flattening
191      final rawOutput = outputs[0]?.value;
192      final List<double> flatOutput = [];
193
194      void flatten(dynamic d) {
195        if (d is num) {
196          flatOutput.add(d.toDouble());
197        } else if (d is List) {
198          for (var i in d) {
199            flatten(i);
200          }
201        }
202      }
203
204      flatten(rawOutput);
205
206      return flatOutput;
207    } finally {
208      imgTensor?.release();
209      charTensor?.release();
210      runOptions?.release();
211      outputs?.forEach((e) => e?.release());
212    }
213  }
214
215  MapEntry<String, double> _findBestFont(List<double> embedding) {
216    String bestFont = "Unknown";
217    double minDist = double.infinity;
218
219    _fontDb!.forEach((fontName, fontFeat) {
220      double sum = 0;
221      for (int i = 0; i < min(embedding.length, fontFeat.length); i++) {
222        double diff = embedding[i] - fontFeat[i];
223        sum += diff * diff;
224      }
225      double dist = sqrt(sum);
226
227      if (dist < minDist) {
228        minDist = dist;
229        bestFont = fontName;
230      }
231    });
232
233    return MapEntry(bestFont, 1 / (1 + minDist));
234  }
235
236  void dispose() {
237    _session?.release();
238    _textRecognizer.close();
239  }
240}