rectangle_detector 1.0.5 copy "rectangle_detector: ^1.0.5" to clipboard
rectangle_detector: ^1.0.5 copied to clipboard

A Flutter plugin for rectangle detection and feature point identification. Supports Uint8List, ui.Image, and ImageProvider inputs.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:ui' as ui;
import 'dart:math' show Point;

import 'package:flutter/services.dart';
import 'package:rectangle_detector/rectangle_detector.dart';
import 'package:image_picker/image_picker.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Rectangle Detector Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const RectangleDetectorDemo(),
    );
  }
}

class RectangleDetectorDemo extends StatefulWidget {
  const RectangleDetectorDemo({super.key});

  @override
  State<RectangleDetectorDemo> createState() => _RectangleDetectorDemoState();
}

class _RectangleDetectorDemoState extends State<RectangleDetectorDemo> {
  List<RectangleFeature> _detectedRectangles = [];
  bool _isDetecting = false;
  ui.Image? _image;
  double _imageWidth = 0;
  double _imageHeight = 0;
  String _statusMessage = '请选择一张图片开始检测矩形';
  final ImagePicker _picker = ImagePicker();
  bool _isPanelExpanded = false;
  bool _showOnlyLargestRectangle = false;

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

  /// 加载默认测试图片
  Future<void> _loadDefaultImage() async {
    try {
      // 从assets加载默认图片
      final ByteData data = await rootBundle.load('assets/images/tv.jpeg');
      final Uint8List bytes = data.buffer.asUint8List();

      // 解码图片
      final ui.Codec codec = await ui.instantiateImageCodec(bytes);
      final ui.FrameInfo frameInfo = await codec.getNextFrame();

      setState(() {
        _image = frameInfo.image;
        _imageWidth = frameInfo.image.width.toDouble();
        _imageHeight = frameInfo.image.height.toDouble();
        _statusMessage = '已加载默认测试图片,点击检测按钮开始检测矩形';
        _detectedRectangles = []; // 清除之前的检测结果
      });
    } catch (e) {
      setState(() {
        _statusMessage = '默认图片加载失败: $e';
      });
    }
  }

  /// 选择图片
  /// 移除尺寸和质量限制,保持原始图片质量以确保检测结果一致性
  Future<void> _pickImage() async {
    try {
      final XFile? pickedFile = await _picker.pickImage(
        source: ImageSource.gallery,
        maxWidth: 3840, // 4K分辨率
        maxHeight: 3840,
        imageQuality: 100,
      );

      if (pickedFile != null) {
        // 使用 XFile 的 readAsBytes 方法,兼容 Web 平台
        final Uint8List bytes = await pickedFile.readAsBytes();

        // 解码图片
        final ui.Codec codec = await ui.instantiateImageCodec(bytes);
        final ui.FrameInfo frameInfo = await codec.getNextFrame();

        setState(() {
          _image = frameInfo.image;
          _imageWidth = frameInfo.image.width.toDouble();
          _imageHeight = frameInfo.image.height.toDouble();
          _statusMessage = '图片加载完成,点击检测按钮开始检测矩形';
          _detectedRectangles = []; // 清除之前的检测结果
        });
      }
    } catch (e) {
      setState(() {
        _statusMessage = '图片选择失败: $e';
      });
    }
  }

