Другое

Flutter: Руководство по захвату на основе расстояния

Узнайте, как измерять расстояние до объекта в Flutter и активировать кнопку захвата только в оптимальном диапазоне. Полное руководство по реализации ARKit/ARCore и компьютерного зрения.

Как можно надежно измерить расстояние до объекта и активировать кнопку захвата только тогда, когда расстояние находится в определенном диапазоне во Flutter?

Я создаю приложение Flutter, где пользователи могут делать снимки объектов, но я хочу, чтобы кнопка захвата была активна только тогда, когда камера находится на определенном расстоянии от объекта. Я использую плагин камеры для получения живого превью и мне нужна логика для оценки расстояния от камеры до объекта перед ней.

Моя текущая рабочая流程:

  1. Показывать живое превью камеры
  2. Непрерывно оценивать расстояние от камеры до объекта
  3. Активировать кнопку захвата только тогда, когда расстояние находится в целевом диапазоне
  4. Захватывать и сохранять/обрезать изображение, когда это разрешено

У меня есть следующий код для захвата изображений:

dart
if (_capturedImage == null && _isCameraInitialized)
              ElevatedButton(
                onPressed: _captureImage,
                child: const Text('Capture Image'),
              ),
dart
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;
  });
}

Какой лучший подход для реализации измерения расстояния во Flutter для контроля, когда кнопка захвата активна?

Да, вы можете надежно измерять расстояние до объектов в Flutter с помощью либо AR-решений, либо подходов компьютерного зрения. Для вашего случая использования, когда кнопка съемки активируется только тогда, когда камера находится на оптимальном расстоянии, я рекомендую использовать ARKit/ARCore для iOS/Android соответственно, поскольку они обеспечивают наиболее точные измерения глубины с помощью датчиков устройства.

Содержание


AR-измерение расстояния

ARKit для iOS и ARCore для Android обеспечивают наиболее надежные возможности измерения расстояния в Flutter. Эти платформы используют датчики устройства для расчета реальных расстояний в реальном мире.

Рекомендуемые плагины

Для интеграции с iOS используйте arkit_plugin, который предоставляет доступ к возможностям Apple ARKit. Обратите внимание, что ARKit недоступен на Android - вам понадобится использовать ARCore.

Для кроссплатформенной поддержки вы можете объединить оба плагина в вашем приложении Flutter:

dart
dependencies:
  flutter:
    sdk: flutter
  arkit_plugin: ^0.5.0  # Для iOS
  arcore_flutter_plugin: ^0.8.0  # Для Android

Согласно Stack Overflow, разработчики успешно объединяли оба плагина в одном приложении Flutter, где ARCore работал на устройствах Android, а ARKit - на устройствах iOS.

Расчет расстояния с ARKit/ARCore

Рабочие единицы для ARKit и ARCore - метры, которые вы можете преобразовать в предпочитаемую единицу измерения:

dart
// Расстояние в метрах
double distanceInMeters = arHitResult.distance;

// Преобразование в сантиметры
double distanceInCm = distanceInMeters * 100;

// Преобразование в дюймы
double distanceInInches = distanceInMeters * 39.37;

Альтернатива с компьютерным зрением

Если поддержка ARKit/ARCore недоступна или вам требуется резервное решение, подходы компьютерного зрения могут оценивать расстояние с помощью обнаружения объектов и калибровки камеры.

Обнаружение объектов с TensorFlow Lite

Используйте Flutter Vision или Google ML Kit для обнаружения объектов и оценки расстояния на основе их известных размеров:

dart
dependencies:
  camera: ^0.10.5+2
  tflite_flutter: ^3.1.0
  flutter_vision: ^0.0.9

Плагин camera может предоставлять потоки изображений, а Flutter Vision обеспечивает обнаружение объектов.

Примечание: Оценка расстояния с помощью компьютерного зрения менее точна, чем AR-методы, но работает на большем количестве устройств. Точность зависит от знания размера объекта и калибровки камеры.


Шаги реализации

Шаг 1: Инициализация AR-сессии

