You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Flutter中如何可靠测量相机到物体的距离并仅在特定范围启用拍照按钮

Flutter中如何可靠测量相机到物体的距离并仅在特定范围启用拍照按钮

嘿,这个需求我之前做证件扫描APP的时候刚好踩过坑,给你分享一下我实际用到的落地方案,分步骤来,保证能搞定:

一、先选适合你场景的距离估算方法

不同设备和场景适配不同方案,我给你列三个实际能用的,按需挑:

1. 基于已知物体尺寸的单目视觉估算(最通用,不需要双摄)

这是我用得最多的方案,核心原理是相似三角形公式距离 = (物体实际宽度 × 相机焦距) / 图像中物体的像素宽度
有两个前提:你得知道目标物体的实际尺寸(比如身份证宽85.6mm),还要能在相机预览里精准检测到物体的像素宽度。

具体实现思路:

  • 获取相机焦距:可以用camera插件的CameraController结合传感器尺寸、预览尺寸计算,或者用camera_info插件直接从硬件参数里读取(注意单位统一,比如转成米)。
  • 实时检测物体像素宽度:用google_ml_kit的物体检测或轮廓检测,在相机预览流的每一帧里,识别目标物体的 bounding box,算出它的像素宽度。
  • 计算距离:套上面的公式就行。

给你个简化的代码示例:

// 目标物体的实际宽度(比如身份证,单位:米)
final double realObjectWidth = 0.0856;

// 计算距离
double calculateDistance(double objectPixelWidth, double focalLength) {
  return (realObjectWidth * focalLength) / objectPixelWidth;
}

// 获取相机焦距(单位:米)
double getCameraFocalLength() {
  // 实际项目里用camera_info插件获取硬件焦距,这里简化模拟
  return 0.004; // 比如4mm焦距转成米
}

2. 利用双摄的深度传感器(精度更高,仅支持带深度相机的设备)

如果你的目标用户用的是中高端机型,有深度传感器,直接拿深度数据精度会高很多。
初始化相机时选支持深度图像的后置相机,启动预览流监听深度帧,取画面中心区域的平均深度(毕竟用户拍的物体一般在中心)。

简化代码示例:

// 初始化时优先选带深度传感器的后置相机
final cameras = await availableCameras();
final depthCamera = cameras.firstWhere(
  (c) => c.lensDirection == CameraLensDirection.back && c.supportsDepthImage,
  orElse: () => cameras.firstWhere((c) => c.lensDirection == CameraLensDirection.back),
);

// 启动深度帧监听
_cameraController!.startImageStream((CameraImage image) {
  // 解析深度帧数据(不同设备格式不同,比如YUV、RAW16)
  final double averageDepth = _calculateAverageDepth(image);
  setState(() => _currentDistance = averageDepth);
});

3. 基于ARCore/ARKit的平面检测(适合AR场景,精度拉满)

如果你的APP是AR相关的,直接用ARCore或ARKit的平面检测功能,框架会自动校准相机到平面的距离,精度非常高,还能自动处理设备姿态变化。

二、动态控制拍照按钮的启用状态

这部分很简单,核心是用状态变量绑定距离范围和按钮可用性:

  1. 先在State类里定义几个关键变量:
double? _currentDistance;
bool _isCaptureEnabled = false;
// 自定义你的距离范围,比如10cm到30cm
final double minDistance = 0.1;
final double maxDistance = 0.3;
// 用历史数据平滑距离,避免按钮频繁跳变
List<double> _distanceHistory = [];
final int _historySize = 5;
  1. 在预览帧处理逻辑里,更新距离和按钮状态:
void _processPreviewFrame(CameraImage image) {
  // 先检测物体的像素宽度(或深度)
  final double? objectPixelWidth = _detectObjectWidth(image);
  if (objectPixelWidth == null) {
    setState(() => _isCaptureEnabled = false);
    return;
  }

  // 计算原始距离
  final focalLength = getCameraFocalLength();
  final rawDistance = (realObjectWidth * focalLength) / objectPixelWidth;
  
  // 平滑处理:取最近5次的平均值,避免数据波动
  _distanceHistory.add(rawDistance);
  if (_distanceHistory.length > _historySize) _distanceHistory.removeAt(0);
  _currentDistance = _distanceHistory.reduce((a, b) => a + b) / _distanceHistory.length;

  // 判断是否在允许的距离范围内
  setState(() {
    _isCaptureEnabled = _currentDistance != null &&
        _currentDistance! >= minDistance &&
        _currentDistance! <= maxDistance &&
        _isCameraInitialized;
  });
}

// 模拟物体检测,实际用google_ml_kit实现
double? _detectObjectWidth(CameraImage image) {
  // 这里替换成你的真实物体检测逻辑:
  // 比如用google_ml_kit的ObjectDetector拿到物体bounding box的width
  return 200.0; // 模拟返回像素宽度
}
  1. 最后修改你的拍照按钮:
// 替换你原来的按钮代码
_capturedImage == null && _isCameraInitialized
    ? ElevatedButton(
        onPressed: _isCaptureEnabled ? _captureImage : null, // onPressed为null时按钮自动禁用
        child: Text(
          _isCaptureEnabled ? '拍照' : '请将物体移到10-30cm范围内',
          style: const TextStyle(fontSize: 16),
        ),
        style: ElevatedButton.styleFrom(
          backgroundColor: _isCaptureEnabled ? Colors.green : Colors.grey,
          shape: const CircleBorder(),
          fixedSize: const Size(100, 50),
        ),
      )
    : ElevatedButton(
        onPressed: () => setState(() => _capturedImage = null),
        child: const Text('重拍'),
      )