  /// 检测矩形
  Future<void> _detectRectangles() async {
    if (_image == null) {
      setState(() {
        _statusMessage = '请先选择一张图片';
      });
      return;
    }

    setState(() {
      _isDetecting = true;
      _statusMessage = '正在检测矩形...';
    });

    try {
      // 将图片转换为字节数组
      final ByteData? byteData = await _image!.toByteData(
        format: ui.ImageByteFormat.png,
      );
      if (byteData == null) {
        setState(() {
          _isDetecting = false;
          _statusMessage = '图片转换失败';
        });
        return;
      }

      final Uint8List imageBytes = byteData.buffer.asUint8List();

      // 调用插件检测矩形
      final List<RectangleFeature> rectangles =
          await RectangleDetector.detectAllRectangles(imageBytes);

      if (rectangles.isNotEmpty) {
        setState(() {
          _detectedRectangles = rectangles;
          _isDetecting = false;
          _statusMessage =
              '检测到 ${rectangles.length} 个矩形:\n'
              '${rectangles
                  .map(
                    (rectangle) =>
                        'TL: (${rectangle.topLeft.x.toStringAsFixed(1)}, ${rectangle.topLeft.y.toStringAsFixed(1)})\n'
                        'TR: (${rectangle.topRight.x.toStringAsFixed(1)}, ${rectangle.topRight.y.toStringAsFixed(1)})\n'
                        'BL: (${rectangle.bottomLeft.x.toStringAsFixed(1)}, ${rectangle.bottomLeft.y.toStringAsFixed(1)})\n'
                        'BR: (${rectangle.bottomRight.x.toStringAsFixed(1)}, ${rectangle.bottomRight.y.toStringAsFixed(1)})',
                  )
                  .join('\n\n')}';
        });
      } else {
        setState(() {
          _detectedRectangles = [];
          _isDetecting = false;
          _statusMessage = '未检测到矩形';
        });
      }
    } catch (e) {
      setState(() {
        _isDetecting = false;
        _statusMessage = '检测失败: $e';
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('矩形检测演示'), backgroundColor: Colors.blue),
      body: Stack(
        children: [
          // 图片显示区域 - 占据全屏
          _image == null
              ? const Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(Icons.image_outlined, size: 80, color: Colors.grey),
                      SizedBox(height: 16),
                      Text(
                        '请选择一张图片开始检测',
                        style: TextStyle(fontSize: 16, color: Colors.grey),
                      ),
                    ],
                  ),
                )
              : InteractiveViewer(
                  child: Center(
                    child: CustomPaint(
                      painter: RectanglePainter(
                        image: _image!,
                        rectangles: _detectedRectangles,
                        imageWidth: _imageWidth,
                        imageHeight: _imageHeight,
                        showOnlyLargestRectangle: _showOnlyLargestRectangle,
                      ),
                      child: Container(),
                    ),
                  ),
                ),

          // 悬浮控制面板
          _buildFloatingControlPanel(),
        ],
      ),
    );
  }

  /// 构建悬浮控制面板
  /// 根据屏幕宽度动态调整面板尺寸,避免布局溢出
  Widget _buildFloatingControlPanel() {
    return Positioned(
      top: 16,
      right: 16,
      child: LayoutBuilder(
        builder: (context, constraints) {
          // 根据可用空间动态计算面板宽度
          final screenWidth = MediaQuery.of(context).size.width;
          final maxPanelWidth = (screenWidth - 32).clamp(
            240.0,
            360.0,
          ); // 留出左右各16像素边距,最小宽度降至240

          return Material(
            elevation: 8,
            borderRadius: BorderRadius.circular(12),
            color: Colors.white,
            child: AnimatedContainer(
              duration: const Duration(milliseconds: 300),
              width: _isPanelExpanded ? maxPanelWidth : 56,
              constraints: BoxConstraints(
                maxHeight: _isPanelExpanded ? 400 : 56,
                minHeight: 56,
              ),
              padding: const EdgeInsets.all(8),
              child: _isPanelExpanded
                  ? _buildExpandedPanel()
                  : _buildCollapsedPanel(),
            ),
          );
        },
      ),
    );
  }

  /// 构建折叠状态的面板
  Widget _buildCollapsedPanel() {
    return GestureDetector(
      onTap: () {
        setState(() {
          _isPanelExpanded = true;
        });
      },
      child: Center(
        child: Container(
          width: 40,
          height: 40,
          decoration: BoxDecoration(
            color: Colors.blue,
            borderRadius: BorderRadius.circular(8),
          ),
          child: const Icon(Icons.settings, color: Colors.white, size: 24),
        ),
      ),
    );
  }

  /// 构建展开状态的面板
  Widget _buildExpandedPanel() {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 标题栏
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            const Flexible(
              child: FittedBox(
                fit: BoxFit.scaleDown,
                child: Text(
                  '控制面板',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                  overflow: TextOverflow.ellipsis,
                ),
              ),
            ),
            GestureDetector(
              onTap: () {
                setState(() {
                  _isPanelExpanded = false;
                });
              },
              child: Container(
                width: 16,
                height: 16,
                decoration: BoxDecoration(
                  color: Colors.grey[300],
                  borderRadius: BorderRadius.circular(2),
                ),
                child: const Icon(Icons.close, size: 12, color: Colors.black54),
              ),
            ),
          ],
        ),
        const SizedBox(height: 12),

        // 按钮区域
        LayoutBuilder(
          builder: (context, constraints) {
            // 根据可用宽度计算按钮高度,保持合理的宽高比
            double buttonHeight = (constraints.maxWidth * 0.15).clamp(
              24.0,
              40.0,
            );

            return Row(
              children: [
                Expanded(
                  child: SizedBox(
                    height: buttonHeight,
                    child: ElevatedButton(
                      onPressed: _pickImage,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.green,
                        foregroundColor: Colors.white,
                        padding: const EdgeInsets.symmetric(
                          vertical: 4,
                          horizontal: 8,
                        ),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(6),
                        ),
                      ),
                      child: const FittedBox(
                        fit: BoxFit.scaleDown,
                        child: Text('选择图片', style: TextStyle(fontSize: 14)),
                      ),
                    ),
                  ),
                ),
                const SizedBox(width: 6),
                Expanded(
                  child: SizedBox(
                    height: buttonHeight,
                    child: ElevatedButton(
                      onPressed: _isDetecting || _image == null
                          ? null
                          : _detectRectangles,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.blue,
                        foregroundColor: Colors.white,
                        padding: const EdgeInsets.symmetric(
                          vertical: 4,
                          horizontal: 8,
                        ),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(6),
                        ),
                      ),
                      child: _isDetecting
                          ? SizedBox(
                              width: buttonHeight * 0.4,
                              height: buttonHeight * 0.4,
                              child: const CircularProgressIndicator(
                                strokeWidth: 2.0,
                                color: Colors.white,
                              ),
                            )
                          : const FittedBox(
                              fit: BoxFit.scaleDown,
                              child: Text(
                                '检测矩形',
                                style: TextStyle(fontSize: 14),
                              ),
                            ),
                    ),
                  ),
                ),
              ],
            );
          },
        ),
        const SizedBox(height: 12),

        // 显示选项开关
        Container(
          padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
          decoration: BoxDecoration(
            color: Colors.grey[50],
            borderRadius: BorderRadius.circular(8),
            border: Border.all(color: Colors.grey[300]!),
          ),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              const Expanded(
                child: Text(
                  '显示最大矩形',
                  style: TextStyle(fontSize: 11),
                  overflow: TextOverflow.ellipsis,
                ),
              ),
              Transform.scale(
                scale: 0.8,
                child: Switch(
                  value: _showOnlyLargestRectangle,
                  onChanged: (value) {
                    setState(() {
                      _showOnlyLargestRectangle = value;
                    });
                  },
                  materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
                  activeColor: Colors.blue,
                ),
              ),
            ],
          ),
        ),
        const SizedBox(height: 12),

        // 状态信息
        Flexible(
          child: Container(
            width: double.infinity,
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: Colors.grey[100],
              borderRadius: BorderRadius.circular(8),
            ),
            child: SingleChildScrollView(
              child: Text(_statusMessage, style: const TextStyle(fontSize: 12)),
            ),
          ),
        ),
      ],
    );
  }
}