Для iOS (ARKit):

dart
import 'package:arkit_plugin/arkit_plugin.dart';

void initializeARKit() async {
  final arkitSession = ARKitSession();
  await arkitSession.configure();
  
  // Добавление обнаружения плоскостей для стабильных измерений
  arkitSession.addConfiguration(ARKitWorldTrackingConfiguration(
    planeDetection: ARKitPlaneDetection.horizontal,
  ));
}

Для Android (ARCore):

dart
import 'package/arcore_flutter_plugin/arcore_flutter_plugin.dart';

void initializeARCore() async {
  final arCoreController = ArCoreController(
    onArCoreViewCreated: _onArCoreViewCreated,
    enableTapRecognizer: true,
  );
}

Шаг 2: Реализация отслеживания расстояния

dart
class DistanceTracker {
  double _targetDistance = 1.0; // 1 метр (настройте в соответствии с вашими потребностями)
  double _tolerance = 0.1; // ±10 см допуска
  
  bool isWithinTargetRange(double currentDistance) {
    return (currentDistance >= (_targetDistance - _tolerance) && 
            currentDistance <= (_targetDistance + _tolerance));
  }
  
  double getDistanceInMeters(ArKitHitResult hitResult) {
    return hitResult.distance;
  }
}

Шаг 3: Непрерывный мониторинг расстояния

Используйте периодический таймер Flutter или обратные вызовы AR-сессии для непрерывной проверки расстояния:

dart
Timer.periodic(Duration(milliseconds: 100), (timer) {
  if (_arSession != null && _arSession!.currentHitResult != null) {
    final distance = _distanceTracker.getDistanceInMeters(
      _arSession!.currentHitResult!
    );
    
    setState(() {
      _isWithinTargetRange = _distanceTracker.isWithinTargetRange(distance);
      _currentDistance = distance;
    });
  }
});

Методы оценки расстояния

Метод 1: Тестирование попаданий ARKit/ARCore

Наиболее точный метод с использованием датчиков устройства:

dart
Future<double> measureDistanceWithAR() async {
  final arSession = ARKitSession();
  final hitTestResults = await arSession.hitTest(
    screenCenter: Offset(0.5, 0.5), // Центр экрана
    types: [ARKitHitTestResultType.existingPlaneUsingExtent]
  );
  
  if (hitTestResults.isNotEmpty) {
    return hitTestResults.first.distance;
  }
  return -1.0; // Нет допустимого измерения
}

Метод 2: Оценка на основе размера объекта

Для подхода компьютерного зрения, когда размеры объекта известны:

dart
double estimateDistanceByObjectSize(
  double objectHeightPixels, 
  double knownObjectHeightMeters,
  double cameraFocalLengthPixels
) {
  // Расстояние = (Известная высота объекта × Фокусное расстояние) / Высота объекта в пикселях
  return (knownObjectHeightMeters * cameraFocalLengthPixels) / objectHeightPixels;
}

Управление состоянием кнопки

Измените существующую кнопку съемки для использования состояния на основе расстояния:

dart
if (_isCameraInitialized)
  ElevatedButton(
    onPressed: _isWithinTargetRange ? _captureImage : null,
    style: ElevatedButton.styleFrom(
      backgroundColor: _isWithinTargetRange ? Colors.green : Colors.grey,
    ),
    child: Text(
      _isWithinTargetRange 
        ? 'Сделать снимок' 
        : 'Расстояние: ${(_currentDistance * 100).toStringAsFixed(0)}см (Цель: ${(_targetDistance * 100).toStringAsFixed(0)}см)',
    ),
  ),

Улучшенная функция съемки с проверкой расстояния

dart
Future<void> _captureImage() async {
  if (!_isCameraInitialized || _cameraController == null || !_isWithinTargetRange) {
    _showDistanceAlert();
    return;
  }

  final XFile file = await _cameraController!.takePicture();

  // Продолжить только если все еще в допустимом диапазоне после задержки съемки
  if (_isWithinTargetRange) {
    final croppedFile = await cropToOverlay(File(file.path), scanSquareSize, context);
    
    setState(() {
      _capturedImage = croppedFile;
      _uploadedImageUrl = null;
      _detections = null;
    });
  } else {
    _showDistanceAlert();
  }
}

