shirne / zxing-dart

zxing-dart is a Dart port of zxing.
Apache License 2.0
54 stars 10 forks source link

无二维码/条形码时,实时识别效率特别低 #5

Open droplet-js opened 1 year ago

droplet-js commented 1 year ago

Is your feature request related to a problem? Please describe. 如题,我对比了 flutter_zxing(ffi)和 zxing_lib(dart)

  1. 在有二维码/条形时,二者实时识别效率相差不大,甚至 zxing_lib 速度更快(0.006-0.02秒左右),表现更优
  2. 在无二维码/条形时,flutter_zxing 的识别效率变化不大,而 zxing_lib 速度特别慢(1-3秒左右),这是完全无法接受的

Describe the solution you'd like 优化 zxing_lib 在无二维码/条形时的实时识别效率

Describe alternatives you've considered A clear and concise description of any alternative solutions or features you've considered.

Additional context Add any other context or screenshots about the feature request here.

droplet-js commented 1 year ago

示例代码

import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'dart:math' as math;

import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_zxing/flutter_zxing.dart' as fzx;
import 'package:image/image.dart' as imglib;
import 'package:tuple/tuple.dart';
import 'package:zxing_lib/common.dart';
import 'package:zxing_lib/zxing.dart';

enum ZXFormat {
  None(fzx.Format.None),
  Aztec(fzx.Format.Aztec),
  Codabar(fzx.Format.Codabar),
  Code39(fzx.Format.Code39),
  Code93(fzx.Format.Code93),
  Code128(fzx.Format.Code128),
  DataBar(fzx.Format.DataBar),
  DataBarExpanded(fzx.Format.DataBarExpanded),
  DataMatrix(fzx.Format.DataMatrix),
  EAN8(fzx.Format.EAN8),
  EAN13(fzx.Format.EAN13),
  ITF(fzx.Format.ITF),
  MaxiCode(fzx.Format.MaxiCode),
  PDF417(fzx.Format.PDF417),
  QRCode(fzx.Format.QRCode),
  UPCA(fzx.Format.UPCA),
  UPCE(fzx.Format.UPCE),
  OneDCodes(fzx.Format.OneDCodes),
  TwoDCodes(fzx.Format.TwoDCodes),
  Any(fzx.Format.Any);

  const ZXFormat(this.value);

  final int value;
}

enum ZXECCLevel {
  M(0),
  L(1),
  H(2),
  Q(3);

  const ZXECCLevel(this.value);

  final int value;
}

class ZXDResult {
  ZXDResult.from(
    fzx.CodeResult result,
  )   : isValid = result.isValidBool,
        format = ZXFormat.values.singleWhere((ZXFormat element) => element.value == result.format),
        text = result.textString;

  ZXDResult.fromDart(
    Result result,
  )   : isValid = true,
        format = ZXFormat.Any,
        text = result.text;

  final bool isValid;
  final ZXFormat format;
  final String? text;
}

class ZXEResult {
  ZXEResult.from({
    required fzx.EncodeResult result,
  })  : isValid = result.isValidBool,
        format = ZXFormat.values.singleWhere((ZXFormat element) => element.value == result.format),
        text = result.textString,
        bytes = Uint8List.view(result.bytes.buffer),
        errorMessage = result.errorMessage;

  final bool isValid;
  final ZXFormat format;
  final String? text;
  final Uint8List bytes;
  final String errorMessage;
}

class ZXingKit {
  const ZXingKit._();

  //

  static Future<ZXDResult?> decodeImage(
    String path, {
    ZXFormat format = ZXFormat.Any,
    double? cropPercent,
  }) async {
    return compute(_computeDecodeImage, Tuple3<String, ZXFormat, double?>(path, format, cropPercent));
  }

  static Future<ZXDResult?> _computeDecodeImage(Tuple3<String, ZXFormat, double?> tuple) async {
    final String path = tuple.item1;
    final ZXFormat format = tuple.item2;
    final double? cropPercent = tuple.item3;
    int cropSize = 0;
    if (cropPercent != null) {
      final imglib.Image? image = imglib.decodeImage(await File(path).readAsBytes());
      if (image != null) {
        cropSize = (math.min(image.width, image.height) * cropPercent).round();
      }
    }
    final fzx.CodeResult? result = await fzx.readBarcodeImagePathString(
      path,
      format: format.value,
      cropWidth: cropSize,
      cropHeight: cropSize,
    );
    return result != null ? ZXDResult.from(result) : null;
  }