/// 自定义画笔,用于绘制图片和检测到的矩形
class RectanglePainter extends CustomPainter {
  final ui.Image image;
  final List<RectangleFeature>? rectangles;
  final double imageWidth;
  final double imageHeight;
  final bool showOnlyLargestRectangle;

  RectanglePainter({
    required this.image,
    this.rectangles,
    required this.imageWidth,
    required this.imageHeight,
    this.showOnlyLargestRectangle = false,
  });

  @override
  void paint(Canvas canvas, Size size) {
    // 计算图片在画布中的显示尺寸和位置
    final double aspectRatio = imageWidth / imageHeight;
    final double canvasAspectRatio = size.width / size.height;

    double displayWidth, displayHeight;
    double offsetX = 0, offsetY = 0;

    if (aspectRatio > canvasAspectRatio) {
      // 图片更宽,以宽度为准
      displayWidth = size.width;
      displayHeight = size.width / aspectRatio;
      offsetY = (size.height - displayHeight) / 2;
    } else {
      // 图片更高,以高度为准
      displayHeight = size.height;
      displayWidth = size.height * aspectRatio;
      offsetX = (size.width - displayWidth) / 2;
    }

    // 绘制图片
    final Rect imageRect = Rect.fromLTWH(
      offsetX,
      offsetY,
      displayWidth,
      displayHeight,
    );
    canvas.drawImageRect(
      image,
      Rect.fromLTWH(0, 0, imageWidth, imageHeight),
      imageRect,
      Paint(),
    );

    // 绘制检测到的矩形
    if (rectangles != null && rectangles!.isNotEmpty) {
      final Paint rectanglePaint = Paint()
        ..color = Colors.red
        ..style = PaintingStyle.stroke
        ..strokeWidth = 3.0;

      final Paint pointPaint = Paint()
        ..color = Colors.blue
        ..style = PaintingStyle.fill;

      // 根据设置决定绘制哪些矩形
      List<RectangleFeature> rectanglesToDraw;
      if (showOnlyLargestRectangle) {
        rectanglesToDraw = [_findLargestRectangle(rectangles!)];
      } else {
        rectanglesToDraw = rectangles!;
      }

      for (final rectangle in rectanglesToDraw) {
        // 获取矩形的四个顶点
        final List<Offset> uiPoints = [
          // 转换坐标系:从图片坐标系转换到UI坐标系
          Offset(
            offsetX + (rectangle.topLeft.x / imageWidth) * displayWidth,
            offsetY + (rectangle.topLeft.y / imageHeight) * displayHeight,
          ),
          Offset(
            offsetX + (rectangle.topRight.x / imageWidth) * displayWidth,
            offsetY + (rectangle.topRight.y / imageHeight) * displayHeight,
          ),
          Offset(
            offsetX + (rectangle.bottomRight.x / imageWidth) * displayWidth,
            offsetY + (rectangle.bottomRight.y / imageHeight) * displayHeight,
          ),
          Offset(
            offsetX + (rectangle.bottomLeft.x / imageWidth) * displayWidth,
            offsetY + (rectangle.bottomLeft.y / imageHeight) * displayHeight,
          ),
        ];

        // 绘制矩形边框
        final Path path = Path();
        path.moveTo(uiPoints[0].dx, uiPoints[0].dy);
        for (int i = 1; i < uiPoints.length; i++) {
          path.lineTo(uiPoints[i].dx, uiPoints[i].dy);
        }
        path.close();
        canvas.drawPath(path, rectanglePaint);

        // 绘制角点
        for (final point in uiPoints) {
          canvas.drawCircle(point, 6, pointPaint);
        }

        // 绘制点坐标标签
        final List<String> labels = ['TL', 'TR', 'BR', 'BL'];
        for (int i = 0; i < uiPoints.length; i++) {
          final point = uiPoints[i];
          final textPainter = TextPainter(
            text: TextSpan(
              text: labels[i],
              style: const TextStyle(
                color: Colors.white,
                fontSize: 10,
                fontWeight: FontWeight.bold,
              ),
            ),
            textDirection: TextDirection.ltr,
          );
          textPainter.layout();
          textPainter.paint(
            canvas,
            Offset(
              point.dx - textPainter.width / 2,
              point.dy - textPainter.height / 2,
            ),
          );
        }
      }
    }
  }

