Flutter二维码识别插件qr_code_vision的使用

发布于 1周前 作者 zlyuanteng 来自 Flutter

Flutter二维码识别插件qr_code_vision的使用

功能介绍

qr_code_vision 插件提供了高级和低级功能,可以从图像中提取二维码数据,包括其位置、类型和内容。此外,它还可以高效地跟踪二维码在不同帧之间的位置和内容,适用于Flutter中的AR(增强现实)应用程序。该插件是用纯Dart编写的,基于流行的jsQR JavaScript QR读取器。

使用方法

为了在图像中定位和解码二维码,您需要首先获取图像的RGBA格式字节数据。这可以通过多种方式完成,具体取决于图像的来源和格式。例如,对于一个dart:uiImage对象,您可以使用toByteData()方法。然后实例化一个新的QrCode对象并调用scanRgbaBytes(imageData, imageWidth, imageHeight)来尝试定位和解码二维码。

示例代码

以下是一个完整的示例代码,展示了如何使用qr_code_vision插件构建一个简单的AR应用,该应用可以识别二维码并显示其内容(如果二维码包含图像URL,则会显示该图像)。

/// 该示例展示了如何使用qr_code_vision Dart包来定位和解码二维码,
/// 并在二维码上方显示图像(如果二维码包含图像URL),同时保持准确的透视和尺寸。
import 'dart:async';
import 'dart:ui' as ui;

import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:qr_code_vision/qr_code_vision.dart';

