Flutter二维码识别插件qr_code_vision的使用
Flutter二维码识别插件qr_code_vision的使用
功能介绍
qr_code_vision
插件提供了高级和低级功能,可以从图像中提取二维码数据,包括其位置、类型和内容。此外,它还可以高效地跟踪二维码在不同帧之间的位置和内容,适用于Flutter中的AR(增强现实)应用程序。该插件是用纯Dart编写的,基于流行的jsQR JavaScript QR读取器。
使用方法
为了在图像中定位和解码二维码,您需要首先获取图像的RGBA格式字节数据。这可以通过多种方式完成,具体取决于图像的来源和格式。例如,对于一个dart:ui
的Image
对象,您可以使用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
更多关于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.xml
和Info.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应用中实现二维码扫描功能。如果你遇到任何问题,请确保你使用的插件版本与示例代码中的版本兼容,并检查官方文档以获取最新信息和可能的更新。