  //
  static Future<ZXEResult> encodeImage(
    String content, {
    ZXFormat format = ZXFormat.QRCode,
    int size = 256,
    int margin = 0,
    ZXECCLevel eccLevel = ZXECCLevel.M,
  }) async {
    return compute(_computeEncodeImage, Tuple5<String, ZXFormat, int, int, ZXECCLevel>(content, format, size, margin, eccLevel));
  }

  static Future<ZXEResult> _computeEncodeImage(Tuple5<String, ZXFormat, int, int, ZXECCLevel> tuple) async {
    final String content = tuple.item1;
    final ZXFormat format = tuple.item2;
    final int size = tuple.item3;
    final int margin = tuple.item4;
    final ZXECCLevel eccLevel = tuple.item5;
    final fzx.EncodeResult result = fzx.encodeBarcode(
      content,
      format: format.value,
      width: size,
      height: size,
      margin: margin,
      eccLevel: eccLevel.value,
    );
    return ZXEResult.from(result: result);
  }
}

// isolates.compute 效率太低了,不适合实时检测结果
class ZXingWorker {
  Isolate? _isolate;
  SendPort? _workerSendPort;

  bool get isRunning => _workerSendPort != null;

  Future<void> start() async {
    final ReceivePort initialize = ReceivePort();
    _isolate = await Isolate.spawn<SendPort>(
      (SendPort initializeSendPort) async {
        final ReceivePort worker = ReceivePort();
        initializeSendPort.send(worker.sendPort);
        // 接收 _workerPort 发送的消息
        await for (dynamic message in worker) {
          if (message is _IsolateData) {
            final CameraImage image = message.image;
            final ZXFormat format = message.format;
            final double cropPercent = message.cropPercent;
            final SendPort callbackSendPort = message.callbackSendPort;
            final Stopwatch stopwatch = Stopwatch()..start();
            try {
              final ZXDResult result = await _inferenceByZXingDart(image, format, cropPercent);
              callbackSendPort.send(result);
            } catch (e) {
              callbackSendPort.send(e);
            }
            stopwatch.stop();
            if (kDebugMode) {
              print('total elapsed: ${stopwatch.elapsed}');
            }
          }
        }
      },
      initialize.sendPort,
      debugName: 'zxing_worker',
    );
    _workerSendPort = await initialize.first as SendPort;
  }

  // zxing dart 无码时很慢 -> 1 ~ 3 秒,有码时甚至比 ffi 还快
  Future<ZXDResult> _inferenceByZXingDart(CameraImage image, ZXFormat format, double cropPercent) async {
    final int cropSize = (math.min(image.width, image.height) * cropPercent).round();
    if (Platform.isAndroid && image.format.group == ImageFormatGroup.yuv420) {
      final WriteBuffer allBytes = WriteBuffer();
      for (final Plane plane in image.planes) {
        allBytes.putUint8List(plane.bytes);
      }
      final Uint8List yuvData = allBytes.done().buffer.asUint8List();
      final LuminanceSource imageSource = PlanarYUVLuminanceSource(yuvData, image.width, image.height);
      final BinaryBitmap bitmap = BinaryBitmap(HybridBinarizer(imageSource));
      final Reader reader = MultiFormatReader();
      final Result result = reader.decode(bitmap.crop((bitmap.width - cropSize) ~/ 2, (bitmap.height - cropSize) ~/ 2, cropSize, cropSize), <DecodeHintType, Object>{
        DecodeHintType.TRY_HARDER: false,
        DecodeHintType.ALSO_INVERTED: false,
      });
      return ZXDResult.fromDart(result);
    }
    throw UnsupportedError('Unsupport format(${image.format.group})');
  }