final cameras = <CameraDescription>[];
late ui.Image overlayImage;

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  cameras.addAll(await availableCameras());

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'QR Vision Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  [@override](/user/override)
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late CameraController _cameraController;

  final _scannedFrameStreamController = StreamController<_ScannedFrame>();

  bool _showDebugOverlay = true;
  bool _showImageOverlay = false;
  bool _processFrameReady = true;

  // The scanned QR code
  final _qrCode = QrCode();

  [@override](/user/override)
  void initState() {
    super.initState();
    // Initialize camera stream and listen to captured frames
    _cameraController = CameraController(cameras[0], ResolutionPreset.medium);

    _cameraController.initialize().then((_) {
      if (!mounted) {
        return;
      }
      _cameraController.startImageStream(_processFrame);
      setState(() {});
    });
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    if (!_cameraController.value.isInitialized) {
      return Container();
    }
    return Scaffold(
      appBar: AppBar(
        title: const Text("QR Vision Demo"),
      ),
      body: ListView(
        children: [
          _buildPreview(),
          const ListTile(
            title: Text("将摄像头对准包含图像URL的二维码。"),
          ),
          SwitchListTile(
            value: _showDebugOverlay,
            onChanged: (value) {
              setState(() {
                _showDebugOverlay = value;
              });
            },
            title: const Text("显示调试覆盖层"),
          ),
          SwitchListTile(
            value: _showImageOverlay,
            onChanged: (value) {
              setState(() {
                _showImageOverlay = value;
              });
            },
            title: const Text("显示图像覆盖层"),
          ),
        ],
      ),
    );
  }

  Widget _buildPreview() {
    return StreamBuilder<_ScannedFrame>(
      stream: _scannedFrameStreamController.stream,
      initialData: null,
      builder: (context, snapshot) => snapshot.data != null
          ? LayoutBuilder(
              builder: (context, constraints) => ClipRect(
                child: _buildFrame(
                    snapshot.data!, constraints.maxWidth, constraints.maxWidth),
                clipBehavior: Clip.hardEdge,
              ),
            )
          : const Center(
              child: CircularProgressIndicator(),
            ),
    );
  }

  Widget _buildFrame(_ScannedFrame frame, double width, double height) {
    final scaleFactor = width / frame.image.width.toDouble();

    return Stack(
      alignment: Alignment.topLeft,
      children: [
        CustomPaint(
          painter: _CameraViewPainter(frame: frame),
          size: ui.Size(width, height),
        ),
        (_showImageOverlay && frame.qrCode != null)
            ? _buildImageOverlay(frame.qrCode!, scaleFactor)
            : Container(),
        (_showDebugOverlay && frame.qrCode != null)
            ? CustomPaint(
                painter: _DebugOverlayPainter(frame: frame),
                size: ui.Size(width, height),
              )
            : Container()
      ],
    );
  }

  Widget _buildImageOverlay(QrCode qrCode, double scaleFactor) {
    final transformMatrix =
        qrCode.location?.computePerspectiveTransform().to3DPerspectiveMatrix();

    final scaledTransformationMatrix = transformMatrix != null
        ? Matrix4.diagonal3Values(scaleFactor, scaleFactor, scaleFactor) *
            Matrix4.fromFloat64List(transformMatrix)
        : null;

    final content = qrCode.content?.text;
    final qrCodeSize = qrCode.location?.dimension.size.toDouble();

    // 检查内容是否为URL
    final url = content != null ? Uri.tryParse(content) : null;

    if (qrCodeSize != null && url != null) {
      return Transform(
        alignment: Alignment.topLeft,
        transform: scaledTransformationMatrix,
        child: Image.network(
          url.toString(),
          width: qrCodeSize,
          height: qrCodeSize,
          errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) {
            return SizedBox(
              height: qrCodeSize,
              width: qrCodeSize,
              child: Center(
                child: Icon(
                  Icons.image_not_supported,
                  size: qrCodeSize * 0.5,
                ),
              ),
            );
          },
        ),
      );
    } else {
      return Container();
    }
  }

  /// 处理捕获的帧并扫描二维码
  Future<void> _processFrame(CameraImage cameraFrame) async {
    // 如果另一帧正在处理,则跳过此帧
    if (!_processFrameReady) {
      return;
    }
    _processFrameReady = false;

    try {
      final width = cameraFrame.width;
      final height = cameraFrame.height;
      Uint8List bytes = Uint8List(cameraFrame.planes[0].bytes.length);

      if (cameraFrame.format.group == ImageFormatGroup.yuv420) {
        List<Uint8List> planes = ImageProcessingUtilities.getPlanes(cameraFrame);
        bytes = ImageProcessingUtilities.yuv420ToRgba8888(planes, width, height);
      } else if (cameraFrame.format.group == ImageFormatGroup.bgra8888) {
        bytes = cameraFrame.planes[0].bytes;
      }
      final image = await ImageProcessingUtilities.createImage(bytes, width, height, ui.PixelFormat.rgba8888);

      // 扫描图像内容以更新二维码
      _qrCode.scanRgbaBytes(bytes, width, height);

      // 发布UI更新
      _scannedFrameStreamController.add(
        _ScannedFrame(
          image: image,
          qrCode: _qrCode,
        ),
      );
    } catch (e) {
      if (kDebugMode) {
        print(e);
      }
    }

    // 允许处理另一帧
    _processFrameReady = true;
  }

  [@override](/user/override)
  void dispose() {
    _cameraController.dispose();
    _scannedFrameStreamController.close();
    super.dispose();
  }
}

/// 一个包含扫描二维码的帧
class _ScannedFrame {
  final ui.Image image;
  final QrCode? qrCode;

  _ScannedFrame({
    required this.image,
    this.qrCode,
  });
}

/// 自定义绘制器,用于显示相机帧
class _CameraViewPainter extends CustomPainter {
  _CameraViewPainter({required this.frame});

  final _ScannedFrame frame;

  [@override](/user/override)
  void paint(Canvas canvas, Size size) {
    Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.0
      ..color = Colors.red;
    canvas.scale(size.width / frame.image.width, size.width / frame.image.width);
    canvas.drawImage(frame.image, Offset.zero, Paint());
  }

  [@override](/user/override)
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

/// 自定义绘制器,用于在相机图像上显示调试覆盖层(如查找模式)
class _DebugOverlayPainter extends CustomPainter {
  _DebugOverlayPainter({required this.frame});

  final _ScannedFrame frame;

