CropAI / backend /spectroscopy.py
bapi143's picture
Deploy CropAI Backend
5fd7795
Raw History Blame Contribute Delete
15.1 kB
"""
CropAI Multi-Stage Botanical Leaf Gatekeeper & Optical Spectroscopy Engine
Performs botanical validation (HSV, ExG, Specular Glare Compensation, Sobel Vein Texture)
to strictly reject non-plant objects (walls, paper, human skin, laptop screens, soil, wood)
and computes true on-leaf Chlorophyll, GLI, VARI & NGRDI with pre-symptomatic stress alerts.
"""
import io
import numpy as np
from PIL import Image
from typing import Dict, Any, Tuple
class OpticalSpectroscopyEngine:
"""Multi-Stage Botanical Leaf Gatekeeper & Optical Spectroscopy Engine."""
@staticmethod
def _rgb_to_hsv(r: np.ndarray, g: np.ndarray, b: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""Converts normalized [0, 1] RGB channels to HSV (H in degrees [0, 360], S & V in [0, 1])."""
max_c = np.maximum(np.maximum(r, g), b)
min_c = np.minimum(np.minimum(r, g), b)
delta = max_c - min_c + 1e-6
# Value (Brightness)
v = max_c
# Saturation
s = np.where(max_c > 1e-5, delta / (max_c + 1e-6), 0.0)
# Hue
h = np.zeros_like(r)
mask_r = (max_c == r) & (delta > 1e-5)
mask_g = (max_c == g) & (delta > 1e-5)
mask_b = (max_c == b) & (delta > 1e-5)
h[mask_r] = 60.0 * (((g[mask_r] - b[mask_r]) / delta[mask_r]) % 6.0)
h[mask_g] = 60.0 * (((b[mask_g] - r[mask_g]) / delta[mask_g]) + 2.0)
h[mask_b] = 60.0 * (((r[mask_b] - g[mask_b]) / delta[mask_b]) + 4.0)
return h, s, v
@staticmethod
def _compute_vein_gradient(green_channel: np.ndarray, mask: np.ndarray) -> float:
"""
Computes 2D spatial gradient (Sobel edge energy) within the masked area
to distinguish real biological leaf venation from flat synthetic plastic/paper.
"""
if not np.any(mask):
return 0.0
# Discrete gradient approximation
gx = np.abs(np.diff(green_channel, axis=1, prepend=green_channel[:, :1]))
gy = np.abs(np.diff(green_channel, axis=0, prepend=green_channel[:1, :]))
grad_mag = gx + gy
masked_grad = grad_mag[mask]
return float(np.mean(masked_grad)) if len(masked_grad) > 0 else 0.0
@classmethod
def validate_and_segment_leaf(cls, img_array: np.ndarray) -> Tuple[bool, str, np.ndarray, Dict[str, Any]]:
"""
Validates if the image contains real botanical leaf tissue.
Filters out flashlight glare, skin, displays, walls, and flat paper.
Returns: (is_valid, rejection_reason, leaf_mask, diagnostics)
"""
r = img_array[:, :, 0] / 255.0
g = img_array[:, :, 1] / 255.0
b = img_array[:, :, 2] / 255.0
total = r + g + b + 1e-6
# Normalized chromaticity
nr = r / total
ng = g / total
nb = b / total
# Excess Green Index (ExG)
exg = 2.0 * ng - nr - nb
# HSV Conversion
h, s, v = cls._rgb_to_hsv(r, g, b)
# 1. Specular Glare / Flashlight Reflection Filter
# Phone flash on waxy cuticle creates bright white spots (V > 0.94, S < 0.12)
glare_mask = (v > 0.94) & (s < 0.12)
glare_ratio = float(np.mean(glare_mask))
# 2. Comprehensive Botanical Plant Tissue Mask
# (A) Lush Green Chloroplast Foliage: Hue 35°-165°, Saturation >= 0.12, ExG > 0.015, V >= 0.08
green_foliage_mask = (h >= 35.0) & (h <= 165.0) & (s >= 0.12) & (exg > 0.015) & (v >= 0.08) & (~glare_mask)
# (B) Chlorotic / Yellowing Stressed Leaf Tissue: Hue 35°-75°, S >= 0.20, G > B + 0.08, ExG > -0.05
chlorotic_foliage_mask = (h >= 35.0) & (h <= 75.0) & (s >= 0.20) & (g > b + 0.08) & (exg > -0.05) & (~glare_mask)
# Total biological leaf tissue mask
leaf_mask = green_foliage_mask | chlorotic_foliage_mask
foliage_ratio = float(np.mean(leaf_mask))
# 3. Non-Plant Interference Detectors
# (A) Human skin: Hue 0°-34°, Saturation 0.12-0.70, R > G, R > B + 0.05
skin_mask = (h >= 0.0) & (h <= 34.0) & (s >= 0.12) & (s <= 0.70) & (r > g) & (r > b + 0.05)
skin_ratio = float(np.mean(skin_mask))
# (B) Electronic displays / Laptop screens: High Blue dominance
screen_mask = (b > r + 0.12) & (b > g) & (h >= 180.0) & (h <= 260.0)
screen_ratio = float(np.mean(screen_mask))
# (C) Flat walls, white paper, cloth: Very low saturation and neutral tone
neutral_mask = (s < 0.12) & (v > 0.30) & (~glare_mask)
neutral_ratio = float(np.mean(neutral_mask))
# 4. Biological Leaf Texture & Venation Energy
vein_energy = cls._compute_vein_gradient(g * 255.0, leaf_mask)
green_std = float(np.std(img_array[:, :, 1]))
diagnostics = {
"foliage_pixel_ratio": round(foliage_ratio, 3),
"glare_pixel_ratio": round(glare_ratio, 3),
"skin_ratio": round(skin_ratio, 3),
"screen_ratio": round(screen_ratio, 3),
"neutral_surface_ratio": round(neutral_ratio, 3),
"vein_gradient_energy": round(vein_energy, 2),
"green_texture_variance": round(green_std, 2)
}
# 5. Botanical Gatekeeper Validation Rules
if foliage_ratio < 0.15:
if skin_ratio > 0.28:
return False, "HUMAN_SKIN_DETECTED", leaf_mask, diagnostics
elif screen_ratio > 0.20:
return False, "ELECTRONIC_DISPLAY_DETECTED", leaf_mask, diagnostics
elif neutral_ratio > 0.45:
return False, "FLAT_SURFACE_WALL_OR_PAPER", leaf_mask, diagnostics
else:
return False, "NON_PLANT_OBJECT", leaf_mask, diagnostics
# Artificial flat green surface check (e.g. green plastic, green painted wall)
if vein_energy < 1.2 and green_std < 6.0 and foliage_ratio < 0.35:
return False, "ARTIFICIAL_SURFACE_NO_VEIN_TEXTURE", leaf_mask, diagnostics
return True, "VALID_BOTANICAL_LEAF", leaf_mask, diagnostics
@classmethod
def analyze_leaf_reflectance(cls, image_bytes: bytes) -> Dict[str, Any]:
"""
Validates botanical leaf presence and computes optical reflectance spectroscopy.
Rejects non-leaf objects with clear, helpful guidance.
"""
try:
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
except Exception:
return {
"is_valid_leaf": False,
"rejection_code": "INVALID_IMAGE_FILE",
"error_message": "अमान्य फोटो फाइल। कृपया दोबारा फोटो लें। (Invalid image format)",
"rejection_reason": "Corrupt or unreadable image file"
}
img_array = np.array(image, dtype=np.float32)
# 1. Botanical Leaf Gatekeeper
is_valid, reason_code, leaf_mask, diagnostics = cls.validate_and_segment_leaf(img_array)
if not is_valid:
rejection_messages = {
"HUMAN_SKIN_DETECTED": "यह इंसान की त्वचा (Skin) है, पौधे की पत्ती नहीं। कृपया फसल की पत्ती की फोटो लें।",
"ELECTRONIC_DISPLAY_DETECTED": "यह मोबाइल/लैपटॉप स्क्रीन है। कृपया खेत में असली पौधे की पत्ती की फोटो लें।",
"FLAT_SURFACE_WALL_OR_PAPER": "यह दीवार, किताब या कागज़ है। कृपया असली हरी पत्ती की फोटो लें।",
"ARTIFICIAL_SURFACE_NO_VEIN_TEXTURE": "यह पत्ता नहीं बल्कि कोई समतल या कृत्रिम वस्तु है। पत्ती की नसों (Veins) वाली फोटो लें।",
"NON_PLANT_OBJECT": "फोटो में कोई फसल या पौधे की पत्ती नहीं मिली। कृपया कैमरे को पत्ती के करीब (6-8 इंच) रखकर साफ फोटो लें।"
}
user_msg = rejection_messages.get(reason_code, "यह पौधे की पत्ती नहीं है। कृपया केवल असली फसल के पत्ते की फोटो लें।")
return {
"is_leaf": False,
"is_valid_leaf": False,
"rejection_code": reason_code,
"error_message": user_msg,
"rejection_reason": reason_code,
"diagnostics": diagnostics,
"chlorophyll_retention_percentage": 0.0,
"green_leaf_index_gli": 0.0,
"vari_index": 0.0,
"ngrdi_index": 0.0,
"health_status": "REJECTED_NON_LEAF",
"asymptomatic_stress_detected": False,
"risk_classification": "INVALID_INPUT",
"spectral_diagnosis": user_msg,
"early_action_protocol": [
"कैमरे की फ्लैशलाइट ऑन करें",
"पत्ते को फ्रेम के बीच में 6-10 इंच की दूरी से कैप्चर करें",
"पृष्ठभूमि (Background) में हाथ, दीवार या स्क्रीन न आने दें"
]
}
# 2. Pure Segmented Leaf Spectroscopy (Only on Masked Foliage Pixels)
r = img_array[:, :, 0] / 255.0
g = img_array[:, :, 1] / 255.0
b = img_array[:, :, 2] / 255.0
leaf_r = r[leaf_mask]
leaf_g = g[leaf_mask]
leaf_b = b[leaf_mask]
epsilon = 1e-6
# (A) Green Leaf Index (GLI) = (2G - R - B) / (2G + R + B)
gli_vals = (2.0 * leaf_g - leaf_r - leaf_b) / (2.0 * leaf_g + leaf_r + leaf_b + epsilon)
gli = float(np.mean(gli_vals))
# (B) Visible Atmospherically Resistant Index (VARI) = (G - R) / (G + R - B + epsilon)
vari_vals = (leaf_g - leaf_r) / (leaf_g + leaf_r - leaf_b + epsilon)
vari = float(np.mean(vari_vals))
# (C) Normalized Green-Red Difference Index (NGRDI) = (G - R) / (G + R)
ngrdi_vals = (leaf_g - leaf_r) / (leaf_g + leaf_r + epsilon)
ngrdi = float(np.mean(ngrdi_vals))
# (D) Calibrated Chlorophyll Retention Percentage
# Maps GLI from range [-0.2, 0.6] to [5%, 99.5%]
chlorophyll_index = float(np.clip(((gli + 0.18) / 0.78) * 100.0, 5.0, 99.5))
# 3. Pre-Symptomatic Health Classification
if chlorophyll_index >= 75.0:
status = "HEALTHY_OPTIMAL"
asymptomatic_stress = False
risk_level = "Zero / Negligible Stress"
diagnosis_desc = "पत्ते के मेसोफिल ऊतक में क्लोरोफिल और जल संतुलन पूर्णतः स्वस्थ है। कोई प्री-सिम्टमैटिक तनाव नहीं मिला।"
action_items = [
"वर्तमान सिंचाई और संतुलित NPK उर्वरक चक्र जारी रखें।",
"फसल की नियमित 3-5 दिन में निगरानी बनाए रखें।"
]
elif chlorophyll_index >= 55.0:
status = "PRE_SYMPTOMATIC_EARLY_STAGE"
asymptomatic_stress = True
risk_level = "Early Cellular Stress (24-48h Pre-Lesion)"
diagnosis_desc = (
"पत्ते की कोशिकाओं में क्लोरोफिल का सूक्ष्म ह्रास (Early Chlorophyll Degradation) डिटेक्ट हुआ है। "
"पत्ता पीला पड़ने या धब्बे दिखने से 24-48 घंटे पहले ही फंगल हाइफी या सूक्ष्म पोषक तत्व (Zinc/Iron/Magnesium) की कमी का संकेत मिला है।"
)
action_items = [
"स्यूडोमोनास फ्लोरेसेंस (Pseudomonas fluorescens) 10g/L या ट्राइकोडर्मा विरिडी 5g/L का प्रिवेंटिव बायो-स्प्रे करें।",
"खेत में अतिरिक्त पानी की निकासी करें ताकि आर्द्रता कम हो सके।",
"अगले 48 घंटे यूरिया का अतिरिक्त छिड़काव रोकें और पोटाश (MOP) का प्रयोग करें।"
]
else:
status = "ACTIVE_TISSUE_DEGRADATION"
asymptomatic_stress = True
risk_level = "Severe Cellular Breakdown / Active Infection"
diagnosis_desc = (
"पत्ते के क्लोरोप्लास्ट मैट्रिक्स में गंभीर गिरावट पाई गई है। "
"सक्रिय फंगल संक्रमण, पर्ण परिगलन (Necrotic Lesions) या विषाणु जनित क्लोरोसिस शुरू हो चुका है।"
)
action_items = [
"संक्रमित क्षेत्र में लक्षित कवकनाशी (Tricyclazole 75% WP @ 0.6g/L या Azoxystrobin 23% SC @ 1ml/L) का तुरंत छिड़काव करें।",
"क्रॉप स्कैनर से अन्य पौधों की भी विस्तृत जांच करें।"
]
return {
"is_leaf": True,
"is_valid_leaf": True,
"rejection_code": None,
"error_message": None,
"chlorophyll_retention_percentage": round(chlorophyll_index, 1),
"green_leaf_index_gli": round(gli, 4),
"vari_index": round(vari, 4),
"ngrdi_index": round(ngrdi, 4),
"health_status": status,
"asymptomatic_stress_detected": asymptomatic_stress,
"risk_classification": risk_level,
"spectral_diagnosis": diagnosis_desc,
"early_action_protocol": action_items,
"foliage_coverage_percentage": round(diagnostics["foliage_pixel_ratio"] * 100.0, 1),
"glare_filtered_percentage": round(diagnostics["glare_pixel_ratio"] * 100.0, 1),
"diagnostics": diagnostics
}
spectroscopy_engine = OpticalSpectroscopyEngine()