I am working on a flutter app that needs to read digital odometer state from an image. The goal is to have an image of the odometer, crop it around the number as close as possible and then do text recognition on it.

For text recognition I am using

google_mlkit_text_recognition

but I am having problems with digital numbers, example:
image cropping screen

For this image I don't get any numbers out, I have a felling that the problem is the spacings inside the numbers between the lines. Has anyone done this or has any suggestions how to do it.

The code that worked best for me:

Future<File?> _pickerImage({required ImageSource source}) async {
    final ImagePicker picker = ImagePicker();
    final XFile? image = await picker.pickImage(source: source);
    if (image != null) {
      return File(image.path);
    }
    return null;
  }

Future<CroppedFile?> _cropImage({required File imageFile}) async {
    return ImageCropper().cropImage(
      sourcePath: imageFile.path,
      aspectRatio: const CropAspectRatio(ratioX: 5, ratioY: 1),
      uiSettings: [
        AndroidUiSettings(
          lockAspectRatio: true,
          hideBottomControls: false,
          toolbarTitle: 'Crop odometer',
        ),
        IOSUiSettings(
          minimumAspectRatio: 1.0,
          title: 'Crop odometer',
          aspectRatioLockEnabled: true,
        ),
      ],
    );
  }


  Future<String?> _extractOdometer(String fullText) async {
    var normalized = fullText.replaceAll(' ', '');
    normalized = normalized.replaceAll('\n', ' ');
    normalized = normalized.replaceAll('T', '1');
    normalized = normalized.replaceAll('I', '1');
    normalized = normalized.replaceAll('l', '1');
    normalized = normalized.replaceAll('O', '0');
    normalized = normalized.replaceAll('S', '5');

    final regex = RegExp(r'\b\d{4,7}\b');
    final matches = regex.allMatches(normalized).toList();
    if (matches.isEmpty) return null;

    matches.sort((a, b) => b.group(0)!.length.compareTo(a.group(0)!.length));
    return matches.first.group(0);
  }
  Future<String> _prepDigitsForOcr(String path) async {
    final file = File(path);
    final bytes = await file.readAsBytes();
    final original = img.decodeImage(bytes);
    if (original == null) return path;

    var processed = original;

    processed = img.grayscale(processed);

    processed = img.adjustColor(
      processed,
      contrast: 1.4,
    );


    final outBytes = img.encodeJpg(processed, quality: 100);
    final dir = await getTemporaryDirectory();
    final outFile = File(
      '${dir.path}/odo_pre_${DateTime.now().millisecondsSinceEpoch}.jpg',
    );
    await outFile.writeAsBytes(outBytes);
    return outFile.path;
  }


  Future<String?> _recognizeOdometerFromImage(String imgPath) async {
    // final bandPath = await _cropToDigitBand(imgPath);
    final preppedPath = await _prepDigitsForOcr(imgPath);

    final image = InputImage.fromFilePath(preppedPath);
    final recognized = await textRecognizer.processImage(image);

    debugPrint('*** MLKit recognized (prepped): "${recognized.text}"');

    final odometer = await _extractOdometer(recognized.text);
    return odometer;
  }


  Future<void> _processImageExtractText({
    required ImageSource imageSource,
  }) async {
    final imageFile = await _pickerImage(source: imageSource);

    if (imageFile == null) return;

    final croppedImage = await _cropImage(
      imageFile: imageFile,
    );

    if (croppedImage == null) return;

    final odometer = await _recognizeOdometerFromImage(croppedImage.path);
    if (!mounted) return;

    setState(() => _extractedText = odometer ?? '');
    if (odometer == null) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('No valid odometer value found')),
      );
    }
  }