  [@override](/user/override)
  void paint(Canvas canvas, Size size) {
    final finderPatternPaint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.0
      ..color = Colors.red;
    canvas.scale(size.width / frame.image.width, size.width / frame.image.width);

    if (frame.qrCode != null && frame.qrCode!.location != null) {
      final location = frame.qrCode!.location!;

      final topLeftOffset = Offset(location.topLeft.x, location.topLeft.y);
      final bottomLeftOffset = Offset(location.bottomLeft.x, location.bottomLeft.y);
      final topRightOffset = Offset(location.topRight.x, location.topRight.y);
      final alignmentPatternOffset = Offset(location.alignmentPattern.x, location.alignmentPattern.y);

      final finderPatternSize = location.dimension.module * 7 / 2;
      final alignmentPatternSize = location.dimension.module * 5 / 2;

      canvas.drawCircle(topLeftOffset, finderPatternSize, finderPatternPaint);
      canvas.drawCircle(bottomLeftOffset, finderPatternSize, finderPatternPaint);
      canvas.drawCircle(topRightOffset, finderPatternSize, finderPatternPaint);
      canvas.drawCircle(alignmentPatternOffset, alignmentPatternSize, finderPatternPaint);

      canvas.transform(location.computePerspectiveTransform().to3DPerspectiveMatrix());
      final targetSize = location.dimension.size.toDouble();
      final textStyle = TextStyle(
        color: Colors.red,
        fontSize: targetSize * 0.1,
      );
      final textSpan = TextSpan(
        text: frame.qrCode!.content?.text,
        style: textStyle,
      );
      final textPainter = TextPainter(
        text: textSpan,
        textDirection: TextDirection.ltr,
      );
      textPainter.layout(
        minWidth: 0,
        maxWidth: targetSize,
      );

      canvas.drawRect(
        Rect.fromLTWH(0, 0, targetSize, targetSize),
        Paint()
          ..style = ui.PaintingStyle.stroke
          ..strokeWidth = 1.0
          ..color = Colors.red,
      );

      textPainter.paint(canvas, Offset(0, targetSize * 1.1));
    }
  }

  [@override](/user/override)
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

class ImageProcessingUtilities {
  static List<Uint8List> getPlanes(CameraImage cameraFrame) {
    List<Uint8List> planes = [];
    for (int planeIndex = 0; planeIndex < 3; planeIndex++) {
      Uint8List buffer;
      int width;
      int height;
      if (planeIndex == 0) {
        width = cameraFrame.width;
        height = cameraFrame.height;
      } else {
        width = cameraFrame.width ~/ 2;
        height = cameraFrame.height ~/ 2;
      }

      buffer = Uint8List(width * height);

      int pixelStride = cameraFrame.planes[planeIndex].bytesPerPixel!;
      int rowStride = cameraFrame.planes[planeIndex].bytesPerRow;
      int index = 0;
      for (int i = 0; i < height; i++) {
        for (int j = 0; j < width; j++) {
          buffer[index++] = cameraFrame
              .planes[planeIndex].bytes[i * rowStride + j * pixelStride];
        }
      }

      planes.add(buffer);
    }
    return planes;
  }

  static Uint8List yuv420ToRgba8888(List<Uint8List> planes, int width, int height) {
    final yPlane = planes[0];
    final uPlane = planes[1];
    final vPlane = planes[2];

    final Uint8List rgbaBytes = Uint8List(width * height * 4);

    for (int y = 0; y < height; y++) {
      for (int x = 0; x < width; x++) {
        final int yIndex = y * width + x;
        final int uvIndex = (y ~/ 2) * (width ~/ 2) + (x ~/ 2);

        final int yValue = yPlane[yIndex] & 0xFF;
        final int uValue = uPlane[uvIndex] & 0xFF;
        final int vValue = vPlane[uvIndex] & 0xFF;

        final int r = (yValue + 1.13983 * (vValue - 128)).round().clamp(0, 255);
        final int g = (yValue - 0.39465 * (uValue - 128) - 0.58060 * (vValue - 128)).round().clamp(0, 255);
        final int b = (yValue + 2.03211 * (uValue - 128)).round().clamp(0, 255);

        final int rgbaIndex = yIndex * 4;
        rgbaBytes[rgbaIndex] = r.toUnsigned(8);
        rgbaBytes[rgbaIndex + 1] = g.toUnsigned(8);
        rgbaBytes[rgbaIndex + 2] = b.toUnsigned(8);
        rgbaBytes[rgbaIndex + 3] = 255; // Alpha值
      }
    }

    return rgbaBytes;
  }

