Flutter: Руководство по захвату на основе расстояния
Узнайте, как измерять расстояние до объекта в Flutter и активировать кнопку захвата только в оптимальном диапазоне. Полное руководство по реализации ARKit/ARCore и компьютерного зрения.
Как можно надежно измерить расстояние до объекта и активировать кнопку захвата только тогда, когда расстояние находится в определенном диапазоне во Flutter?
Я создаю приложение Flutter, где пользователи могут делать снимки объектов, но я хочу, чтобы кнопка захвата была активна только тогда, когда камера находится на определенном расстоянии от объекта. Я использую плагин камеры для получения живого превью и мне нужна логика для оценки расстояния от камеры до объекта перед ней.
Моя текущая рабочая流程:
- Показывать живое превью камеры
- Непрерывно оценивать расстояние от камеры до объекта
- Активировать кнопку захвата только тогда, когда расстояние находится в целевом диапазоне
- Захватывать и сохранять/обрезать изображение, когда это разрешено
У меня есть следующий код для захвата изображений:
if (_capturedImage == null && _isCameraInitialized)
ElevatedButton(
onPressed: _captureImage,
child: const Text('Capture Image'),
),
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-измерение расстояния
- Альтернатива с компьютерным зрением
- Шаги реализации
- Методы оценки расстояния
- Управление состоянием кнопки
- Рекомендации по производительности
- Пример полной интеграции
AR-измерение расстояния
ARKit для iOS и ARCore для Android обеспечивают наиболее надежные возможности измерения расстояния в Flutter. Эти платформы используют датчики устройства для расчета реальных расстояний в реальном мире.
Рекомендуемые плагины
Для интеграции с iOS используйте arkit_plugin, который предоставляет доступ к возможностям Apple ARKit. Обратите внимание, что ARKit недоступен на Android - вам понадобится использовать ARCore.
Для кроссплатформенной поддержки вы можете объединить оба плагина в вашем приложении Flutter:
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 - метры, которые вы можете преобразовать в предпочитаемую единицу измерения:
// Расстояние в метрах
double distanceInMeters = arHitResult.distance;
// Преобразование в сантиметры
double distanceInCm = distanceInMeters * 100;
// Преобразование в дюймы
double distanceInInches = distanceInMeters * 39.37;
Альтернатива с компьютерным зрением
Если поддержка ARKit/ARCore недоступна или вам требуется резервное решение, подходы компьютерного зрения могут оценивать расстояние с помощью обнаружения объектов и калибровки камеры.
Обнаружение объектов с TensorFlow Lite
Используйте Flutter Vision или Google ML Kit для обнаружения объектов и оценки расстояния на основе их известных размеров:
dependencies:
camera: ^0.10.5+2
tflite_flutter: ^3.1.0
flutter_vision: ^0.0.9
Плагин camera может предоставлять потоки изображений, а Flutter Vision обеспечивает обнаружение объектов.
Примечание: Оценка расстояния с помощью компьютерного зрения менее точна, чем AR-методы, но работает на большем количестве устройств. Точность зависит от знания размера объекта и калибровки камеры.
Шаги реализации
Шаг 1: Инициализация AR-сессии
Для iOS (ARKit):
import 'package:arkit_plugin/arkit_plugin.dart';
void initializeARKit() async {
final arkitSession = ARKitSession();
await arkitSession.configure();
// Добавление обнаружения плоскостей для стабильных измерений
arkitSession.addConfiguration(ARKitWorldTrackingConfiguration(
planeDetection: ARKitPlaneDetection.horizontal,
));
}
Для Android (ARCore):
import 'package/arcore_flutter_plugin/arcore_flutter_plugin.dart';
void initializeARCore() async {
final arCoreController = ArCoreController(
onArCoreViewCreated: _onArCoreViewCreated,
enableTapRecognizer: true,
);
}
Шаг 2: Реализация отслеживания расстояния
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-сессии для непрерывной проверки расстояния:
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
Наиболее точный метод с использованием датчиков устройства:
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: Оценка на основе размера объекта
Для подхода компьютерного зрения, когда размеры объекта известны:
double estimateDistanceByObjectSize(
double objectHeightPixels,
double knownObjectHeightMeters,
double cameraFocalLengthPixels
) {
// Расстояние = (Известная высота объекта × Фокусное расстояние) / Высота объекта в пикселях
return (knownObjectHeightMeters * cameraFocalLengthPixels) / objectHeightPixels;
}
Управление состоянием кнопки
Измените существующую кнопку съемки для использования состояния на основе расстояния:
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)}см)',
),
),
Улучшенная функция съемки с проверкой расстояния
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,
),
);
}
Рекомендации по производительности
Оптимизация измерений расстояния
- Частота кадров: Расчеты расстояния должны выполняться с частотой 10-15 Гц, а не с полной частотой кадров камеры
- Ограничение: Реализуйте минимальный интервал между измерениями для избежания проблем с производительностью
- Обработка ошибок: Добавьте проверку недопустимых измерений и недоступности датчиков
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 недоступен
Пример полной интеграции
Вот полный виджет, интегрирующий съемку на основе расстояния:
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!),
],
],
),
),
],
);
}
}
Источники
- GitHub - codingcafe1/ArDistanceTrackerApp - Приложение для измерения AR на Flutter 2.0 с использованием плагина Apple ARKit
- arkit_plugin | Flutter package - Официальная документация плагина ARKit
- Поддерживает ли flutter одновременно плагины ARCore и ARKit в одном приложении? - Обсуждение на Stack Overflow по кроссплатформенной интеграции AR
- Как измерять расстояние с помощью ARCore? - Техники измерения расстояния ARCore
- Обнаружение объектов в реальном времени во Flutter - Подход компьютерного зрения для обнаружения объектов
- Умная камера: Обнаружение объектов во Flutter - Руководство по интеграции TensorFlow Lite
- google_mlkit_object_detection | Flutter package - Плагин обнаружения объектов Google ML Kit
Заключение
Для надежного измерения расстояния и управления состоянием кнопки съемки в вашем приложении Flutter:
- Используйте ARKit/ARCore для наиболее точных измерений расстояния, особенно для производственных приложений, требующих точности
- Реализуйте непрерывный мониторинг с ограниченными интервалами измерений для поддержания производительности
- Добавьте визуальную обратную связь для руководства пользователей к оптимальному расстоянию перед съемкой
- Включите обработку отката для устройств без возможностей AR
- Тщательно тестируйте при различных условиях освещения и типах объектов
AR-подход обеспечивает наилучшую точность измерения расстояния, в то время как методы компьютерного зрения предлагают более широкую совместимость с устройствами. Для вашего конкретного случая использования я рекомендую начать с реализации ARKit/ARCore и добавление компьютерного зрения отката при необходимости.
Хотите, чтобы я подробнее рассказал о каком-либо конкретном аспекте реализации, таком как обработка различных возможностей устройств или оптимизация производительности для конкретных случаев использования?