UNPKG

react-native-guided-camera

Version:

A React Native component for agricultural camera guidance with sensor-based motion detection, orientation tracking, and real-time feedback.

165 lines (143 loc) 4.4 kB
import { Accelerometer } from "expo-sensors"; export interface AngleMetrics { roll: number; // left/right tilt, -90 to 90 pitch: number; // back/front tilt, -90 to 90 isLevel: boolean; direction: | "level" | "tilt_left" | "tilt_right" | "tilt_forward" | "tilt_backward"; severity: "good" | "minor" | "major"; } export interface PitchDetectorConfig { rollTolerance?: number; pitchTolerance?: number; pitchVertical?: number; updateInterval?: number; } export class PitchDetector { private subscription: any = null; private config: Required<PitchDetectorConfig>; private onAngleChange: (metrics: AngleMetrics) => void; constructor( onAngleChange: (metrics: AngleMetrics) => void, config: PitchDetectorConfig = {} ) { this.onAngleChange = onAngleChange; this.config = { rollTolerance: config.rollTolerance || 15, pitchTolerance: config.pitchTolerance || 15, pitchVertical: config.pitchVertical || 90, updateInterval: config.updateInterval || 100, }; } public start(): void { if (this.subscription) { this.stop(); } Accelerometer.setUpdateInterval(this.config.updateInterval); this.subscription = Accelerometer.addListener((accelerometerData) => { const metrics = this.calculateAngleMetrics(accelerometerData); this.onAngleChange(metrics); }); } public stop(): void { if (this.subscription) { this.subscription.remove(); this.subscription = null; } } private calculateAngleMetrics(accelerometerData: { x: number; y: number; z: number; }): AngleMetrics { const { x, y, z } = accelerometerData; // Roll: left/right tilt (rotation around y axis) const roll = Math.atan2(x, Math.sqrt(y * y + z * z)) * (180 / Math.PI); // Pitch: front/back tilt (rotation around x axis) const pitch = Math.atan2(y, Math.sqrt(x * x + z * z)) * (180 / Math.PI); // isLevel: roll near 0, pitch near +90 (upright) const isLevel = Math.abs(roll) < this.config.rollTolerance && Math.abs(Math.abs(pitch) - this.config.pitchVertical) < this.config.pitchTolerance; // Determine direction and severity let direction: AngleMetrics["direction"] = "level"; let severity: AngleMetrics["severity"] = "good"; if (!isLevel) { // If pitch is not vertical enough, suggest forward/backward if ( Math.abs(Math.abs(pitch) - this.config.pitchVertical) > this.config.pitchTolerance ) { direction = pitch > 0 ? "tilt_backward" : "tilt_forward"; // Severity based on how far from vertical const pitchDelta = Math.abs( Math.abs(pitch) - this.config.pitchVertical ); if (pitchDelta > 45) { severity = "major"; } else if (pitchDelta > 25) { severity = "minor"; } } else { // Otherwise, it's a roll (side tilt) issue direction = roll > 0 ? "tilt_right" : "tilt_left"; if (Math.abs(roll) > 45) { severity = "major"; } else if (Math.abs(roll) > 25) { severity = "minor"; } } } return { roll, pitch, isLevel, direction, severity, }; } } // Utility functions for angle calculations export const calculateAngleColor = ( severity: AngleMetrics["severity"] ): string => { switch (severity) { case "good": return "#4CAF50"; case "minor": return "#FF9800"; case "major": return "#F44336"; default: return "#9E9E9E"; } }; export const getAngleMessage = (metrics: AngleMetrics): string => { if (metrics.isLevel) { return "✓ Great! Keep it steady"; } let directionMsg = ""; switch (metrics.direction) { case "tilt_left": directionMsg = "Tilt Right"; break; case "tilt_right": directionMsg = "Tilt Left"; break; case "tilt_forward": directionMsg = "Tilt Back"; break; case "tilt_backward": directionMsg = "Tilt Forward"; break; default: directionMsg = "Adjust"; } const severity = metrics.severity === "major" ? " (Adjust)" : ""; return `${directionMsg}${severity}`; };