Download backend/spectroscopy.py from bapi143/CropAI: direct link, hf CLI and curl.
- Browser
- Download file 15.1 kB
-
https://huggingface.co/spaces/bapi143/CropAI/resolve/main/backend/spectroscopy.py
- Command line
-
hf download hf://spaces/bapi143/CropAI/backend/spectroscopy.py
-
curl -L -o spectroscopy.py https://huggingface.co/spaces/bapi143/CropAI/resolve/main/backend/spectroscopy.py
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.""" | |
| 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 | |
| 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 | |
| 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 | |
| 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() | |