Flutter中如何根据四边形对图像进行畸变处理?
实现Flutter图像透视畸变(四边形转矩形)
要实现将用户标记的四边形图像区域映射到画布四角的畸变效果,核心是透视变换(单应性变换),可以通过CustomPainter结合ImageShader和单应矩阵来完成,具体步骤如下:
1. 核心原理:单应矩阵计算
透视变换需要一个8自由度的单应矩阵,它能将任意四边形的四个顶点精准映射到目标矩形的四个角。你可以选择手动实现计算逻辑,或者借助轻量矩阵库简化开发。
方式一:手动实现高斯消元求解单应矩阵
如果不想引入第三方库,可用以下代码计算单应矩阵:
import 'dart:math'; Matrix4 computeHomography(List<Offset> srcPoints, List<Offset> dstPoints) { assert(srcPoints.length == 4 && dstPoints.length == 4); // 构造线性方程组的系数矩阵A和结果向量b final List<double> A = []; final List<double> b = []; for (int i = 0; i < 4; i++) { final x = srcPoints[i].dx; final y = srcPoints[i].dy; final u = dstPoints[i].dx; final v = dstPoints[i].dy; // 对应u的方程 A.addAll([x, y, 1, 0, 0, 0, -u * x, -u * y]); b.add(u); // 对应v的方程 A.addAll([0, 0, 0, x, y, 1, -v * x, -v * y]); b.add(v); } // 高斯消元求解线性方程组(简化实现) final matrix = List.generate(8, (i) => A.sublist(i * 8, (i + 1) * 8)..add(b[i])); // 前向消元 for (int col = 0; col < 8; col++) { // 找主元行 int pivotRow = col; for (int row = col; row < 8; row++) { if (matrix[row][col].abs() > matrix[pivotRow][col].abs()) { pivotRow = row; } } // 交换主元行与当前行 final temp = matrix[col]; matrix[col] = matrix[pivotRow]; matrix[pivotRow] = temp; // 归一化主元行 final pivotVal = matrix[col][col]; for (int j = col; j < 9; j++) { matrix[col][j] /= pivotVal; } // 消去其他行的当前列 for (int row = 0; row < 8; row++) { if (row != col && matrix[row][col] != 0) { final factor = matrix[row][col]; for (int j = col; j < 9; j++) { matrix[row][j] -= factor * matrix[col][j]; } } } } // 提取单应矩阵元素 final h0 = matrix[0][8]; final h1 = matrix[1][8]; final h2 = matrix[2][8]; final h3 = matrix[3][8]; final h4 = matrix[4][8]; final h5 = matrix[5][8]; final h6 = matrix[6][8]; final h7 = matrix[7][8]; // 构造齐次单应矩阵(Flutter的Matrix4是列优先) return Matrix4( h0, h3, 0, h6, h1, h4, 0, h7, 0, 0, 1, 0, h2, h5, 0, 1, ); }
方式二:借助vector_math库简化计算
添加依赖到pubspec.yaml:
dependencies: vector_math: ^2.1.4
然后用矩阵逆运算求解:
import 'package:vector_math/vector_math.dart'; Matrix4 computeHomography(List<Offset> src, List<Offset> dst) { final List<double> A = []; final List<double> b = []; for (int i = 0; i < 4; i++) { final x = src[i].dx; final y = src[i].dy; final u = dst[i].dx; final v = dst[i].dy; A.addAll([x, y, 1, 0, 0, 0, -u*x, -u*y]); b.add(u); A.addAll([0, 0, 0, x, y, 1, -v*x, -v*y]); b.add(v); } final Matrix M = Matrix.fromList(8, 8, A); final Matrix B = Matrix.fromList(8, 1, b); final Matrix X = M.inverse() * B; final h = X.column(0); return Matrix4( h[0], h[3], 0, h[6], h[1], h[4], 0, h[7], 0, 0, 1, 0, h[2], h[5], 0, 1, ); }
2. 实现可交互的畸变绘制Widget
创建CustomPainter应用单应变换,同时添加交互让用户拖动标记点:
import 'dart:ui' as ui; import 'package:flutter/material.dart'; class WarpImagePainter extends CustomPainter { final ui.Image image; final List<Offset> srcPoints; final Size targetSize; WarpImagePainter({ required this.image, required this.srcPoints, required this.targetSize, }); @override void paint(Canvas canvas, Size size) { // 目标点:画布的四个角 final dstPoints = [ Offset(0, 0), Offset(targetSize.width, 0), Offset(targetSize.width, targetSize.height), Offset(0, targetSize.height), ]; // 计算单应矩阵 final homography = computeHomography(srcPoints, dstPoints); // 创建带变换的图像Shader final shader = ImageShader( image, TileMode.clamp, TileMode.clamp, homography.storage, ); // 绘制变换后的图像 canvas.drawRect( Rect.fromLTWH(0, 0, targetSize.width, targetSize.height), Paint()..shader = shader, ); // 绘制用户标记的四个点(可选,方便交互调试) final pointPaint = Paint() ..color = Colors.red ..strokeWidth = 4 ..style = PaintingStyle.fill; for (final point in srcPoints) { canvas.drawCircle(point, 6, pointPaint); } } @override bool shouldRepaint(WarpImagePainter oldDelegate) { return image != oldDelegate.image || srcPoints != oldDelegate.srcPoints; } } // 可交互的图像畸变Widget class WarpImageWidget extends StatefulWidget { final File imageFile; const WarpImageWidget({super.key, required this.imageFile}); @override State<WarpImageWidget> createState() => _WarpImageWidgetState(); } class _WarpImageWidgetState extends State<WarpImageWidget> { ui.Image? _image; Size _imageSize = Size.zero; List<Offset> _srcPoints = []; int? _draggingPointIndex; @override void initState() { super.initState(); _loadImage(); } Future<void> _loadImage() async { final bytes = await widget.imageFile.readAsBytes(); final image = await decodeImageFromList(bytes); setState(() { _image = image; _imageSize = Size(image.width.toDouble(), image.height.toDouble()); // 初始化四个点为图像的默认四边形(可根据需求调整) _srcPoints = [ Offset(50, 50), Offset(_imageSize.width - 50, 50), Offset(_imageSize.width - 50, _imageSize.height - 50), Offset(50, _imageSize.height - 50), ]; }); } // 判断触摸点是否接近某个标记点 int? _getNearbyPoint(Offset position) { for (int i = 0; i < _srcPoints.length; i++) { if ((position - _srcPoints[i]).distance < 20) { return i; } } return null; } @override Widget build(BuildContext context) { if (_image == null) { return const Center(child: CircularProgressIndicator()); } final targetSize = MediaQuery.of(context).size; return GestureDetector( onPanStart: (details) { setState(() { _draggingPointIndex = _getNearbyPoint(details.localPosition); }); }, onPanUpdate: (details) { if (_draggingPointIndex != null) { setState(() { _srcPoints[_draggingPointIndex!] = details.localPosition; }); } }, onPanEnd: (_) { setState(() { _draggingPointIndex = null; }); }, child: CustomPaint( size: targetSize, painter: WarpImagePainter( image: _image!, srcPoints: _srcPoints, targetSize: targetSize, ), ), ); } }
3. 关键注意事项
- 点的顺序一致性:原始标记点和目标四角的顺序必须一致(比如都按顺时针:左上→右上→右下→左下),否则变换会出现扭曲错误。
- 坐标空间匹配:确保原始标记点的坐标是相对于画布的,若使用图像自身坐标,需先转换为画布坐标(比如根据图像缩放比例计算)。
- 性能优化:如果图像尺寸较大,建议先压缩图像再进行变换,避免绘制卡顿。
内容的提问来源于stack exchange,提问作者matteoh
相关产品推荐
相关产品推荐