void _showDistanceAlert() {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text('Пожалуйста, подойдите к целевому расстоянию: ${(_targetDistance * 100).toStringAsFixed(0)}см'),
      backgroundColor: Colors.orange,
    ),
  );
}

Рекомендации по производительности

Оптимизация измерений расстояния

  1. Частота кадров: Расчеты расстояния должны выполняться с частотой 10-15 Гц, а не с полной частотой кадров камеры
  2. Ограничение: Реализуйте минимальный интервал между измерениями для избежания проблем с производительностью
  3. Обработка ошибок: Добавьте проверку недопустимых измерений и недоступности датчиков
dart
class OptimizedDistanceTracker {
  double _lastMeasurementTime = 0;
  static const double _minMeasurementInterval = 0.1; // 100 мс
  
  bool shouldTakeMeasurement() {
    final now = DateTime.now().millisecondsSinceEpoch / 1000;
    return (now - _lastMeasurementTime) >= _minMeasurementInterval;
  }
  
  void recordMeasurement() {
    _lastMeasurementTime = DateTime.now().millisecondsSinceEpoch / 1000;
  }
}

Оптимизация батареи

  • Используйте паузу/возобновление для AR-сессий, когда приложение в фоновом режиме
  • Сократите частоту измерений, когда заряд батареи низкий
  • Реализуйте откат к базовой камере, когда AR недоступен

Пример полной интеграции

Вот полный виджет, интегрирующий съемку на основе расстояния:

dart
class DistanceBasedCaptureWidget extends StatefulWidget {
  final double targetDistance;
  final double tolerance;
  
  const DistanceBasedCaptureWidget({
    Key? key,
    required this.targetDistance,
    this.tolerance = 0.1,
  }) : super(key: key);

  @override
  _DistanceBasedCaptureWidgetState createState() => _DistanceBasedCaptureWidgetState();
}

class _DistanceBasedCaptureWidgetState extends State<DistanceBasedCaptureWidget> {
  CameraController? _cameraController;
  bool _isCameraInitialized = false;
  bool _isWithinTargetRange = false;
  double _currentDistance = -1.0;
  ARKitSession? _arSession;
  DistanceTracker? _distanceTracker;
  Timer? _distanceTimer;
  File? _capturedImage;

  @override
  void initState() {
    super.initState();
    _initializeCamera();
    _initializeAR();
    _startDistanceMonitoring();
  }

  Future<void> _initializeCamera() async {
    _cameraController = CameraController(
      cameras[0], // Используем заднюю камеру
      ResolutionPreset.medium,
    );
    
    await _cameraController!.initialize();
    setState(() => _isCameraInitialized = true);
  }

  Future<void> _initializeAR() async {
    _distanceTracker = DistanceTracker(
      targetDistance: widget.targetDistance,
      tolerance: widget.tolerance,
    );
    
    _arSession = ARKitSession();
    await _arSession!.configure();
  }

  void _startDistanceMonitoring() {
    _distanceTimer = Timer.periodic(Duration(milliseconds: 100), (timer) {
      _updateDistanceMeasurement();
    });
  }

  Future<void> _updateDistanceMeasurement() async {
    if (_arSession == null) return;
    
    try {
      final hitResults = await _arSession!.hitTest(
        screenCenter: Offset(0.5, 0.5),
        types: [ARKitHitTestResultType.existingPlaneUsingExtent]
      );
      
      if (hitResults.isNotEmpty) {
        final distance = hitResults.first.distance;
        setState(() {
          _currentDistance = distance;
          _isWithinTargetRange = _distanceTracker!.isWithinTargetRange(distance);
        });
      }
    } catch (e) {
      print('Ошибка измерения расстояния: $e');
    }
  }

