You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 21:07:32