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.

367 lines (316 loc) 11.7 kB
import { DeviceMotion } from "expo-sensors"; export interface LightingMetrics { meanLuminance: number; contrastRatio: number; shadowDetail: number; highlightClipping: number; colorTemperature: number; quality: "excellent" | "good" | "fair" | "poor" | "very_poor"; isOptimal: boolean; recommendation: string; score: number; // 0-100 source: "realtime" | "mock" | "estimated"; } export interface RealtimeBrightnessConfig { updateInterval?: number; historySize?: number; smoothingFactor?: number; enableTimeBasedEstimation?: boolean; } interface BrightnessData { luminance: number; contrast: number; timestamp: number; } export class RealtimeBrightnessDetector { private analysisInterval: any = null; private isActive = false; private config: Required<RealtimeBrightnessConfig>; private onLightingChange: (metrics: LightingMetrics) => void; private cameraRef: any = null; // Data history for smoothing private brightnessHistory: BrightnessData[] = []; private lastMetrics: LightingMetrics; constructor( onLightingChange: (metrics: LightingMetrics) => void, config: Partial<RealtimeBrightnessConfig> = {} ) { this.onLightingChange = onLightingChange; this.config = { updateInterval: config.updateInterval || 2000, // 2 second updates historySize: config.historySize || 5, smoothingFactor: config.smoothingFactor || 0.8, enableTimeBasedEstimation: config.enableTimeBasedEstimation ?? true, }; this.lastMetrics = { meanLuminance: 128, contrastRatio: 3.0, shadowDetail: 20, highlightClipping: 0, colorTemperature: 5500, quality: "fair", isOptimal: false, recommendation: "Analyzing lighting conditions...", score: 50, source: "estimated", }; } public async start(cameraRef?: any): Promise<void> { if (this.isActive) return; this.cameraRef = cameraRef; try { console.log("Real-time brightness detector starting..."); this.startRealtimeAnalysis(); this.isActive = true; console.log("Real-time brightness detector started successfully"); } catch (error) { console.error("Failed to start brightness detector:", error); this.isActive = true; } } private startRealtimeAnalysis(): void { this.analysisInterval = setInterval(async () => { try { const brightnessData = await this.analyzeCurrentLighting(); this.handleBrightnessUpdate(brightnessData); } catch (error) { console.error("Error analyzing brightness:", error); // Continue with time-based estimation const fallbackData = this.getTimeBasedEstimation(); this.handleBrightnessUpdate(fallbackData); } }, this.config.updateInterval); } private async analyzeCurrentLighting(): Promise<BrightnessData> { // If camera reference is available, try to analyze the preview if (this.cameraRef?.current) { try { // Try to get a quick preview analysis without taking a full picture const previewData = await this.getPreviewBrightness(); if (previewData) { return previewData; } } catch (error) { console.log("Preview analysis failed, using estimation"); } } // Fallback to intelligent estimation return this.getTimeBasedEstimation(); } private async getPreviewBrightness(): Promise<BrightnessData | null> { try { // Take a very small, low quality image for analysis only const image = await this.cameraRef.current.takePictureAsync({ quality: 0.1, // Very low quality for speed base64: true, skipProcessing: true, }); if (image?.base64) { const brightness = await this.analyzeImageBrightness(image.base64); return brightness; } } catch (error) { console.log("Quick image analysis failed:", error); } return null; } private async analyzeImageBrightness( base64Image: string ): Promise<BrightnessData> { // Convert base64 to analyze brightness // This is a simplified analysis - in production you'd use more sophisticated methods // Estimate brightness from base64 length and characteristics const imageSize = base64Image.length; const compressionRatio = imageSize / 1000; // Rough estimate // Dark images compress better (smaller size), bright images are larger let estimatedLuminance = Math.min(255, Math.max(30, compressionRatio * 15)); // Analyze base64 content for more clues const brightChars = (base64Image.match(/[M-Z]/g) || []).length; const darkChars = (base64Image.match(/[A-L]/g) || []).length; const brightRatio = brightChars / (brightChars + darkChars); // Adjust luminance based on character analysis estimatedLuminance = estimatedLuminance * 0.7 + brightRatio * 200 * 0.3; // Estimate contrast from luminance variation const contrast = this.estimateContrastFromLuminance(estimatedLuminance); return { luminance: estimatedLuminance, contrast: contrast, timestamp: Date.now(), }; } private getTimeBasedEstimation(): BrightnessData { const hour = new Date().getHours(); const minute = new Date().getMinutes(); let baseLuminance: number; // More realistic lighting estimation based on time if (hour >= 6 && hour < 9) { // Early morning - gradually increasing baseLuminance = 60 + (hour - 6) * 25 + (minute / 60) * 15; } else if (hour >= 9 && hour < 17) { // Daytime - bright but with some variation baseLuminance = 140 + Math.sin(((hour - 9) * Math.PI) / 8) * 40; } else if (hour >= 17 && hour < 20) { // Evening - gradually decreasing baseLuminance = 120 - (hour - 17) * 20 - (minute / 60) * 15; } else if (hour >= 20 && hour < 22) { // Twilight baseLuminance = 70 - (hour - 20) * 15; } else { // Night - very low baseLuminance = 40 + Math.random() * 20; } // Add some realistic variation const variation = (Math.random() - 0.5) * 30; const finalLuminance = Math.max( 25, Math.min(255, baseLuminance + variation) ); const contrast = this.estimateContrastFromLuminance(finalLuminance); return { luminance: finalLuminance, contrast: contrast, timestamp: Date.now(), }; } private estimateContrastFromLuminance(luminance: number): number { if (luminance > 180) { return 1.8 + Math.random() * 0.8; // Bright = often low contrast } else if (luminance > 120) { return 2.5 + Math.random() * 1.0; // Good lighting = good contrast } else if (luminance > 80) { return 2.0 + Math.random() * 1.2; // Dim = variable contrast } else { return 1.2 + Math.random() * 0.6; // Dark = poor contrast } } private handleBrightnessUpdate(brightnessData: BrightnessData): void { // Add to history this.brightnessHistory.push(brightnessData); if (this.brightnessHistory.length > this.config.historySize) { this.brightnessHistory.shift(); } // Apply smoothing const smoothedLuminance = this.applySmoothing(brightnessData.luminance); const smoothedContrast = this.applySmoothing(brightnessData.contrast); // Calculate comprehensive metrics const metrics = this.calculateLightingMetrics( smoothedLuminance, smoothedContrast ); this.lastMetrics = metrics; this.onLightingChange(metrics); } private applySmoothing(currentValue: number): number { if (this.brightnessHistory.length <= 1) return currentValue; const previousValue = this.lastMetrics.meanLuminance; return ( this.config.smoothingFactor * previousValue + (1 - this.config.smoothingFactor) * currentValue ); } private calculateLightingMetrics( luminance: number, contrast: number ): LightingMetrics { // Calculate individual quality scores const luminanceScore = this.scoreLuminance(luminance); const contrastScore = this.scoreContrast(contrast); // Estimate other metrics const shadowDetail = Math.max(0, Math.min(50, (luminance - 50) * 0.4)); const highlightClipping = luminance > 220 ? (luminance - 220) * 0.5 : 0; const colorTemperature = this.estimateColorTemperature(luminance); // Overall score const overallScore = (luminanceScore + contrastScore) / 2; // Determine quality level and recommendations let quality: LightingMetrics["quality"]; let isOptimal: boolean; let recommendation: string; if (overallScore >= 85) { quality = "excellent"; isOptimal = true; recommendation = "🌟 Excellent lighting conditions!"; } else if (overallScore >= 70) { quality = "good"; isOptimal = true; recommendation = "✅ Good lighting for recording"; } else if (overallScore >= 55) { quality = "fair"; isOptimal = false; recommendation = "⚠️ Adequate lighting - could be improved"; } else if (overallScore >= 35) { quality = "poor"; isOptimal = false; recommendation = "💡 Poor lighting - add more light"; } else { quality = "very_poor"; isOptimal = false; recommendation = "🔦 Very poor lighting - insufficient for recording"; } return { meanLuminance: Math.round(luminance), contrastRatio: Math.round(contrast * 10) / 10, shadowDetail: Math.round(shadowDetail), highlightClipping: Math.round(highlightClipping), colorTemperature: Math.round(colorTemperature), quality, isOptimal, recommendation, score: Math.round(overallScore), source: this.cameraRef ? "realtime" : "estimated", }; } private scoreLuminance(luminance: number): number { // Optimal range: 120-180 if (luminance >= 120 && luminance <= 180) { return 100; } else if (luminance >= 100 && luminance <= 200) { return 80; } else if (luminance >= 80 && luminance <= 220) { return 60; } else if (luminance >= 60 && luminance <= 240) { return 40; } else { return 20; } } private scoreContrast(contrast: number): number { // Optimal range: 2.0-4.0 if (contrast >= 2.0 && contrast <= 4.0) { return 100; } else if (contrast >= 1.5 && contrast <= 5.0) { return 80; } else if (contrast >= 1.2 && contrast <= 6.0) { return 60; } else { return 40; } } private estimateColorTemperature(luminance: number): number { // Estimate color temperature based on brightness // This is a very rough estimation if (luminance > 180) { return 6500; // Bright daylight } else if (luminance > 120) { return 5500; // Good daylight } else if (luminance > 80) { return 4500; // Indoor/cloudy } else { return 3500; // Warm indoor lighting } } public stop(): void { if (this.analysisInterval) { clearInterval(this.analysisInterval); this.analysisInterval = null; } this.brightnessHistory = []; this.isActive = false; console.log("Real-time brightness detector stopped"); } public getLastMetrics(): LightingMetrics { return this.lastMetrics; } public isRunning(): boolean { return this.isActive; } }