  static Future<ui.Image> createImage(Uint8List buffer, int width, int height, ui.PixelFormat pixelFormat) {
    final Completer<ui.Image> completer = Completer();

    ui.decodeImageFromPixels(buffer, width, height, pixelFormat, (ui.Image img) {
      completer.complete(img);
    });

    return completer.future;
  }
}

更多关于Flutter二维码识别插件qr_code_vision的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter二维码识别插件qr_code_vision的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是如何在Flutter项目中使用qr_code_vision插件进行二维码识别的代码示例。qr_code_vision是一个基于Google ML Kit的二维码识别插件,适用于Android和iOS平台。

1. 添加依赖

首先,在你的pubspec.yaml文件中添加qr_code_vision依赖:

dependencies:
  flutter:
    sdk: flutter
  qr_code_vision: ^0.4.0  # 请注意版本号,这里使用的是0.4.0,实际使用时请检查最新版本

2. 导入插件

在你的Dart文件中导入插件:

import 'package:qr_code_vision/qr_code_vision.dart';
import 'package:camera/camera.dart';

3. 请求相机权限

AndroidManifest.xmlInfo.plist中请求相机权限。这部分配置通常在插件安装时会自动处理,但如果你遇到问题,可以手动检查并添加相关权限。

4. 实现二维码扫描页面

下面是一个完整的二维码扫描页面示例:

import 'package:flutter/material.dart';
import 'package:qr_code_vision/qr_code_vision.dart';
import 'package:camera/camera.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ScanPage(),
    );
  }
}

class ScanPage extends StatefulWidget {
  @override
  _ScanPageState createState() => _ScanPageState();
}

class _ScanPageState extends State<ScanPage> {
  CameraController? controller;
  final BarcodeDetector _barcodeDetector = BarcodeDetector();
  String? resultText = "";

  @override
  void initState() {
    super.initState();
    _initCamera();
  }

  Future<void> _initCamera() async {
    // 获取可用的相机列表
    final cameras = await availableCameras();

    // 使用第一个相机
    final firstCamera = cameras.first;
    controller = CameraController(firstCamera, ResolutionPreset.medium);

    // 当相机初始化完成后开始预览
    controller!.initialize().then((_) {
      if (!mounted) {
        return;
      }
      setState(() {});
    });

    // 监听图像帧以进行二维码检测
    controller!.startImageStream((image) async {
      final result = await _barcodeDetector.processImage(image);
      if (mounted && result.codes.isNotEmpty) {
        setState(() {
          resultText = result.codes.first.value!;
        });

        // 停止相机预览并导航到结果页面(这里可以自定义)
        Navigator.of(context).push(MaterialPageRoute(
          builder: (context) => ResultPage(resultText: resultText!),
        ));

        controller!.dispose();
      }
    });
  }

  @override
  void dispose() {
    controller?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (controller == null || !controller!.value.isInitialized) {
      return Container();
    }

    return Scaffold(
      appBar: AppBar(
        title: Text('二维码扫描'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Expanded(
              child: CameraPreview(controller!),
            ),
            if (resultText != null)
              Text(
                '扫描结果: $resultText',
                style: TextStyle(fontSize: 24),
              ),
          ],
        ),
      ),
    );
  }
}

class ResultPage extends StatelessWidget {
  final String resultText;

  ResultPage({required this.resultText});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('扫描结果'),
      ),
      body: Center(
        child: Text(
          '扫描结果: $resultText',
          style: TextStyle(fontSize: 24),
        ),
      ),
    );
  }
}

5. 运行应用

确保你已经连接了物理设备或启动了模拟器,然后运行Flutter应用:

flutter run

这个示例展示了如何使用qr_code_vision插件和camera插件在Flutter应用中实现二维码扫描功能。如果你遇到任何问题,请确保你使用的插件版本与示例代码中的版本兼容,并检查官方文档以获取最新信息和可能的更新。

回到顶部