  Future<void> _captureImage() async {
    if (!_isCameraInitialized || _cameraController == null || !_isWithinTargetRange) {
      _showDistanceAlert();
      return;
    }

    try {
      final XFile file = await _cameraController!.takePicture();
      
      // Добавляем небольшую задержку для предотвращения спама кнопкой
      await Future.delayed(Duration(milliseconds: 200));
      
      if (_isWithinTargetRange) {
        setState(() {
          _capturedImage = File(file.path);
        });
      } else {
        _showDistanceAlert();
      }
    } catch (e) {
      print('Ошибка съемки: $e');
    }
  }

  void _showDistanceAlert() {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(
          'Пожалуйста, подойдите к целевому расстоянию: ${(widget.targetDistance * 100).toStringAsFixed(0)}см ± ${(widget.tolerance * 100).toStringAsFixed(0)}см'
        ),
        backgroundColor: Colors.orange,
        duration: Duration(seconds: 2),
      ),
    );
  }

  @override
  void dispose() {
    _distanceTimer?.cancel();
    _arSession?.dispose();
    _cameraController?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: _isCameraInitialized
            ? CameraPreview(_cameraController!)
            : Center(child: CircularProgressIndicator()),
        ),
        
        Container(
          padding: EdgeInsets.all(16),
          child: Column(
            children: [
              Text(
                'Расстояние: ${_currentDistance >= 0 ? (_currentDistance * 100).toStringAsFixed(1) : '--'}см',
                style: TextStyle(fontSize: 18),
              ),
              
              SizedBox(height: 8),
              
              ElevatedButton(
                onPressed: _isWithinTargetRange ? _captureImage : null,
                style: ElevatedButton.styleFrom(
                  backgroundColor: _isWithinTargetRange ? Colors.green : Colors.grey,
                  minimumSize: Size(double.infinity, 48),
                ),
                child: Text(
                  _isWithinTargetRange 
                    ? 'Сделать снимок' 
                    : 'Подойдите на ${(widget.targetDistance * 100).toStringAsFixed(0)}см',
                ),
              ),
              
              if (_capturedImage != null) ...[
                SizedBox(height: 16),
                Image.file(_capturedImage!),
              ],
            ],
          ),
        ),
      ],
    );
  }
}

Источники

  1. GitHub - codingcafe1/ArDistanceTrackerApp - Приложение для измерения AR на Flutter 2.0 с использованием плагина Apple ARKit
  2. arkit_plugin | Flutter package - Официальная документация плагина ARKit
  3. Поддерживает ли flutter одновременно плагины ARCore и ARKit в одном приложении? - Обсуждение на Stack Overflow по кроссплатформенной интеграции AR
  4. Как измерять расстояние с помощью ARCore? - Техники измерения расстояния ARCore
  5. Обнаружение объектов в реальном времени во Flutter - Подход компьютерного зрения для обнаружения объектов
  6. Умная камера: Обнаружение объектов во Flutter - Руководство по интеграции TensorFlow Lite
  7. google_mlkit_object_detection | Flutter package - Плагин обнаружения объектов Google ML Kit

Заключение

Для надежного измерения расстояния и управления состоянием кнопки съемки в вашем приложении Flutter:

  1. Используйте ARKit/ARCore для наиболее точных измерений расстояния, особенно для производственных приложений, требующих точности
  2. Реализуйте непрерывный мониторинг с ограниченными интервалами измерений для поддержания производительности
  3. Добавьте визуальную обратную связь для руководства пользователей к оптимальному расстоянию перед съемкой
  4. Включите обработку отката для устройств без возможностей AR
  5. Тщательно тестируйте при различных условиях освещения и типах объектов

AR-подход обеспечивает наилучшую точность измерения расстояния, в то время как методы компьютерного зрения предлагают более широкую совместимость с устройствами. Для вашего конкретного случая использования я рекомендую начать с реализации ARKit/ARCore и добавление компьютерного зрения отката при необходимости.

Хотите, чтобы я подробнее рассказал о каком-либо конкретном аспекте реализации, таком как обработка различных возможностей устройств или оптимизация производительности для конкретных случаев использования?

Авторы
Проверено модерацией
Модерация