  /// 计算矩形面积并找到最大的矩形
  RectangleFeature _findLargestRectangle(List<RectangleFeature> rectangles) {
    RectangleFeature largestRectangle = rectangles.first;
    double maxArea = _calculateRectangleArea(largestRectangle);

    for (final rectangle in rectangles) {
      final double area = _calculateRectangleArea(rectangle);
      if (area > maxArea) {
        maxArea = area;
        largestRectangle = rectangle;
      }
    }

    return largestRectangle;
  }

  /// 计算矩形面积(使用向量叉积计算四边形面积)
  double _calculateRectangleArea(RectangleFeature rectangle) {
    // 使用鞋带公式计算四边形面积
    final List<Point<double>> points = [
      rectangle.topLeft,
      rectangle.topRight,
      rectangle.bottomRight,
      rectangle.bottomLeft,
    ];

    double area = 0;
    for (int i = 0; i < points.length; i++) {
      final int j = (i + 1) % points.length;
      area += points[i].x * points[j].y;
      area -= points[j].x * points[i].y;
    }

    return (area / 2).abs();
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}
0
likes
145
points
19
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter plugin for rectangle detection and feature point identification. Supports Uint8List, ui.Image, and ImageProvider inputs.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, flutter_web_plugins, plugin_platform_interface, web

More

Packages that depend on rectangle_detector

Packages that implement rectangle_detector