  // zxing ffi 无码时也很快
  Future<ZXDResult> _inferenceByZXingFFI(CameraImage image, ZXFormat format, double cropPercent) async {
    final Uint8List bytes = await _convertImage(image);
    final int cropSize = (math.min(image.width, image.height) * cropPercent).round();
    final fzx.CodeResult result = fzx.readBarcode(
      bytes,
      width: image.width,
      height: image.height,
      format: format.value,
      cropWidth: cropSize,
      cropHeight: cropSize,
      tryHarder: false,
      tryRotate: false,
    );
    return ZXDResult.from(result);
  }

  Future<Uint8List> _convertImage(CameraImage image) async {
    if (Platform.isAndroid && image.format.group == ImageFormatGroup.yuv420) {
      return image.planes.first.bytes;
    } else if (image.format.group == ImageFormatGroup.bgra8888) {
      final imglib.Image img = imglib.Image.fromBytes(
        image.width,
        image.height,
        image.planes[0].bytes,
        format: imglib.Format.bgra,
      );
      return img.getBytes(format: imglib.Format.luminance);
    }
    throw UnsupportedError('Unsupport format(${image.format.group})');
  }

  Future<ZXDResult> decodeImage(
    CameraImage image, {
    ZXFormat format = ZXFormat.Any,
    double cropPercent = 0.5,
  }) async {
    assert(_workerSendPort != null);
    final ReceivePort callback = ReceivePort();
    _workerSendPort!.send(_IsolateData(image, format, cropPercent, callback.sendPort));
    final dynamic result = await callback.first;
    if (result is ZXDResult) {
      return result;
    } else {
      // ignore: only_throw_errors
      throw result as Object;
    }
  }

  Future<void> stop() async {
    _isolate?.kill(priority: Isolate.immediate);
    _isolate = null;
    _workerSendPort = null;
  }
}

class _IsolateData {
  _IsolateData(
    this.image,
    this.format,
    this.cropPercent,
    this.callbackSendPort,
  );

  final CameraImage image;
  final ZXFormat format;
  final double cropPercent;
  final SendPort callbackSendPort;
}
shirne commented 1 year ago

dart慢是正常的。目前我还没有明确的优化方向。

我试下代码看看情况吧

droplet-js commented 1 year ago

dart慢是正常的。目前我还没有明确的优化方向。

我试下代码看看情况吧

  static Future<Result> _inferenceByZXingDart(CameraImage image, double cropPercent) async {
    final int cropSize = (math.min(image.width, image.height) * cropPercent).round();
    late final LuminanceSource imageSource;
    if (Platform.isAndroid && image.format.group == ImageFormatGroup.yuv420) {
      final WriteBuffer allBytes = WriteBuffer();
      for (final Plane plane in image.planes) {
        allBytes.putUint8List(plane.bytes);
      }
      final Uint8List yuvData = allBytes.done().buffer.asUint8List();
      imageSource = PlanarYUVLuminanceSource(yuvData, image.width, image.height);
    } else if (image.format.group == ImageFormatGroup.jpeg) {
      final imglib.Image img = imglib.decodeJpg(image.planes.first.bytes)!;// jpeg: rgb/bgr
      final Uint8List rgbData = img.getBytes(format: imglib.Format.luminance);
      imageSource = RGBLuminanceSource(image.width, image.height, rgbData);
    } else if (image.format.group == ImageFormatGroup.bgra8888) {
      final imglib.Image img = imglib.Image.fromBytes(
        image.width,
        image.height,
        image.planes.first.bytes,
        format: imglib.Format.bgra,
      );
      final Uint8List rgbData = img.getBytes(format: imglib.Format.luminance);
      imageSource = RGBLuminanceSource(image.width, image.height, rgbData);
    } else {
      throw UnsupportedError('Unsupport format(${image.format.group})');
    }
    final BinaryBitmap bitmap = BinaryBitmap(HybridBinarizer(imageSource));
    final Reader reader = MultiFormatReader();
    final Result result = reader.decode(bitmap.crop((bitmap.width - cropSize) ~/ 2, (bitmap.height - cropSize) ~/ 2, cropSize, cropSize), <DecodeHintType, Object>{
      DecodeHintType.TRY_HARDER: false,
      DecodeHintType.ALSO_INVERTED: false,
    });
    return result;
  }

iOS 上倒是不慢,就 Android 上慢