react-native-guided-camera
Version:
A React Native component for agricultural camera guidance with sensor-based motion detection, orientation tracking, and real-time feedback.
374 lines (320 loc) • 11.3 kB
text/typescript
import { Gyroscope, Accelerometer } from "expo-sensors";
export interface MotionMetrics {
score: number; // Overall stability score (0-100)
isStable: boolean; // Whether device is stable enough for recording
stability: "excellent" | "good" | "fair" | "poor" | "very_poor";
accelerationMagnitude: number; // Current acceleration magnitude
rotationMagnitude: number; // Current rotation magnitude
recommendation: string;
source: "gyroscope" | "accelerometer" | "hybrid" | "mock";
}
export interface MotionDetectorConfig {
updateInterval?: number;
historySize?: number;
excellentThreshold?: number;
goodThreshold?: number;
fairThreshold?: number;
poorThreshold?: number;
accelerationWeight?: number;
rotationWeight?: number;
smoothingFactor?: number;
enableSensorFusion?: boolean;
}
interface GyroscopeData {
x: number;
y: number;
z: number;
timestamp: number;
}
interface AccelerometerData {
x: number;
y: number;
z: number;
timestamp: number;
}
export class MotionDetector {
private gyroSubscription: any = null;
private accelSubscription: any = null;
private mockInterval: any = null;
private isActive = false;
private config: Required<MotionDetectorConfig>;
private onMotionChange: (metrics: MotionMetrics) => void;
// Data history for smoothing and sensor fusion
private gyroscopeHistory: GyroscopeData[] = [];
private accelerometerHistory: AccelerometerData[] = [];
private stabilityHistory: number[] = [];
private lastMetrics: MotionMetrics;
constructor(
onMotionChange: (metrics: MotionMetrics) => void,
config: Partial<MotionDetectorConfig> = {}
) {
this.onMotionChange = onMotionChange;
this.config = {
updateInterval: config.updateInterval || 100,
historySize: config.historySize || 10,
excellentThreshold: config.excellentThreshold || 85,
goodThreshold: config.goodThreshold || 70,
fairThreshold: config.fairThreshold || 50,
poorThreshold: config.poorThreshold || 30,
accelerationWeight: config.accelerationWeight || 0.6,
rotationWeight: config.rotationWeight || 0.4,
smoothingFactor: config.smoothingFactor || 0.7,
enableSensorFusion: config.enableSensorFusion ?? true,
};
this.lastMetrics = {
score: 100,
isStable: true,
stability: "excellent",
accelerationMagnitude: 0,
rotationMagnitude: 0,
recommendation: "Perfect stability!",
source: "gyroscope",
};
}
public async start(): Promise<void> {
if (this.isActive) return;
try {
console.log("Motion detector starting with config:", this.config);
// Try to start gyroscope first
const gyroStarted = await this.startGyroscopeTracking();
// Try accelerometer if sensor fusion is enabled
if (this.config.enableSensorFusion) {
await this.startAccelerometerTracking();
}
// If no real sensors available, use mock data
if (!gyroStarted) {
console.warn("No motion sensors available - using mock data");
this.startMockData();
}
this.isActive = true;
console.log("Motion detector started successfully");
} catch (error) {
console.error("Failed to start motion detector:", error);
console.log("Falling back to mock motion data");
this.startMockData();
this.isActive = true;
}
}
private async startGyroscopeTracking(): Promise<boolean> {
try {
// Check if gyroscope is available
const isAvailable = await Gyroscope.isAvailableAsync();
if (!isAvailable) {
console.warn("Gyroscope not available on this device");
return false;
}
// Request gyroscope permissions
const { status } = await Gyroscope.requestPermissionsAsync();
if (status !== "granted") {
console.warn("Gyroscope permission not granted");
return false;
}
console.log("Starting gyroscope tracking...");
Gyroscope.setUpdateInterval(this.config.updateInterval);
this.gyroSubscription = Gyroscope.addListener((data) => {
const gyroData: GyroscopeData = {
x: data.x,
y: data.y,
z: data.z,
timestamp: Date.now(),
};
this.handleGyroscopeUpdate(gyroData);
});
return true;
} catch (error) {
console.error("Failed to start gyroscope tracking:", error);
return false;
}
}
private async startAccelerometerTracking(): Promise<boolean> {
try {
// Check if accelerometer is available
const isAvailable = await Accelerometer.isAvailableAsync();
if (!isAvailable) {
console.warn("Accelerometer not available for motion detection");
return false;
}
console.log("Starting accelerometer tracking...");
Accelerometer.setUpdateInterval(this.config.updateInterval);
this.accelSubscription = Accelerometer.addListener((data) => {
const accelData: AccelerometerData = {
x: data.x,
y: data.y,
z: data.z,
timestamp: Date.now(),
};
this.handleAccelerometerUpdate(accelData);
});
return true;
} catch (error) {
console.error("Failed to start accelerometer tracking:", error);
return false;
}
}
private startMockData(): void {
console.log("Starting mock motion data...");
this.mockInterval = setInterval(() => {
const mockGyroData: GyroscopeData = {
x: (Math.random() - 0.5) * 0.1, // Small random motion
y: (Math.random() - 0.5) * 0.1,
z: (Math.random() - 0.5) * 0.1,
timestamp: Date.now(),
};
this.handleGyroscopeUpdate(mockGyroData, "mock");
}, this.config.updateInterval);
}
private handleGyroscopeUpdate(
gyroData: GyroscopeData,
source: "gyroscope" | "mock" = "gyroscope"
): void {
// Add to gyroscope history
this.gyroscopeHistory.push(gyroData);
if (this.gyroscopeHistory.length > this.config.historySize) {
this.gyroscopeHistory.shift();
}
// Calculate rotation magnitude
const rotationMagnitude = Math.sqrt(
gyroData.x * gyroData.x +
gyroData.y * gyroData.y +
gyroData.z * gyroData.z
);
// Get acceleration magnitude from accelerometer if available
let accelerationMagnitude = 0;
let finalSource: MotionMetrics["source"] = source;
if (
this.config.enableSensorFusion &&
this.accelerometerHistory.length > 0
) {
const latestAccel =
this.accelerometerHistory[this.accelerometerHistory.length - 1];
accelerationMagnitude = Math.sqrt(
latestAccel.x * latestAccel.x +
latestAccel.y * latestAccel.y +
latestAccel.z * latestAccel.z
);
finalSource = source === "mock" ? "mock" : "hybrid";
}
// Calculate stability score
const stabilityScore = this.calculateStabilityScore(
rotationMagnitude,
accelerationMagnitude
);
// Apply smoothing
this.stabilityHistory.push(stabilityScore);
if (this.stabilityHistory.length > this.config.historySize) {
this.stabilityHistory.shift();
}
const smoothedScore = this.applySmoothingToScore(stabilityScore);
// Calculate final metrics
const metrics = this.calculateMotionMetrics(
smoothedScore,
rotationMagnitude,
accelerationMagnitude,
finalSource
);
this.lastMetrics = metrics;
this.onMotionChange(metrics);
}
private handleAccelerometerUpdate(accelData: AccelerometerData): void {
// Add to accelerometer history
this.accelerometerHistory.push(accelData);
if (this.accelerometerHistory.length > this.config.historySize) {
this.accelerometerHistory.shift();
}
}
private calculateStabilityScore(
rotation: number,
acceleration: number
): number {
// Weight rotation and acceleration components
const rotationScore = Math.max(0, 100 - rotation * 100);
const accelerationScore = Math.max(0, 100 - acceleration * 50);
// Combine scores with weights
const combinedScore =
rotationScore * this.config.rotationWeight +
accelerationScore * this.config.accelerationWeight;
return Math.max(0, Math.min(100, combinedScore));
}
private applySmoothingToScore(currentScore: number): number {
if (this.stabilityHistory.length === 0) return currentScore;
const previousSmoothed = this.lastMetrics.score;
return (
this.config.smoothingFactor * previousSmoothed +
(1 - this.config.smoothingFactor) * currentScore
);
}
private calculateMotionMetrics(
score: number,
rotationMagnitude: number,
accelerationMagnitude: number,
source: MotionMetrics["source"]
): MotionMetrics {
// Determine stability level
let stability: MotionMetrics["stability"];
let isStable: boolean;
let recommendation: string;
if (score >= this.config.excellentThreshold) {
stability = "excellent";
isStable = true;
recommendation = "Perfect stability! 🎯";
} else if (score >= this.config.goodThreshold) {
stability = "good";
isStable = true;
recommendation = "Good stability ✅";
} else if (score >= this.config.fairThreshold) {
stability = "fair";
isStable = true;
recommendation = "Fair stability ⚠️";
} else if (score >= this.config.poorThreshold) {
stability = "poor";
isStable = false;
recommendation = "Poor stability - steady your device ⚡";
} else {
stability = "very_poor";
isStable = false;
recommendation = "Very poor stability - hold device steady! 🔴";
}
return {
score: Math.round(score),
isStable,
stability,
accelerationMagnitude: Math.round(accelerationMagnitude * 1000) / 1000,
rotationMagnitude: Math.round(rotationMagnitude * 1000) / 1000,
recommendation,
source,
};
}
public stop(): void {
// Stop gyroscope
if (this.gyroSubscription) {
if (typeof this.gyroSubscription.remove === "function") {
this.gyroSubscription.remove();
}
this.gyroSubscription = null;
}
// Stop accelerometer
if (this.accelSubscription) {
if (typeof this.accelSubscription.remove === "function") {
this.accelSubscription.remove();
}
this.accelSubscription = null;
}
// Stop mock data
if (this.mockInterval) {
clearInterval(this.mockInterval);
this.mockInterval = null;
}
// Clear history
this.gyroscopeHistory = [];
this.accelerometerHistory = [];
this.stabilityHistory = [];
this.isActive = false;
console.log("Motion detector stopped");
}
public getLastMetrics(): MotionMetrics {
return this.lastMetrics;
}
public isRunning(): boolean {
return this.isActive;
}
}