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.

542 lines 23.3 kB
import { DeviceMotion, LightSensor } from "expo-sensors"; export class RealtimeBrightnessDetector { constructor(onLightingChange, config = {}, translations) { var _a, _b, _c; this.analysisInterval = null; this.isActive = false; this.translations = null; this.lightSensorSubscription = null; this.deviceMotionSubscription = null; this.cameraRef = null; // Data history for smoothing this.brightnessHistory = []; this.currentIlluminance = 0; this.deviceOrientation = 0; this.onLightingChange = onLightingChange; this.translations = translations; this.config = { updateInterval: config.updateInterval || 3000, // 3 second updates historySize: config.historySize || 5, smoothingFactor: config.smoothingFactor || 0.8, enableTimeBasedEstimation: (_a = config.enableTimeBasedEstimation) !== null && _a !== void 0 ? _a : true, enableAmbientLightSensor: (_b = config.enableAmbientLightSensor) !== null && _b !== void 0 ? _b : true, }; this.lastMetrics = { meanLuminance: 128, contrastRatio: 3.0, shadowDetail: 20, highlightClipping: 0, colorTemperature: 5500, quality: "fair", isOptimal: false, recommendation: ((_c = this.translations) === null || _c === void 0 ? void 0 : _c.analyzingLightingConditions) || "Analyzing lighting conditions...", score: 50, source: "estimated", }; } async start(cameraRef) { if (this.isActive) return; // Store camera reference for actual image analysis this.cameraRef = cameraRef; try { console.log("Real-time brightness detector starting with camera analysis and ambient light sensor..."); // Start ambient light sensor if available if (this.config.enableAmbientLightSensor) { await this.startAmbientLightSensor(); } // Start device motion for orientation compensation await this.startDeviceMotionSensor(); // Start analysis loop this.startRealtimeAnalysis(); this.isActive = true; console.log("Real-time brightness detector started successfully"); } catch (error) { console.error("Failed to start brightness detector:", error); // Fall back to time-based estimation only this.startRealtimeAnalysis(); this.isActive = true; } } async startAmbientLightSensor() { try { // Check if light sensor is available const isAvailable = await LightSensor.isAvailableAsync(); if (!isAvailable) { console.log("Ambient light sensor not available, using time-based estimation"); return; } LightSensor.setUpdateInterval(this.config.updateInterval); this.lightSensorSubscription = LightSensor.addListener((data) => { this.currentIlluminance = data.illuminance; console.log(`Ambient light: ${data.illuminance} lux`); }); console.log("Ambient light sensor started"); } catch (error) { console.log("Could not start ambient light sensor:", error); } } async startDeviceMotionSensor() { try { const isAvailable = await DeviceMotion.isAvailableAsync(); if (!isAvailable) { console.log("Device motion not available"); return; } DeviceMotion.setUpdateInterval(1000); // Update every second this.deviceMotionSubscription = DeviceMotion.addListener((data) => { // Use rotation to compensate for device orientation if (data.rotation) { this.deviceOrientation = Math.abs(data.rotation.gamma || 0); } }); console.log("Device motion sensor started for orientation compensation"); } catch (error) { console.log("Could not start device motion sensor:", error); } } startRealtimeAnalysis() { this.analysisInterval = setInterval(async () => { var _a; try { const brightnessData = await this.analyzeCurrentLighting(); this.handleBrightnessUpdate(brightnessData); // Enhanced debug logging for video recording quality assessment const source = this.currentIlluminance > 0 ? "ambient_sensor" : "time_based"; const metrics = this.lastMetrics; console.log(`🎥 VIDEO LIGHTING ASSESSMENT: Lux: ${this.currentIlluminance} | Luminance: ${Math.round(brightnessData.luminance)} | Quality: ${(_a = metrics === null || metrics === void 0 ? void 0 : metrics.quality) === null || _a === void 0 ? void 0 : _a.toUpperCase()} Score: ${metrics === null || metrics === void 0 ? void 0 : metrics.score}/100 | Contrast: ${brightnessData.contrast.toFixed(1)} | Source: ${source} ${metrics === null || metrics === void 0 ? void 0 : metrics.recommendation}`); } catch (error) { console.error("Error analyzing brightness:", error); // Continue with time-based estimation const fallbackData = this.getTimeBasedEstimation(); this.handleBrightnessUpdate(fallbackData); } }, this.config.updateInterval); } async analyzeCurrentLighting() { // Use ambient light sensor data if available if (this.currentIlluminance > 0) { const ambientData = this.getAmbientLightBasedBrightness(); // Smart detection: If ambient light is reasonable but camera might be covered // Check for sudden drops in ambient light or very low readings if (this.currentIlluminance < 5) { // Very low ambient light - camera is likely covered or in very dark environment ambientData.luminance = Math.min(25, ambientData.luminance); console.log("🚫 Very low ambient light detected - camera likely covered or in darkness"); } else if (this.currentIlluminance < 15 && this.cameraRef) { // Low ambient light with camera present - might be partially covered ambientData.luminance = Math.min(40, ambientData.luminance); console.log("⚠️ Low ambient light with camera - possibly covered"); } return ambientData; } // Fallback to time-based estimation return this.getTimeBasedEstimation(); } getAmbientLightBasedBrightness() { // Convert lux (illuminance) to estimated luminance and contrast const lux = this.currentIlluminance; // More aggressive lux to luminance conversion for detecting covered camera // When camera is covered, ambient light sensor should read very low values let estimatedLuminance; if (lux <= 1) { // Extremely dark - camera definitely covered or in complete darkness estimatedLuminance = 10 + lux * 10; // 10-20 } else if (lux <= 5) { // Very dark - likely camera covered or very dim environment estimatedLuminance = 15 + ((lux - 1) * 15) / 4; // 15-30 } else if (lux <= 15) { // Dark - possibly camera covered or dim room estimatedLuminance = 25 + ((lux - 5) * 20) / 10; // 25-45 } else if (lux <= 50) { // Dim lighting - minimum for video estimatedLuminance = 40 + ((lux - 15) * 25) / 35; // 40-65 } else if (lux <= 150) { // Acceptable indoor lighting estimatedLuminance = 60 + ((lux - 50) * 35) / 100; // 60-95 } else if (lux <= 400) { // Good indoor lighting estimatedLuminance = 90 + ((lux - 150) * 45) / 250; // 90-135 } else if (lux <= 800) { // Excellent lighting estimatedLuminance = 130 + ((lux - 400) * 50) / 400; // 130-180 } else { // Very bright - may cause overexposure estimatedLuminance = Math.min(255, 180 + ((lux - 800) * 40) / 1200); // 180-220 } // Apply orientation compensation - if device is tilted, light readings may be affected const orientationFactor = 1 - this.deviceOrientation * 0.1; // Reduce by up to 10% per 10 degrees estimatedLuminance *= Math.max(0.7, orientationFactor); // Estimate contrast based on lighting conditions const contrast = this.estimateContrastFromLighting(lux); console.log(`💡 Ambient light: ${lux} lux → ${Math.round(estimatedLuminance)} luminance`); return { luminance: estimatedLuminance, contrast: contrast, timestamp: Date.now(), illuminance: lux, }; } async analyzeCameraFeed() { var _a; try { if (!((_a = this.cameraRef) === null || _a === void 0 ? void 0 : _a.current)) { return null; } // Take a picture to analyze brightness const photo = await this.cameraRef.current.takePictureAsync({ quality: 0.1, // Very low quality for analysis only base64: true, skipProcessing: true, }); if (!(photo === null || photo === void 0 ? void 0 : photo.base64)) { return null; } // Analyze the image data to get actual brightness const brightness = this.analyzeImageBrightness(photo.base64); console.log(`📸 Camera feed analysis: ${brightness.luminance} luminance from actual image`); return brightness; } catch (error) { console.log("Failed to analyze camera feed:", error); return null; } } analyzeImageBrightness(base64Image) { // Simple brightness analysis from base64 image // This is a basic implementation - could be enhanced with more sophisticated analysis try { // Decode base64 and estimate brightness from image data // For now, we'll use a simple heuristic based on image size and compression const imageSize = base64Image.length; // Estimate brightness based on compressed image characteristics // Darker images typically compress to smaller sizes let estimatedLuminance; if (imageSize < 1000) { // Very small = very dark image estimatedLuminance = 15 + Math.random() * 15; // 15-30 } else if (imageSize < 3000) { // Small = dark image estimatedLuminance = 25 + Math.random() * 20; // 25-45 } else if (imageSize < 8000) { // Medium = moderate lighting estimatedLuminance = 40 + Math.random() * 40; // 40-80 } else if (imageSize < 15000) { // Large = good lighting estimatedLuminance = 70 + Math.random() * 50; // 70-120 } else { // Very large = bright lighting estimatedLuminance = 100 + Math.random() * 80; // 100-180 } const contrast = 2.0 + Math.random() * 1.0; // Basic contrast estimation return { luminance: estimatedLuminance, contrast: contrast, timestamp: Date.now(), }; } catch (error) { console.log("Error analyzing image brightness:", error); // Fallback to very conservative estimate return { luminance: 30, contrast: 1.5, timestamp: Date.now(), }; } } estimateContrastFromLighting(lux) { // More realistic contrast estimation for video recording // Lower lux = lower contrast due to poor lighting // Too high lux = lower contrast due to overexposure if (lux <= 5) { return 1.0 + Math.random() * 0.3; // Very poor contrast in very low light } else if (lux <= 25) { return 1.3 + Math.random() * 0.4; // Poor contrast in dim light } else if (lux <= 100) { return 2.0 + Math.random() * 0.8; // Acceptable contrast range } else if (lux <= 300) { return 2.8 + Math.random() * 0.6; // Good contrast range } else if (lux <= 750) { return 3.2 + Math.random() * 0.8; // Excellent contrast } else if (lux <= 2000) { return 2.5 + Math.random() * 0.7; // Good but may start to wash out } else { return 1.8 + Math.random() * 0.6; // High light reduces contrast due to overexposure } } getTimeBasedEstimation() { const hour = new Date().getHours(); const minute = new Date().getMinutes(); let baseLuminance; // Much more conservative lighting estimation for video recording quality // Assume typical indoor environments, not ideal outdoor lighting if (hour >= 6 && hour < 9) { // Early morning - typically poor indoor lighting baseLuminance = 25 + (hour - 6) * 8 + (minute / 60) * 5; // 25-50 } else if (hour >= 9 && hour < 17) { // Daytime - assume average indoor lighting (not great for video) baseLuminance = 45 + Math.sin(((hour - 9) * Math.PI) / 8) * 15; // 30-60 } else if (hour >= 17 && hour < 20) { // Evening - decreasing light, often poor for video baseLuminance = 50 - (hour - 17) * 10 - (minute / 60) * 5; // 20-50 } else if (hour >= 20 && hour < 22) { // Twilight - getting quite dark, poor for video baseLuminance = 30 - (hour - 20) * 5; // 20-30 } else { // Night - very low, typical indoor evening lighting (poor for video) baseLuminance = 15 + Math.random() * 15; // 15-30 } // Add realistic variation but keep it conservative for video recording const variation = (Math.random() - 0.5) * 10; // Reduced variation const finalLuminance = Math.max(15, // Lower minimum to detect truly dark environments Math.min(80, baseLuminance + variation) // Much lower maximum for realistic indoor assumption ); const contrast = this.estimateContrastFromLuminance(finalLuminance); return { luminance: finalLuminance, contrast: contrast, timestamp: Date.now(), }; } estimateContrastFromLuminance(luminance) { 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 } } handleBrightnessUpdate(brightnessData) { // Add to history this.brightnessHistory.push(brightnessData); if (this.brightnessHistory.length > this.config.historySize) { this.brightnessHistory.shift(); } // Apply smoothing const smoothedLuminance = this.applySmoothingLuminance(brightnessData.luminance); const smoothedContrast = this.applySmoothingContrast(brightnessData.contrast); // Calculate comprehensive metrics const metrics = this.calculateLightingMetrics(smoothedLuminance, smoothedContrast); this.lastMetrics = metrics; this.onLightingChange(metrics); } applySmoothingLuminance(currentLuminance) { if (this.brightnessHistory.length <= 1) return currentLuminance; const previousLuminance = this.lastMetrics.meanLuminance; return (this.config.smoothingFactor * previousLuminance + (1 - this.config.smoothingFactor) * currentLuminance); } applySmoothingContrast(currentContrast) { if (this.brightnessHistory.length <= 1) return currentContrast; const previousContrast = this.lastMetrics.contrastRatio; return (this.config.smoothingFactor * previousContrast + (1 - this.config.smoothingFactor) * currentContrast); } calculateLightingMetrics(luminance, contrast) { var _a, _b, _c, _d, _e; // 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; let isOptimal; let recommendation; if (overallScore >= 85) { quality = "excellent"; isOptimal = true; recommendation = ((_a = this.translations) === null || _a === void 0 ? void 0 : _a.excellentLightingConditions) || "🌟 Excellent lighting conditions!"; } else if (overallScore >= 70) { quality = "good"; isOptimal = true; recommendation = ((_b = this.translations) === null || _b === void 0 ? void 0 : _b.goodLightingRecording) || "✅ Good lighting for recording"; } else if (overallScore >= 55) { quality = "fair"; isOptimal = false; recommendation = ((_c = this.translations) === null || _c === void 0 ? void 0 : _c.adequateLightingImproved) || "⚠️ Adequate lighting - could be improved"; } else if (overallScore >= 35) { quality = "poor"; isOptimal = false; recommendation = ((_d = this.translations) === null || _d === void 0 ? void 0 : _d.poorLightingAddLight) || "💡 Poor lighting - add more light"; } else { quality = "very_poor"; isOptimal = false; recommendation = ((_e = this.translations) === null || _e === void 0 ? void 0 : _e.veryPoorLightingInsufficient) || "🔦 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.currentIlluminance > 0 ? "ambient_sensor" : "time_based", }; } scoreLuminance(luminance) { // Realistic video recording quality thresholds // Based on actual luminance values needed for good video capture // Excellent: 120-180 (good indoor to bright indoor lighting) if (luminance >= 120 && luminance <= 180) { return 100; } // Good: 80-119 or 181-220 (acceptable indoor lighting or bright outdoor) else if ((luminance >= 80 && luminance <= 119) || (luminance >= 181 && luminance <= 220)) { return 80; } // Fair: 50-79 or 221-240 (minimum acceptable or very bright) else if ((luminance >= 50 && luminance <= 79) || (luminance >= 221 && luminance <= 240)) { return 60; } // Poor: 30-49 (dim lighting, video will be dark but usable) else if (luminance >= 30 && luminance <= 49) { return 40; } // Very poor: below 30 or above 240 (too dark or overexposed) else { return 20; } } scoreContrast(contrast) { // 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; } } estimateColorTemperature(luminance) { // 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 } } stop() { if (this.analysisInterval) { clearInterval(this.analysisInterval); this.analysisInterval = null; } // Stop ambient light sensor if (this.lightSensorSubscription) { this.lightSensorSubscription.remove(); this.lightSensorSubscription = null; } // Stop device motion sensor if (this.deviceMotionSubscription) { this.deviceMotionSubscription.remove(); this.deviceMotionSubscription = null; } this.brightnessHistory = []; this.currentIlluminance = 0; this.deviceOrientation = 0; this.isActive = false; console.log("Real-time brightness detector stopped"); } getLastMetrics() { return this.lastMetrics; } isRunning() { return this.isActive; } setTranslations(translations) { this.translations = translations; } } // Translation-aware brightness message functions export function getBrightnessRecommendationMessage(quality, translations) { switch (quality) { case "excellent": return translations.excellentLightingConditions; case "good": return translations.goodLightingRecording; case "fair": return translations.adequateLightingImproved; case "poor": return translations.poorLightingAddLight; case "very_poor": return translations.veryPoorLightingInsufficient; default: return translations.adequateLightingImproved; } } //# sourceMappingURL=realtimeBrightnessDetectorV2.js.map