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
JavaScript
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