三、几个提高可靠性的实用技巧

这些都是我踩坑总结的经验,必须加上:

  • 不要每一帧都处理:相机预览每秒30帧,全处理太耗性能,加个节流(比如每100ms处理一次)或者跳帧处理。
  • 加视觉引导框:在预览上画个框,提示用户把物体放到框内,提升物体检测的准确率,距离计算也更准。
  • 做好设备兼容:没有深度传感器的设备自动降级到单目视觉;不同设备的相机参数不一样,要做好适配。
  • 友好的错误提示:检测不到物体时,按钮禁用的同时提示“请对准物体”,用户体验更好。
  • 增加校准选项:如果是通用场景,可以加个校准步骤——让用户把物体放在已知距离(比如20cm)处,自动计算焦距,进一步提升精度。

四、整合到你现有代码的完整示例

把你的代码修改后,整合上面的逻辑,关键部分都标出来了:

class _YourCameraScreenState extends State<YourCameraScreen> {
  CameraController? _cameraController;
  bool _isCameraInitialized = false;
  XFile? _capturedImage;
  String? _uploadedImageUrl;
  dynamic _detections;
  double? _currentDistance;
  bool _isCaptureEnabled = false;
  final double minDistance = 0.1;
  final double maxDistance = 0.3;
  final double realObjectWidth = 0.0856;
  List<double> _distanceHistory = [];
  final int _historySize = 5;

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

  Future<void> _initCamera() async {
    final cameras = await availableCameras();
    final backCamera = cameras.firstWhere((c) => c.lensDirection == CameraLensDirection.back);
    _cameraController = CameraController(
      backCamera,
      ResolutionPreset.medium, // 用中等分辨率平衡性能和精度
      enableAudio: false,
    );
    await _cameraController!.initialize();
    // 启动预览流处理帧
    _cameraController!.startImageStream((CameraImage image) {
      _processPreviewFrame(image);
    });
    setState(() => _isCameraInitialized = true);
  }

  void _processPreviewFrame(CameraImage image) {
    final double? objectPixelWidth = _detectObjectWidth(image);
    if (objectPixelWidth == null) {
      setState(() => _isCaptureEnabled = false);
      return;
    }

    final focalLength = getCameraFocalLength();
    final rawDistance = (realObjectWidth * focalLength) / objectPixelWidth;
    
    _distanceHistory.add(rawDistance);
    if (_distanceHistory.length > _historySize) _distanceHistory.removeAt(0);
    _currentDistance = _distanceHistory.reduce((a, b) => a + b) / _distanceHistory.length;

    setState(() {
      _isCaptureEnabled = _currentDistance != null &&
          _currentDistance! >= minDistance &&
          _currentDistance! <= maxDistance &&
          _isCameraInitialized;
    });
  }

  double getCameraFocalLength() {
    // 实际用camera_info插件获取硬件焦距,这里简化
    return 0.004;
  }

  double? _detectObjectWidth(CameraImage image) {
    // 替换成你的真实物体检测逻辑
    return 200.0; // 模拟值
  }

  Future<void> _captureImage() async {
    if (!_isCameraInitialized || _cameraController == null) return;
    final XFile file = await _cameraController!.takePicture();
    final croppedFile = await cropToOverlay(File(file.path), scanSquareSize, context);
    setState(() {
      _capturedImage = croppedFile;
      _uploadedImageUrl = null;
      _detections = null;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          if (_isCameraInitialized) CameraPreview(_cameraController!),
          // 视觉引导框,根据状态变颜色
          Positioned(
            left: MediaQuery.of(context).size.width / 2 - 100,
            top: MediaQuery.of(context).size.height / 2 - 100,
            child: Container(
              width: 200,
              height: 200,
              decoration: BoxDecoration(
                border: Border.all(
                  color: _isCaptureEnabled ? Colors.green : Colors.red,
                  width: 2,
                ),
              ),
            ),
          ),
          // 拍照按钮
          Positioned(
            bottom: 30,
            left: MediaQuery.of(context).size.width / 2 - 50,
            child: _capturedImage == null && _isCameraInitialized
                ? ElevatedButton(
                    onPressed: _isCaptureEnabled ? _captureImage : null,
                    child: Text(
                      _isCaptureEnabled ? '拍照' : '请移到10-30cm范围内',
                      style: const TextStyle(fontSize: 16),
                    ),
                    style: ElevatedButton.styleFrom(
                      backgroundColor: _isCaptureEnabled ? Colors.green : Colors.grey,
                      shape: const CircleBorder(),
                      fixedSize: const Size(100, 50),
                    ),
                  )
                : ElevatedButton(
                    onPressed: () => setState(() => _capturedImage = null),
                    child: const Text('重拍'),
                  ),
          ),
        ],
      ),
    );
  }
}

最后再提醒你几个关键点:

  • 单目视觉的精度完全依赖物体检测的准确性,所以物体检测逻辑要尽量鲁棒。
  • 深度传感器的部分数据是相对值,可能需要校准才能拿到真实距离。
  • 性能方面,尽量用低一点的预览分辨率,或者跳帧处理,避免APP卡顿。

这个方案我之前用在证件扫描APP里,用户反馈挺不错的,应该能完美解决你的需求!

火山引擎 最新活动