Spaces:
Runtime error
Runtime error
Commit ·
2d332ab
1
Parent(s): 8ad7851
Deploy Sentinel-2 Pipeline
Browse files- DEPLOYMENT_CHECKLIST.md +173 -0
- DEVELOPER_GUIDE.md +386 -0
- Dockerfile +24 -0
- PACKAGE_SUMMARY.md +305 -0
- README.md +367 -10
- app.py +41 -0
- crop_stress_pipeline.py +477 -0
- example_usage.py +71 -0
- llm_analysis.py +435 -0
- requirements.txt +11 -0
- stress_detection_model.py +475 -0
- stress_detection_preprocessing.py +203 -0
- vegetation_indices.py +246 -0
DEPLOYMENT_CHECKLIST.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Production Pipeline - Files to Send to Developer
|
| 2 |
+
|
| 3 |
+
## Core Pipeline Files (Required)
|
| 4 |
+
|
| 5 |
+
1. **crop_stress_pipeline.py** - Main pipeline orchestrator
|
| 6 |
+
2. **vegetation_indices.py** - Vegetation indices calculation module
|
| 7 |
+
3. **stress_detection_preprocessing.py** - Data preprocessing for deep learning
|
| 8 |
+
4. **stress_detection_model.py** - CNN+LSTM stress detection model
|
| 9 |
+
5. **llm_analysis.py** - LLM integration and prompt engineering
|
| 10 |
+
|
| 11 |
+
## Configuration Files (Required)
|
| 12 |
+
|
| 13 |
+
6. **requirements.txt** - Python dependencies
|
| 14 |
+
7. **.env.template** - Environment variables template (rename to .env and fill in credentials)
|
| 15 |
+
|
| 16 |
+
## Documentation (Required)
|
| 17 |
+
|
| 18 |
+
8. **DEVELOPER_GUIDE.md** - Complete developer documentation
|
| 19 |
+
9. **DEPLOYMENT_CHECKLIST.md** - This file
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## Deployment Checklist
|
| 24 |
+
|
| 25 |
+
### Pre-Deployment
|
| 26 |
+
|
| 27 |
+
- [ ] Install Python 3.9+ on target server
|
| 28 |
+
- [ ] Create virtual environment: `python -m venv venv`
|
| 29 |
+
- [ ] Activate virtual environment
|
| 30 |
+
- [ ] Install dependencies: `pip install -r requirements.txt`
|
| 31 |
+
- [ ] Copy `.env.template` to `.env`
|
| 32 |
+
- [ ] Fill in Sentinel Hub credentials in `.env`
|
| 33 |
+
- [ ] Fill in Gemini API key in `.env`
|
| 34 |
+
- [ ] Test Sentinel Hub connection
|
| 35 |
+
- [ ] Test Gemini API connection
|
| 36 |
+
|
| 37 |
+
### Testing
|
| 38 |
+
|
| 39 |
+
- [ ] Run test with sample coordinates
|
| 40 |
+
- [ ] Verify log file is created
|
| 41 |
+
- [ ] Verify JSON output is generated
|
| 42 |
+
- [ ] Check all 13 vegetation indices are calculated
|
| 43 |
+
- [ ] Verify stress detection runs successfully
|
| 44 |
+
- [ ] Confirm LLM analysis completes
|
| 45 |
+
- [ ] Review output JSON structure
|
| 46 |
+
|
| 47 |
+
### Production Deployment
|
| 48 |
+
|
| 49 |
+
- [ ] Set up logging directory with write permissions
|
| 50 |
+
- [ ] Configure log rotation (optional)
|
| 51 |
+
- [ ] Set up monitoring for pipeline failures
|
| 52 |
+
- [ ] Configure API rate limits (Gemini: 60 requests/min)
|
| 53 |
+
- [ ] Set up backup for output JSON files
|
| 54 |
+
- [ ] Document server specifications (min 4GB RAM)
|
| 55 |
+
- [ ] Create systemd service (Linux) or Windows Service (optional)
|
| 56 |
+
|
| 57 |
+
### Security
|
| 58 |
+
|
| 59 |
+
- [ ] Ensure `.env` file is NOT committed to version control
|
| 60 |
+
- [ ] Add `.env` to `.gitignore`
|
| 61 |
+
- [ ] Restrict file permissions on `.env` (chmod 600)
|
| 62 |
+
- [ ] Use environment-specific credentials (dev/staging/prod)
|
| 63 |
+
- [ ] Rotate API keys regularly
|
| 64 |
+
- [ ] Enable HTTPS for API endpoints (if exposing via REST)
|
| 65 |
+
|
| 66 |
+
### Monitoring
|
| 67 |
+
|
| 68 |
+
- [ ] Set up log monitoring (e.g., ELK stack, CloudWatch)
|
| 69 |
+
- [ ] Configure alerts for pipeline failures
|
| 70 |
+
- [ ] Monitor API quota usage (Sentinel Hub, Gemini)
|
| 71 |
+
- [ ] Track processing time metrics
|
| 72 |
+
- [ ] Monitor disk space for output files
|
| 73 |
+
|
| 74 |
+
---
|
| 75 |
+
|
| 76 |
+
## Quick Test Script
|
| 77 |
+
|
| 78 |
+
```python
|
| 79 |
+
# test_pipeline.py
|
| 80 |
+
from crop_stress_pipeline import CropStressPipeline
|
| 81 |
+
|
| 82 |
+
pipeline = CropStressPipeline()
|
| 83 |
+
|
| 84 |
+
# Test with PAU Experimental Farm
|
| 85 |
+
params = {
|
| 86 |
+
'center_lat': 30.2300,
|
| 87 |
+
'center_lon': 75.8300,
|
| 88 |
+
'crop_type': 'Wheat',
|
| 89 |
+
'analysis_date': '2024-01-15',
|
| 90 |
+
'field_size_hectares': 0.04,
|
| 91 |
+
'farmer_context': {
|
| 92 |
+
'role': 'Owner-Operator',
|
| 93 |
+
'years_farming': 15,
|
| 94 |
+
'irrigation_method': 'Drip Irrigation',
|
| 95 |
+
'farming_goal': 'Maximize yield'
|
| 96 |
+
},
|
| 97 |
+
'output_path': 'test_results.json'
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
try:
|
| 101 |
+
results = pipeline.run(**params)
|
| 102 |
+
print("✓ Pipeline test successful!")
|
| 103 |
+
print(f"✓ Output saved to: {params['output_path']}")
|
| 104 |
+
except Exception as e:
|
| 105 |
+
print(f"✗ Pipeline test failed: {e}")
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
---
|
| 109 |
+
|
| 110 |
+
## Expected Output Structure
|
| 111 |
+
|
| 112 |
+
```
|
| 113 |
+
production_pipeline/
|
| 114 |
+
├── crop_stress_pipeline.py
|
| 115 |
+
├── vegetation_indices.py
|
| 116 |
+
├── stress_detection_preprocessing.py
|
| 117 |
+
├── stress_detection_model.py
|
| 118 |
+
├── llm_analysis.py
|
| 119 |
+
├── requirements.txt
|
| 120 |
+
├── .env.template
|
| 121 |
+
├── .env (create from template)
|
| 122 |
+
├── DEVELOPER_GUIDE.md
|
| 123 |
+
├── DEPLOYMENT_CHECKLIST.md
|
| 124 |
+
├── crop_stress_pipeline.log (auto-generated)
|
| 125 |
+
└── *.json (output files)
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
---
|
| 129 |
+
|
| 130 |
+
## API Quotas & Limits
|
| 131 |
+
|
| 132 |
+
### Sentinel Hub (CDSE)
|
| 133 |
+
- **Free tier**: 30,000 processing units/month
|
| 134 |
+
- **Rate limit**: ~10 requests/second
|
| 135 |
+
- **Typical usage**: ~100 PU per field analysis
|
| 136 |
+
|
| 137 |
+
### Google Gemini
|
| 138 |
+
- **Free tier**: 60 requests/minute
|
| 139 |
+
- **Rate limit**: 1500 requests/day (free)
|
| 140 |
+
- **Typical usage**: 1 request per field analysis
|
| 141 |
+
|
| 142 |
+
---
|
| 143 |
+
|
| 144 |
+
## Troubleshooting Common Issues
|
| 145 |
+
|
| 146 |
+
### Issue: "Sentinel Hub credentials not found"
|
| 147 |
+
**Solution**: Ensure `.env` file exists and contains valid credentials
|
| 148 |
+
|
| 149 |
+
### Issue: "No images found for date range"
|
| 150 |
+
**Solution**: Adjust `analysis_date` or check cloud cover threshold
|
| 151 |
+
|
| 152 |
+
### Issue: "Gemini API quota exceeded"
|
| 153 |
+
**Solution**: Wait for quota reset or upgrade to paid tier
|
| 154 |
+
|
| 155 |
+
### Issue: "Out of memory error"
|
| 156 |
+
**Solution**: Reduce AOI size or increase server RAM
|
| 157 |
+
|
| 158 |
+
---
|
| 159 |
+
|
| 160 |
+
## Support Contacts
|
| 161 |
+
|
| 162 |
+
- **Technical Issues**: Check logs in `crop_stress_pipeline.log`
|
| 163 |
+
- **API Issues**: Refer to DEVELOPER_GUIDE.md
|
| 164 |
+
- **Architecture Questions**: Review pipeline source code comments
|
| 165 |
+
|
| 166 |
+
---
|
| 167 |
+
|
| 168 |
+
## Version Information
|
| 169 |
+
|
| 170 |
+
- **Pipeline Version**: 1.0
|
| 171 |
+
- **Python Version**: 3.9+
|
| 172 |
+
- **TensorFlow Version**: 2.13+
|
| 173 |
+
- **Last Updated**: 2024-12-04
|
DEVELOPER_GUIDE.md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Crop Stress Detection Pipeline - Developer Guide
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
This production-ready pipeline performs comprehensive crop stress analysis using Sentinel-2 satellite imagery, deep learning models, and LLM-powered insights.
|
| 6 |
+
|
| 7 |
+
**Pipeline Architecture:**
|
| 8 |
+
1. **Data Acquisition**: Fetch Sentinel-2 L2A imagery from Copernicus Data Space Ecosystem
|
| 9 |
+
2. **Vegetation Indices**: Calculate 13 indices (NDVI, EVI, NDWI, NDRE, RECI, SMI, NDSI, PRI, PSRI, MCARI, SASI, SOMI, SFI)
|
| 10 |
+
3. **Temporal Analysis**: Extract temporal statistics and trends
|
| 11 |
+
4. **Stress Detection**: CNN + LSTM spatial-temporal encoding with K-Means clustering (k=3)
|
| 12 |
+
5. **Anomaly Detection**: Isolation Forest for unusual patterns
|
| 13 |
+
6. **LLM Analysis**: Gemini-powered comprehensive crop and soil insights
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## Quick Start
|
| 18 |
+
|
| 19 |
+
### 1. Installation
|
| 20 |
+
|
| 21 |
+
```bash
|
| 22 |
+
# Create virtual environment
|
| 23 |
+
python -m venv venv
|
| 24 |
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
| 25 |
+
|
| 26 |
+
# Install dependencies
|
| 27 |
+
pip install -r requirements.txt
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
### 2. Environment Setup
|
| 31 |
+
|
| 32 |
+
Create a `.env` file in the project root:
|
| 33 |
+
|
| 34 |
+
```env
|
| 35 |
+
# Sentinel Hub Credentials (Copernicus Data Space Ecosystem)
|
| 36 |
+
SH_CLIENT_ID=your_client_id_here
|
| 37 |
+
SH_CLIENT_SECRET=your_client_secret_here
|
| 38 |
+
|
| 39 |
+
# Google Gemini API Key
|
| 40 |
+
GEMINI_API_KEY=your_gemini_api_key_here
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
**How to get credentials:**
|
| 44 |
+
- **Sentinel Hub**: Register at https://dataspace.copernicus.eu/
|
| 45 |
+
- **Gemini API**: Get key from https://makersuite.google.com/app/apikey
|
| 46 |
+
|
| 47 |
+
### 3. Run Pipeline
|
| 48 |
+
|
| 49 |
+
```python
|
| 50 |
+
from crop_stress_pipeline import CropStressPipeline
|
| 51 |
+
|
| 52 |
+
# Initialize pipeline
|
| 53 |
+
pipeline = CropStressPipeline()
|
| 54 |
+
|
| 55 |
+
# Define parameters
|
| 56 |
+
params = {
|
| 57 |
+
'center_lat': 30.2300,
|
| 58 |
+
'center_lon': 75.8300,
|
| 59 |
+
'crop_type': 'Wheat',
|
| 60 |
+
'analysis_date': '2024-01-15',
|
| 61 |
+
'field_size_hectares': 0.04,
|
| 62 |
+
'farmer_context': {
|
| 63 |
+
'role': 'Owner-Operator',
|
| 64 |
+
'years_farming': 15,
|
| 65 |
+
'irrigation_method': 'Drip Irrigation',
|
| 66 |
+
'farming_goal': 'Maximize yield while maintaining soil health'
|
| 67 |
+
},
|
| 68 |
+
'output_path': 'results.json'
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
# Run analysis
|
| 72 |
+
results = pipeline.run(**params)
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
---
|
| 76 |
+
|
| 77 |
+
## File Structure
|
| 78 |
+
|
| 79 |
+
```
|
| 80 |
+
production_pipeline/
|
| 81 |
+
├── crop_stress_pipeline.py # Main pipeline script
|
| 82 |
+
├── vegetation_indices.py # Vegetation indices calculation
|
| 83 |
+
├── stress_detection_preprocessing.py # Data preprocessing for DL model
|
| 84 |
+
├── stress_detection_model.py # CNN+LSTM stress detection model
|
| 85 |
+
├── llm_analysis.py # LLM integration and prompt engineering
|
| 86 |
+
├── requirements.txt # Python dependencies
|
| 87 |
+
├── .env # Environment variables (create this)
|
| 88 |
+
├── DEVELOPER_GUIDE.md # This file
|
| 89 |
+
└── crop_stress_pipeline.log # Auto-generated log file
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
---
|
| 93 |
+
|
| 94 |
+
## API Reference
|
| 95 |
+
|
| 96 |
+
### CropStressPipeline Class
|
| 97 |
+
|
| 98 |
+
#### `__init__(config_path: str = None)`
|
| 99 |
+
Initialize pipeline with environment configuration.
|
| 100 |
+
|
| 101 |
+
**Args:**
|
| 102 |
+
- `config_path`: Optional path to .env file
|
| 103 |
+
|
| 104 |
+
#### `run(center_lat, center_lon, crop_type, analysis_date, field_size_hectares, farmer_context, output_path=None)`
|
| 105 |
+
Execute complete pipeline.
|
| 106 |
+
|
| 107 |
+
**Args:**
|
| 108 |
+
- `center_lat` (float): Field center latitude
|
| 109 |
+
- `center_lon` (float): Field center longitude
|
| 110 |
+
- `crop_type` (str): Crop type (e.g., 'Wheat', 'Rice', 'Corn')
|
| 111 |
+
- `analysis_date` (str): Target date in 'YYYY-MM-DD' format
|
| 112 |
+
- `field_size_hectares` (float): Field size in hectares
|
| 113 |
+
- `farmer_context` (dict): Farmer profile with keys:
|
| 114 |
+
- `role`: Farmer role
|
| 115 |
+
- `years_farming`: Years of experience
|
| 116 |
+
- `irrigation_method`: Irrigation type
|
| 117 |
+
- `farming_goal`: Primary farming objective
|
| 118 |
+
- `output_path` (str, optional): Path to save JSON results
|
| 119 |
+
|
| 120 |
+
**Returns:**
|
| 121 |
+
- `dict`: Complete analysis results
|
| 122 |
+
|
| 123 |
+
---
|
| 124 |
+
|
| 125 |
+
## Output Format
|
| 126 |
+
|
| 127 |
+
The pipeline generates a JSON file with the following structure:
|
| 128 |
+
|
| 129 |
+
```json
|
| 130 |
+
{
|
| 131 |
+
"metadata": {
|
| 132 |
+
"crop_type": "Wheat",
|
| 133 |
+
"analysis_date": "2024-01-15",
|
| 134 |
+
"location": {"lat": 30.23, "lon": 75.83},
|
| 135 |
+
"field_size_hectares": 0.04,
|
| 136 |
+
"farmer_context": {...},
|
| 137 |
+
"num_images": 10,
|
| 138 |
+
"date_range": ["2023-10-15", "2024-01-15"]
|
| 139 |
+
},
|
| 140 |
+
"vegetation_indices_summary": {
|
| 141 |
+
"indices": {
|
| 142 |
+
"NDVI": {
|
| 143 |
+
"latest": {"mean": 0.65, "std": 0.12},
|
| 144 |
+
"max_in_field": 0.82,
|
| 145 |
+
"min_in_field": 0.41,
|
| 146 |
+
"change": 0.15
|
| 147 |
+
},
|
| 148 |
+
...
|
| 149 |
+
}
|
| 150 |
+
},
|
| 151 |
+
"stress_detection": {
|
| 152 |
+
"field_statistics": {
|
| 153 |
+
"overall_stress": {"mean": 0.45, "std": 0.15},
|
| 154 |
+
"stress_distribution": {"low": 15, "moderate": 20, "high": 5}
|
| 155 |
+
},
|
| 156 |
+
"cluster_statistics": [
|
| 157 |
+
{
|
| 158 |
+
"cluster_id": 0,
|
| 159 |
+
"percentage": 40.0,
|
| 160 |
+
"stress_score": {"mean": 0.25, "std": 0.05},
|
| 161 |
+
"band_statistics": {...},
|
| 162 |
+
"temporal_trends": {
|
| 163 |
+
"B04": {"change": -0.01, "trend_direction": "stable"},
|
| 164 |
+
"B08": {"change": 0.05, "trend_direction": "increasing"}
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
],
|
| 168 |
+
"anomaly_information": {
|
| 169 |
+
"total_anomalies": 2,
|
| 170 |
+
"anomaly_percentage": 5.0
|
| 171 |
+
}
|
| 172 |
+
},
|
| 173 |
+
"llm_analysis": {
|
| 174 |
+
"soil_moisture": {
|
| 175 |
+
"level": "moderate",
|
| 176 |
+
"maximum_value": 0.65,
|
| 177 |
+
"minimum_value": 0.32,
|
| 178 |
+
"analysis": "..."
|
| 179 |
+
},
|
| 180 |
+
"vegetation_stress": {...},
|
| 181 |
+
"overall_health": {
|
| 182 |
+
"status": "good",
|
| 183 |
+
"key_concerns": ["..."],
|
| 184 |
+
"recommendations": ["..."]
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
---
|
| 191 |
+
|
| 192 |
+
## Logging
|
| 193 |
+
|
| 194 |
+
All pipeline operations are logged to:
|
| 195 |
+
- **Console**: Real-time progress
|
| 196 |
+
- **File**: `crop_stress_pipeline.log`
|
| 197 |
+
|
| 198 |
+
**Log Levels:**
|
| 199 |
+
- `INFO`: Normal operations
|
| 200 |
+
- `ERROR`: Failures and exceptions
|
| 201 |
+
|
| 202 |
+
**Example Log Output:**
|
| 203 |
+
```
|
| 204 |
+
2024-12-04 16:45:00 - INFO - Pipeline initialized successfully
|
| 205 |
+
2024-12-04 16:45:05 - INFO - Fetching satellite data...
|
| 206 |
+
2024-12-04 16:45:30 - INFO - Found 10 suitable images
|
| 207 |
+
2024-12-04 16:46:00 - INFO - Calculated 13 indices
|
| 208 |
+
2024-12-04 16:46:15 - INFO - Stress detection complete
|
| 209 |
+
2024-12-04 16:46:30 - INFO - LLM analysis complete
|
| 210 |
+
2024-12-04 16:46:35 - INFO - PIPELINE COMPLETED SUCCESSFULLY
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
---
|
| 214 |
+
|
| 215 |
+
## Configuration Parameters
|
| 216 |
+
|
| 217 |
+
### Clustering
|
| 218 |
+
- **Number of clusters**: Fixed at 3 (low, moderate, high stress)
|
| 219 |
+
- **Contamination**: 0.1 (10% expected anomalies)
|
| 220 |
+
|
| 221 |
+
### Spatial Resolution
|
| 222 |
+
- **Default**: 10m per pixel
|
| 223 |
+
- **Patch size**: 8x8 pixels
|
| 224 |
+
- **Stride**: 4 pixels (50% overlap)
|
| 225 |
+
|
| 226 |
+
### Temporal Analysis
|
| 227 |
+
- **Images**: 10 most recent cloud-free images
|
| 228 |
+
- **Search window**: 90 days before + 30 days after target date
|
| 229 |
+
- **Cloud threshold**: < 20%
|
| 230 |
+
|
| 231 |
+
### Deep Learning Model
|
| 232 |
+
- **Spatial encoder**: CNN (32→64→128 filters)
|
| 233 |
+
- **Temporal encoder**: Bidirectional LSTM (64 units)
|
| 234 |
+
- **Embedding dimensions**: 128
|
| 235 |
+
|
| 236 |
+
---
|
| 237 |
+
|
| 238 |
+
## Error Handling
|
| 239 |
+
|
| 240 |
+
Common errors and solutions:
|
| 241 |
+
|
| 242 |
+
### 1. Missing Credentials
|
| 243 |
+
```
|
| 244 |
+
ValueError: Sentinel Hub credentials not found
|
| 245 |
+
```
|
| 246 |
+
**Solution**: Ensure `.env` file exists with valid credentials
|
| 247 |
+
|
| 248 |
+
### 2. No Images Found
|
| 249 |
+
```
|
| 250 |
+
IndexError: list index out of range
|
| 251 |
+
```
|
| 252 |
+
**Solution**: Adjust `analysis_date` or expand search window
|
| 253 |
+
|
| 254 |
+
### 3. LLM API Error
|
| 255 |
+
```
|
| 256 |
+
google.api_core.exceptions.PermissionDenied
|
| 257 |
+
```
|
| 258 |
+
**Solution**: Verify `GEMINI_API_KEY` is valid and has quota
|
| 259 |
+
|
| 260 |
+
### 4. Insufficient Patches
|
| 261 |
+
```
|
| 262 |
+
WARNING: Only X patches generated
|
| 263 |
+
```
|
| 264 |
+
**Solution**: Increase AOI size or reduce `patch_size`
|
| 265 |
+
|
| 266 |
+
---
|
| 267 |
+
|
| 268 |
+
## Performance Optimization
|
| 269 |
+
|
| 270 |
+
### Memory Usage
|
| 271 |
+
- **Typical**: 2-4 GB RAM
|
| 272 |
+
- **Large AOIs**: Consider batch processing
|
| 273 |
+
|
| 274 |
+
### Processing Time
|
| 275 |
+
- **Small field (0.04 ha)**: ~2-3 minutes
|
| 276 |
+
- **Medium field (1 ha)**: ~5-10 minutes
|
| 277 |
+
- **Large field (10 ha)**: ~20-30 minutes
|
| 278 |
+
|
| 279 |
+
**Bottlenecks:**
|
| 280 |
+
1. Satellite data download (~30%)
|
| 281 |
+
2. Deep learning inference (~40%)
|
| 282 |
+
3. LLM API call (~20%)
|
| 283 |
+
4. Index calculation (~10%)
|
| 284 |
+
|
| 285 |
+
---
|
| 286 |
+
|
| 287 |
+
## Integration Guide
|
| 288 |
+
|
| 289 |
+
### REST API Wrapper
|
| 290 |
+
|
| 291 |
+
```python
|
| 292 |
+
from flask import Flask, request, jsonify
|
| 293 |
+
from crop_stress_pipeline import CropStressPipeline
|
| 294 |
+
|
| 295 |
+
app = Flask(__name__)
|
| 296 |
+
pipeline = CropStressPipeline()
|
| 297 |
+
|
| 298 |
+
@app.route('/analyze', methods=['POST'])
|
| 299 |
+
def analyze():
|
| 300 |
+
data = request.json
|
| 301 |
+
try:
|
| 302 |
+
results = pipeline.run(
|
| 303 |
+
center_lat=data['lat'],
|
| 304 |
+
center_lon=data['lon'],
|
| 305 |
+
crop_type=data['crop_type'],
|
| 306 |
+
analysis_date=data['date'],
|
| 307 |
+
field_size_hectares=data['field_size'],
|
| 308 |
+
farmer_context=data['farmer_context']
|
| 309 |
+
)
|
| 310 |
+
return jsonify(results)
|
| 311 |
+
except Exception as e:
|
| 312 |
+
return jsonify({'error': str(e)}), 500
|
| 313 |
+
|
| 314 |
+
if __name__ == '__main__':
|
| 315 |
+
app.run(host='0.0.0.0', port=5000)
|
| 316 |
+
```
|
| 317 |
+
|
| 318 |
+
### Batch Processing
|
| 319 |
+
|
| 320 |
+
```python
|
| 321 |
+
import pandas as pd
|
| 322 |
+
|
| 323 |
+
# Load field data
|
| 324 |
+
fields = pd.read_csv('fields.csv')
|
| 325 |
+
|
| 326 |
+
# Process each field
|
| 327 |
+
for _, field in fields.iterrows():
|
| 328 |
+
results = pipeline.run(
|
| 329 |
+
center_lat=field['lat'],
|
| 330 |
+
center_lon=field['lon'],
|
| 331 |
+
crop_type=field['crop'],
|
| 332 |
+
analysis_date=field['date'],
|
| 333 |
+
field_size_hectares=field['size'],
|
| 334 |
+
farmer_context={...},
|
| 335 |
+
output_path=f"results_{field['id']}.json"
|
| 336 |
+
)
|
| 337 |
+
```
|
| 338 |
+
|
| 339 |
+
---
|
| 340 |
+
|
| 341 |
+
## Troubleshooting
|
| 342 |
+
|
| 343 |
+
### Enable Debug Logging
|
| 344 |
+
|
| 345 |
+
```python
|
| 346 |
+
import logging
|
| 347 |
+
logging.getLogger().setLevel(logging.DEBUG)
|
| 348 |
+
```
|
| 349 |
+
|
| 350 |
+
### Test Individual Components
|
| 351 |
+
|
| 352 |
+
```python
|
| 353 |
+
# Test Sentinel Hub connection
|
| 354 |
+
from sentinelhub import SHConfig
|
| 355 |
+
config = SHConfig()
|
| 356 |
+
config.sh_client_id = "your_id"
|
| 357 |
+
config.sh_client_secret = "your_secret"
|
| 358 |
+
# Should not raise errors
|
| 359 |
+
|
| 360 |
+
# Test LLM connection
|
| 361 |
+
import google.generativeai as genai
|
| 362 |
+
genai.configure(api_key="your_key")
|
| 363 |
+
model = genai.GenerativeModel('gemini-flash-latest')
|
| 364 |
+
response = model.generate_content("Hello")
|
| 365 |
+
print(response.text)
|
| 366 |
+
```
|
| 367 |
+
|
| 368 |
+
---
|
| 369 |
+
|
| 370 |
+
## Support
|
| 371 |
+
|
| 372 |
+
For issues or questions:
|
| 373 |
+
1. Check logs in `crop_stress_pipeline.log`
|
| 374 |
+
2. Review this guide
|
| 375 |
+
3. Contact: SIH ML Team
|
| 376 |
+
|
| 377 |
+
---
|
| 378 |
+
|
| 379 |
+
## Version History
|
| 380 |
+
|
| 381 |
+
- **v1.0** (2024-12-04): Initial production release
|
| 382 |
+
- 13 vegetation indices
|
| 383 |
+
- CNN+LSTM stress detection
|
| 384 |
+
- Fixed 3-cluster configuration
|
| 385 |
+
- Gemini LLM integration
|
| 386 |
+
- Comprehensive logging
|
Dockerfile
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
build-essential \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# Copy requirements first to leverage cache
|
| 11 |
+
COPY requirements.txt .
|
| 12 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
+
|
| 14 |
+
# Copy application code
|
| 15 |
+
COPY . .
|
| 16 |
+
|
| 17 |
+
# Create directory for outputs if needed
|
| 18 |
+
RUN mkdir -p sar_prediction_output && chmod 777 sar_prediction_output
|
| 19 |
+
|
| 20 |
+
# Expose port
|
| 21 |
+
EXPOSE 7860
|
| 22 |
+
|
| 23 |
+
# Run the application
|
| 24 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
PACKAGE_SUMMARY.md
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PRODUCTION PIPELINE - PACKAGE SUMMARY
|
| 2 |
+
|
| 3 |
+
## Package Information
|
| 4 |
+
|
| 5 |
+
**Package Name:** Crop Stress Detection Pipeline
|
| 6 |
+
**Version:** 1.0
|
| 7 |
+
**Date Created:** 2024-12-04
|
| 8 |
+
**Total Files:** 11
|
| 9 |
+
**Total Size:** ~100 KB (excluding dependencies)
|
| 10 |
+
**Python Version:** 3.9+
|
| 11 |
+
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
## Complete File List
|
| 15 |
+
|
| 16 |
+
### 1. Core Pipeline Files (5 files)
|
| 17 |
+
|
| 18 |
+
| # | Filename | Size | Description |
|
| 19 |
+
|---|----------|------|-------------|
|
| 20 |
+
| 1 | `crop_stress_pipeline.py` | 19 KB | Main pipeline orchestrator with logging |
|
| 21 |
+
| 2 | `vegetation_indices.py` | 8 KB | 13 vegetation indices calculation |
|
| 22 |
+
| 3 | `stress_detection_preprocessing.py` | 7 KB | Data preprocessing for deep learning |
|
| 23 |
+
| 4 | `stress_detection_model.py` | 19 KB | CNN+LSTM stress detection model |
|
| 24 |
+
| 5 | `llm_analysis.py` | 19 KB | LLM integration and prompt engineering |
|
| 25 |
+
|
| 26 |
+
### 2. Configuration Files (2 files)
|
| 27 |
+
|
| 28 |
+
| # | Filename | Size | Description |
|
| 29 |
+
|---|----------|------|-------------|
|
| 30 |
+
| 6 | `requirements.txt` | 160 B | Python dependencies (8 packages) |
|
| 31 |
+
| 7 | `.env.template` | 200 B | Environment variables template |
|
| 32 |
+
|
| 33 |
+
### 3. Documentation Files (3 files)
|
| 34 |
+
|
| 35 |
+
| # | Filename | Size | Description |
|
| 36 |
+
|---|----------|------|-------------|
|
| 37 |
+
| 8 | `README.md` | 11 KB | Package overview and quick start |
|
| 38 |
+
| 9 | `DEVELOPER_GUIDE.md` | 10 KB | Complete developer documentation |
|
| 39 |
+
| 10 | `DEPLOYMENT_CHECKLIST.md` | 5 KB | Deployment guide and checklist |
|
| 40 |
+
|
| 41 |
+
### 4. Example Files (1 file)
|
| 42 |
+
|
| 43 |
+
| # | Filename | Size | Description |
|
| 44 |
+
|---|----------|------|-------------|
|
| 45 |
+
| 11 | `example_usage.py` | 2 KB | Example usage script |
|
| 46 |
+
|
| 47 |
+
---
|
| 48 |
+
|
| 49 |
+
## Key Changes from Notebook Version
|
| 50 |
+
|
| 51 |
+
### ✅ Removed
|
| 52 |
+
- All visualization code (matplotlib plots)
|
| 53 |
+
- Jupyter notebook cells
|
| 54 |
+
- Interactive displays
|
| 55 |
+
- Clustering optimization function (find_optimal_clusters)
|
| 56 |
+
- Verbose print statements
|
| 57 |
+
|
| 58 |
+
### ✅ Added
|
| 59 |
+
- Production logging (console + file)
|
| 60 |
+
- Class-based architecture
|
| 61 |
+
- Error handling
|
| 62 |
+
- Comprehensive documentation
|
| 63 |
+
- Example usage scripts
|
| 64 |
+
- Deployment checklist
|
| 65 |
+
|
| 66 |
+
### ✅ Fixed
|
| 67 |
+
- Clustering: Fixed to k=3 (low, moderate, high stress)
|
| 68 |
+
- Logging: Structured logging for monitoring
|
| 69 |
+
- Output: JSON-only output format
|
| 70 |
+
- Architecture: Maintained full integrity
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
## Dependencies (requirements.txt)
|
| 75 |
+
|
| 76 |
+
```
|
| 77 |
+
numpy>=1.24.0
|
| 78 |
+
pandas>=2.0.0
|
| 79 |
+
matplotlib>=3.7.0
|
| 80 |
+
scikit-learn>=1.3.0
|
| 81 |
+
tensorflow>=2.13.0
|
| 82 |
+
sentinelhub>=3.9.0
|
| 83 |
+
python-dotenv>=1.0.0
|
| 84 |
+
google-generativeai>=0.3.0
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
**Total Dependencies:** 8 packages
|
| 88 |
+
**Installation:** `pip install -r requirements.txt`
|
| 89 |
+
|
| 90 |
+
---
|
| 91 |
+
|
| 92 |
+
## Environment Variables (.env.template)
|
| 93 |
+
|
| 94 |
+
```env
|
| 95 |
+
SH_CLIENT_ID=your_client_id_here
|
| 96 |
+
SH_CLIENT_SECRET=your_client_secret_here
|
| 97 |
+
GEMINI_API_KEY=your_gemini_api_key_here
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
**Setup:**
|
| 101 |
+
1. Copy `.env.template` to `.env`
|
| 102 |
+
2. Fill in actual credentials
|
| 103 |
+
3. Never commit `.env` to version control
|
| 104 |
+
|
| 105 |
+
---
|
| 106 |
+
|
| 107 |
+
## Usage Example
|
| 108 |
+
|
| 109 |
+
```python
|
| 110 |
+
from crop_stress_pipeline import CropStressPipeline
|
| 111 |
+
|
| 112 |
+
# Initialize
|
| 113 |
+
pipeline = CropStressPipeline()
|
| 114 |
+
|
| 115 |
+
# Run analysis
|
| 116 |
+
results = pipeline.run(
|
| 117 |
+
center_lat=30.2300,
|
| 118 |
+
center_lon=75.8300,
|
| 119 |
+
crop_type='Wheat',
|
| 120 |
+
analysis_date='2024-01-15',
|
| 121 |
+
field_size_hectares=0.04,
|
| 122 |
+
farmer_context={
|
| 123 |
+
'role': 'Owner-Operator',
|
| 124 |
+
'years_farming': 15,
|
| 125 |
+
'irrigation_method': 'Drip Irrigation',
|
| 126 |
+
'farming_goal': 'Maximize yield'
|
| 127 |
+
},
|
| 128 |
+
output_path='results.json'
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
# Results saved to results.json
|
| 132 |
+
# Logs saved to crop_stress_pipeline.log
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
---
|
| 136 |
+
|
| 137 |
+
## Output Files
|
| 138 |
+
|
| 139 |
+
### Generated During Execution
|
| 140 |
+
|
| 141 |
+
1. **`crop_stress_pipeline.log`** - Execution logs
|
| 142 |
+
2. **`results.json`** - Analysis results (or custom name)
|
| 143 |
+
|
| 144 |
+
### Output JSON Structure
|
| 145 |
+
|
| 146 |
+
```json
|
| 147 |
+
{
|
| 148 |
+
"metadata": {...},
|
| 149 |
+
"vegetation_indices_summary": {...},
|
| 150 |
+
"stress_detection": {
|
| 151 |
+
"field_statistics": {...},
|
| 152 |
+
"cluster_statistics": [
|
| 153 |
+
{
|
| 154 |
+
"cluster_id": 0,
|
| 155 |
+
"temporal_trends": {...}
|
| 156 |
+
}
|
| 157 |
+
],
|
| 158 |
+
"anomaly_information": {...}
|
| 159 |
+
},
|
| 160 |
+
"llm_analysis": {...}
|
| 161 |
+
}
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
---
|
| 165 |
+
|
| 166 |
+
## Deployment Instructions
|
| 167 |
+
|
| 168 |
+
### Step 1: Transfer Files
|
| 169 |
+
Copy all 11 files to production server
|
| 170 |
+
|
| 171 |
+
### Step 2: Install Dependencies
|
| 172 |
+
```bash
|
| 173 |
+
python -m venv venv
|
| 174 |
+
source venv/bin/activate # Windows: venv\Scripts\activate
|
| 175 |
+
pip install -r requirements.txt
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
### Step 3: Configure Environment
|
| 179 |
+
```bash
|
| 180 |
+
cp .env.template .env
|
| 181 |
+
# Edit .env with actual credentials
|
| 182 |
+
```
|
| 183 |
+
|
| 184 |
+
### Step 4: Test
|
| 185 |
+
```bash
|
| 186 |
+
python example_usage.py
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
### Step 5: Integrate
|
| 190 |
+
Use `crop_stress_pipeline.py` in your application
|
| 191 |
+
|
| 192 |
+
---
|
| 193 |
+
|
| 194 |
+
## Architecture Integrity
|
| 195 |
+
|
| 196 |
+
### ✅ Maintained
|
| 197 |
+
- 13 vegetation indices calculation
|
| 198 |
+
- CNN + LSTM spatial-temporal encoding
|
| 199 |
+
- K-Means clustering (k=3)
|
| 200 |
+
- Isolation Forest anomaly detection
|
| 201 |
+
- Temporal trend calculation per cluster
|
| 202 |
+
- LLM prompt with comprehensive context
|
| 203 |
+
- All band statistics and temporal features
|
| 204 |
+
|
| 205 |
+
### ✅ Configuration
|
| 206 |
+
- **Clusters:** Fixed at 3 (production default)
|
| 207 |
+
- **Patch Size:** 8x8 pixels
|
| 208 |
+
- **Stride:** 4 pixels
|
| 209 |
+
- **Contamination:** 0.1 (10%)
|
| 210 |
+
- **Resolution:** 10m
|
| 211 |
+
- **Images:** 10 cloud-free
|
| 212 |
+
|
| 213 |
+
---
|
| 214 |
+
|
| 215 |
+
## What to Send to Developer
|
| 216 |
+
|
| 217 |
+
### Minimum Package (9 files - Required)
|
| 218 |
+
1. `crop_stress_pipeline.py`
|
| 219 |
+
2. `vegetation_indices.py`
|
| 220 |
+
3. `stress_detection_preprocessing.py`
|
| 221 |
+
4. `stress_detection_model.py`
|
| 222 |
+
5. `llm_analysis.py`
|
| 223 |
+
6. `requirements.txt`
|
| 224 |
+
7. `.env.template`
|
| 225 |
+
8. `DEVELOPER_GUIDE.md`
|
| 226 |
+
9. `DEPLOYMENT_CHECKLIST.md`
|
| 227 |
+
|
| 228 |
+
### Complete Package (11 files - Recommended)
|
| 229 |
+
All 9 above +
|
| 230 |
+
10. `README.md`
|
| 231 |
+
11. `example_usage.py`
|
| 232 |
+
|
| 233 |
+
---
|
| 234 |
+
|
| 235 |
+
## Support Documentation
|
| 236 |
+
|
| 237 |
+
| Document | Purpose | Audience |
|
| 238 |
+
|----------|---------|----------|
|
| 239 |
+
| `README.md` | Quick start and overview | All users |
|
| 240 |
+
| `DEVELOPER_GUIDE.md` | Complete API reference | Developers |
|
| 241 |
+
| `DEPLOYMENT_CHECKLIST.md` | Deployment steps | DevOps |
|
| 242 |
+
|
| 243 |
+
---
|
| 244 |
+
|
| 245 |
+
## Quality Assurance
|
| 246 |
+
|
| 247 |
+
### ✅ Code Quality
|
| 248 |
+
- No visualization dependencies
|
| 249 |
+
- Production logging only
|
| 250 |
+
- Clean error handling
|
| 251 |
+
- Type hints included
|
| 252 |
+
- Comprehensive docstrings
|
| 253 |
+
|
| 254 |
+
### ✅ Documentation
|
| 255 |
+
- Complete API reference
|
| 256 |
+
- Usage examples
|
| 257 |
+
- Deployment guide
|
| 258 |
+
- Troubleshooting section
|
| 259 |
+
|
| 260 |
+
### ✅ Testing
|
| 261 |
+
- All modules importable
|
| 262 |
+
- Example script provided
|
| 263 |
+
- Logging verified
|
| 264 |
+
- Output format validated
|
| 265 |
+
|
| 266 |
+
---
|
| 267 |
+
|
| 268 |
+
## Version Control
|
| 269 |
+
|
| 270 |
+
**Recommended .gitignore:**
|
| 271 |
+
```
|
| 272 |
+
.env
|
| 273 |
+
*.log
|
| 274 |
+
*.json
|
| 275 |
+
__pycache__/
|
| 276 |
+
*.pyc
|
| 277 |
+
venv/
|
| 278 |
+
```
|
| 279 |
+
|
| 280 |
+
---
|
| 281 |
+
|
| 282 |
+
## Contact
|
| 283 |
+
|
| 284 |
+
For questions or issues:
|
| 285 |
+
1. Check `DEVELOPER_GUIDE.md`
|
| 286 |
+
2. Review `crop_stress_pipeline.log`
|
| 287 |
+
3. Contact: SIH ML Team
|
| 288 |
+
|
| 289 |
+
---
|
| 290 |
+
|
| 291 |
+
## Changelog
|
| 292 |
+
|
| 293 |
+
### v1.0 (2024-12-04)
|
| 294 |
+
- Initial production release
|
| 295 |
+
- Converted from Jupyter notebook
|
| 296 |
+
- Removed all visualizations
|
| 297 |
+
- Added production logging
|
| 298 |
+
- Fixed clustering to k=3
|
| 299 |
+
- Added comprehensive documentation
|
| 300 |
+
- Created deployment checklist
|
| 301 |
+
- Added example usage script
|
| 302 |
+
|
| 303 |
+
---
|
| 304 |
+
|
| 305 |
+
**Package Ready for Handover to Developer** ✅
|
README.md
CHANGED
|
@@ -1,10 +1,367 @@
|
|
| 1 |
-
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
--
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Crop Stress Detection Pipeline - Production Package
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
Production-ready pipeline for comprehensive crop stress analysis using Sentinel-2 satellite imagery, deep learning, and LLM-powered insights.
|
| 6 |
+
|
| 7 |
+
**Version:** 1.0
|
| 8 |
+
**Date:** 2024-12-04
|
| 9 |
+
**Python:** 3.9+
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## Features
|
| 14 |
+
|
| 15 |
+
✅ **13 Vegetation Indices**: NDVI, EVI, NDWI, NDRE, RECI, SMI, NDSI, PRI, PSRI, MCARI, SASI, SOMI, SFI
|
| 16 |
+
✅ **Temporal Analysis**: Multi-temporal statistics and trend detection
|
| 17 |
+
✅ **Deep Learning**: CNN + LSTM spatial-temporal encoding
|
| 18 |
+
✅ **Stress Clustering**: K-Means clustering (k=3: low, moderate, high)
|
| 19 |
+
✅ **Anomaly Detection**: Isolation Forest for unusual patterns
|
| 20 |
+
✅ **LLM Analysis**: Gemini-powered comprehensive insights
|
| 21 |
+
✅ **Production Logging**: Comprehensive logging for monitoring
|
| 22 |
+
✅ **JSON Output**: Structured results for easy integration
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
## Quick Start
|
| 27 |
+
|
| 28 |
+
### 1. Install Dependencies
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
pip install -r requirements.txt
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
### 2. Configure Environment
|
| 35 |
+
|
| 36 |
+
Copy `.env.template` to `.env` and fill in your credentials:
|
| 37 |
+
|
| 38 |
+
```env
|
| 39 |
+
SH_CLIENT_ID=your_sentinel_hub_client_id
|
| 40 |
+
SH_CLIENT_SECRET=your_sentinel_hub_secret
|
| 41 |
+
GEMINI_API_KEY=your_gemini_api_key
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
### 3. Run Pipeline
|
| 45 |
+
|
| 46 |
+
```python
|
| 47 |
+
from crop_stress_pipeline import CropStressPipeline
|
| 48 |
+
|
| 49 |
+
pipeline = CropStressPipeline()
|
| 50 |
+
|
| 51 |
+
results = pipeline.run(
|
| 52 |
+
center_lat=30.2300,
|
| 53 |
+
center_lon=75.8300,
|
| 54 |
+
crop_type='Wheat',
|
| 55 |
+
analysis_date='2024-01-15',
|
| 56 |
+
field_size_hectares=0.04,
|
| 57 |
+
farmer_context={
|
| 58 |
+
'role': 'Owner-Operator',
|
| 59 |
+
'years_farming': 15,
|
| 60 |
+
'irrigation_method': 'Drip Irrigation',
|
| 61 |
+
'farming_goal': 'Maximize yield'
|
| 62 |
+
},
|
| 63 |
+
output_path='results.json'
|
| 64 |
+
)
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
---
|
| 68 |
+
|
| 69 |
+
## Package Contents
|
| 70 |
+
|
| 71 |
+
### Core Files (9 files)
|
| 72 |
+
|
| 73 |
+
| File | Description | Size |
|
| 74 |
+
|------|-------------|------|
|
| 75 |
+
| `crop_stress_pipeline.py` | Main pipeline orchestrator | ~15 KB |
|
| 76 |
+
| `vegetation_indices.py` | 13 vegetation indices calculation | ~8 KB |
|
| 77 |
+
| `stress_detection_preprocessing.py` | Data preprocessing for DL | ~7 KB |
|
| 78 |
+
| `stress_detection_model.py` | CNN+LSTM stress detection | ~19 KB |
|
| 79 |
+
| `llm_analysis.py` | LLM integration & prompts | ~19 KB |
|
| 80 |
+
| `requirements.txt` | Python dependencies | ~200 B |
|
| 81 |
+
| `.env.template` | Environment variables template | ~150 B |
|
| 82 |
+
| `DEVELOPER_GUIDE.md` | Complete documentation | ~12 KB |
|
| 83 |
+
| `DEPLOYMENT_CHECKLIST.md` | Deployment guide | ~6 KB |
|
| 84 |
+
|
| 85 |
+
### Optional Files
|
| 86 |
+
|
| 87 |
+
| File | Description |
|
| 88 |
+
|------|-------------|
|
| 89 |
+
| `example_usage.py` | Example usage script |
|
| 90 |
+
| `README.md` | This file |
|
| 91 |
+
|
| 92 |
+
**Total Package Size:** ~90 KB (excluding dependencies)
|
| 93 |
+
|
| 94 |
+
---
|
| 95 |
+
|
| 96 |
+
## Architecture
|
| 97 |
+
|
| 98 |
+
```
|
| 99 |
+
┌─────────────────────────────────────────────────────────┐
|
| 100 |
+
│ INPUT PARAMETERS │
|
| 101 |
+
│ (lat, lon, crop_type, date, farmer_context) │
|
| 102 |
+
└────────────────────┬────────────────────────────────────┘
|
| 103 |
+
│
|
| 104 |
+
▼
|
| 105 |
+
┌─────────────────────────────────────────────────────────┐
|
| 106 |
+
│ STEP 1: SATELLITE DATA ACQUISITION │
|
| 107 |
+
│ • Sentinel Hub API (CDSE) │
|
| 108 |
+
│ • 10 cloud-free images (< 20% cloud cover) │
|
| 109 |
+
│ • 10m resolution, 13 spectral bands │
|
| 110 |
+
└────────────────────┬────────────────────────────────────┘
|
| 111 |
+
│
|
| 112 |
+
▼
|
| 113 |
+
┌─────────────────────────────────────────────────────────┐
|
| 114 |
+
│ STEP 2: VEGETATION INDICES CALCULATION │
|
| 115 |
+
│ • 13 indices calculated per pixel per timestamp │
|
| 116 |
+
│ • Temporal statistics (mean, std, trend, rolling avg) │
|
| 117 |
+
└────────────────────┬────────────────────────────────────┘
|
| 118 |
+
│
|
| 119 |
+
▼
|
| 120 |
+
┌─────────────────────────────────────────────────────────┐
|
| 121 |
+
│ STEP 3: STRESS DETECTION (DEEP LEARNING) │
|
| 122 |
+
│ • Preprocessing: 8x8 patches, stride=4 │
|
| 123 |
+
│ • Spatial Encoding: CNN (32→64→128 filters) │
|
| 124 |
+
│ • Temporal Encoding: Bidirectional LSTM (64 units) │
|
| 125 |
+
│ • Clustering: K-Means (k=3) │
|
| 126 |
+
│ • Anomaly Detection: Isolation Forest (10% contam.) │
|
| 127 |
+
└────────────────────┬────────────────────────────────────┘
|
| 128 |
+
│
|
| 129 |
+
▼
|
| 130 |
+
┌─────────────────────────────────────────────────────────┐
|
| 131 |
+
│ STEP 4: LLM ANALYSIS (GEMINI) │
|
| 132 |
+
│ • Input: Indices + Temporal Stats + Stress Context │
|
| 133 |
+
│ • Output: Soil, stress, fertility, health insights │
|
| 134 |
+
│ • Format: Structured JSON │
|
| 135 |
+
└────────────────────┬────────────────────────────────────┘
|
| 136 |
+
│
|
| 137 |
+
▼
|
| 138 |
+
┌─────────────────────────────────────────────────────────┐
|
| 139 |
+
│ JSON OUTPUT FILE │
|
| 140 |
+
│ • Metadata, indices, stress detection, LLM analysis │
|
| 141 |
+
└─────────────────────────────────────────────────────────┘
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
---
|
| 145 |
+
|
| 146 |
+
## Output Format
|
| 147 |
+
|
| 148 |
+
```json
|
| 149 |
+
{
|
| 150 |
+
"metadata": {
|
| 151 |
+
"crop_type": "Wheat",
|
| 152 |
+
"analysis_date": "2024-01-15",
|
| 153 |
+
"location": {"lat": 30.23, "lon": 75.83},
|
| 154 |
+
"num_images": 10
|
| 155 |
+
},
|
| 156 |
+
"vegetation_indices_summary": {
|
| 157 |
+
"indices": {
|
| 158 |
+
"NDVI": {
|
| 159 |
+
"latest": {"mean": 0.65},
|
| 160 |
+
"change": 0.15
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
},
|
| 164 |
+
"stress_detection": {
|
| 165 |
+
"field_statistics": {
|
| 166 |
+
"overall_stress": {"mean": 0.45}
|
| 167 |
+
},
|
| 168 |
+
"cluster_statistics": [
|
| 169 |
+
{
|
| 170 |
+
"cluster_id": 0,
|
| 171 |
+
"stress_score": {"mean": 0.25},
|
| 172 |
+
"temporal_trends": {
|
| 173 |
+
"B08": {"trend_direction": "increasing"}
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
]
|
| 177 |
+
},
|
| 178 |
+
"llm_analysis": {
|
| 179 |
+
"soil_moisture": {"level": "moderate"},
|
| 180 |
+
"overall_health": {"status": "good"}
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
---
|
| 186 |
+
|
| 187 |
+
## System Requirements
|
| 188 |
+
|
| 189 |
+
### Minimum
|
| 190 |
+
- **CPU**: 2 cores
|
| 191 |
+
- **RAM**: 4 GB
|
| 192 |
+
- **Disk**: 1 GB free space
|
| 193 |
+
- **Python**: 3.9+
|
| 194 |
+
- **Internet**: Required (API calls)
|
| 195 |
+
|
| 196 |
+
### Recommended
|
| 197 |
+
- **CPU**: 4+ cores
|
| 198 |
+
- **RAM**: 8 GB
|
| 199 |
+
- **Disk**: 5 GB free space
|
| 200 |
+
- **Python**: 3.10+
|
| 201 |
+
- **GPU**: Optional (speeds up DL inference)
|
| 202 |
+
|
| 203 |
+
---
|
| 204 |
+
|
| 205 |
+
## API Credentials
|
| 206 |
+
|
| 207 |
+
### Sentinel Hub (CDSE)
|
| 208 |
+
- **Register**: https://dataspace.copernicus.eu/
|
| 209 |
+
- **Free Tier**: 30,000 processing units/month
|
| 210 |
+
- **Usage**: ~100 PU per field analysis
|
| 211 |
+
|
| 212 |
+
### Google Gemini
|
| 213 |
+
- **Get Key**: https://makersuite.google.com/app/apikey
|
| 214 |
+
- **Free Tier**: 60 requests/minute, 1500/day
|
| 215 |
+
- **Usage**: 1 request per field analysis
|
| 216 |
+
|
| 217 |
+
---
|
| 218 |
+
|
| 219 |
+
## Performance
|
| 220 |
+
|
| 221 |
+
| Field Size | Processing Time | Memory Usage |
|
| 222 |
+
|------------|----------------|--------------|
|
| 223 |
+
| 0.04 ha (small) | 2-3 minutes | 2-3 GB |
|
| 224 |
+
| 1 ha (medium) | 5-10 minutes | 3-5 GB |
|
| 225 |
+
| 10 ha (large) | 20-30 minutes | 6-8 GB |
|
| 226 |
+
|
| 227 |
+
**Bottlenecks:**
|
| 228 |
+
1. Satellite data download (30%)
|
| 229 |
+
2. Deep learning inference (40%)
|
| 230 |
+
3. LLM API call (20%)
|
| 231 |
+
4. Index calculation (10%)
|
| 232 |
+
|
| 233 |
+
---
|
| 234 |
+
|
| 235 |
+
## Configuration
|
| 236 |
+
|
| 237 |
+
### Fixed Parameters (Production)
|
| 238 |
+
- **Clusters**: 3 (low, moderate, high stress)
|
| 239 |
+
- **Patch Size**: 8x8 pixels
|
| 240 |
+
- **Stride**: 4 pixels
|
| 241 |
+
- **Contamination**: 0.1 (10% anomalies)
|
| 242 |
+
- **Resolution**: 10m per pixel
|
| 243 |
+
- **Images**: 10 most recent cloud-free
|
| 244 |
+
|
| 245 |
+
### Customizable Parameters
|
| 246 |
+
- `center_lat`, `center_lon`: Field location
|
| 247 |
+
- `crop_type`: Crop being analyzed
|
| 248 |
+
- `analysis_date`: Target date
|
| 249 |
+
- `field_size_hectares`: Field size
|
| 250 |
+
- `farmer_context`: Farmer profile
|
| 251 |
+
|
| 252 |
+
---
|
| 253 |
+
|
| 254 |
+
## Logging
|
| 255 |
+
|
| 256 |
+
All operations logged to:
|
| 257 |
+
- **Console**: Real-time progress
|
| 258 |
+
- **File**: `crop_stress_pipeline.log`
|
| 259 |
+
|
| 260 |
+
**Log Levels:**
|
| 261 |
+
- `INFO`: Normal operations
|
| 262 |
+
- `ERROR`: Failures
|
| 263 |
+
|
| 264 |
+
**Example:**
|
| 265 |
+
```
|
| 266 |
+
2024-12-04 18:45:00 - INFO - Pipeline initialized
|
| 267 |
+
2024-12-04 18:45:30 - INFO - Found 10 suitable images
|
| 268 |
+
2024-12-04 18:46:00 - INFO - Calculated 13 indices
|
| 269 |
+
2024-12-04 18:46:30 - INFO - PIPELINE COMPLETED SUCCESSFULLY
|
| 270 |
+
```
|
| 271 |
+
|
| 272 |
+
---
|
| 273 |
+
|
| 274 |
+
## Error Handling
|
| 275 |
+
|
| 276 |
+
Common errors and solutions:
|
| 277 |
+
|
| 278 |
+
| Error | Solution |
|
| 279 |
+
|-------|----------|
|
| 280 |
+
| Missing credentials | Check `.env` file |
|
| 281 |
+
| No images found | Adjust `analysis_date` |
|
| 282 |
+
| LLM API error | Verify `GEMINI_API_KEY` |
|
| 283 |
+
| Out of memory | Reduce AOI size or increase RAM |
|
| 284 |
+
|
| 285 |
+
---
|
| 286 |
+
|
| 287 |
+
## Integration Examples
|
| 288 |
+
|
| 289 |
+
### REST API
|
| 290 |
+
|
| 291 |
+
```python
|
| 292 |
+
from flask import Flask, request, jsonify
|
| 293 |
+
from crop_stress_pipeline import CropStressPipeline
|
| 294 |
+
|
| 295 |
+
app = Flask(__name__)
|
| 296 |
+
pipeline = CropStressPipeline()
|
| 297 |
+
|
| 298 |
+
@app.route('/analyze', methods=['POST'])
|
| 299 |
+
def analyze():
|
| 300 |
+
data = request.json
|
| 301 |
+
results = pipeline.run(**data)
|
| 302 |
+
return jsonify(results)
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
### Batch Processing
|
| 306 |
+
|
| 307 |
+
```python
|
| 308 |
+
import pandas as pd
|
| 309 |
+
|
| 310 |
+
fields = pd.read_csv('fields.csv')
|
| 311 |
+
|
| 312 |
+
for _, field in fields.iterrows():
|
| 313 |
+
results = pipeline.run(
|
| 314 |
+
center_lat=field['lat'],
|
| 315 |
+
center_lon=field['lon'],
|
| 316 |
+
crop_type=field['crop'],
|
| 317 |
+
analysis_date=field['date'],
|
| 318 |
+
field_size_hectares=field['size'],
|
| 319 |
+
farmer_context={...},
|
| 320 |
+
output_path=f"results_{field['id']}.json"
|
| 321 |
+
)
|
| 322 |
+
```
|
| 323 |
+
|
| 324 |
+
---
|
| 325 |
+
|
| 326 |
+
## Files to Send to Developer
|
| 327 |
+
|
| 328 |
+
**Required (9 files):**
|
| 329 |
+
1. `crop_stress_pipeline.py`
|
| 330 |
+
2. `vegetation_indices.py`
|
| 331 |
+
3. `stress_detection_preprocessing.py`
|
| 332 |
+
4. `stress_detection_model.py`
|
| 333 |
+
5. `llm_analysis.py`
|
| 334 |
+
6. `requirements.txt`
|
| 335 |
+
7. `.env.template`
|
| 336 |
+
8. `DEVELOPER_GUIDE.md`
|
| 337 |
+
9. `DEPLOYMENT_CHECKLIST.md`
|
| 338 |
+
|
| 339 |
+
**Optional:**
|
| 340 |
+
- `example_usage.py`
|
| 341 |
+
- `README.md`
|
| 342 |
+
|
| 343 |
+
---
|
| 344 |
+
|
| 345 |
+
## Support
|
| 346 |
+
|
| 347 |
+
- **Documentation**: See `DEVELOPER_GUIDE.md`
|
| 348 |
+
- **Deployment**: See `DEPLOYMENT_CHECKLIST.md`
|
| 349 |
+
- **Logs**: Check `crop_stress_pipeline.log`
|
| 350 |
+
|
| 351 |
+
---
|
| 352 |
+
|
| 353 |
+
## License
|
| 354 |
+
|
| 355 |
+
Internal use only - SIH ML Team
|
| 356 |
+
|
| 357 |
+
---
|
| 358 |
+
|
| 359 |
+
## Version History
|
| 360 |
+
|
| 361 |
+
- **v1.0** (2024-12-04): Initial production release
|
| 362 |
+
- 13 vegetation indices
|
| 363 |
+
- CNN+LSTM stress detection
|
| 364 |
+
- Fixed 3-cluster configuration
|
| 365 |
+
- Gemini LLM integration
|
| 366 |
+
- Production logging
|
| 367 |
+
- No visualizations (production-ready)
|
app.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import uvicorn
|
| 3 |
+
from fastapi import FastAPI, HTTPException
|
| 4 |
+
from pydantic import BaseModel
|
| 5 |
+
from typing import Dict, Any, Optional
|
| 6 |
+
from crop_stress_pipeline import CropStressPipeline
|
| 7 |
+
|
| 8 |
+
app = FastAPI(title="Sentinel-2 Crop Stress Pipeline")
|
| 9 |
+
|
| 10 |
+
# Initialize pipeline
|
| 11 |
+
pipeline = CropStressPipeline()
|
| 12 |
+
|
| 13 |
+
class AnalysisRequest(BaseModel):
|
| 14 |
+
center_lat: float
|
| 15 |
+
center_lon: float
|
| 16 |
+
crop_type: str
|
| 17 |
+
analysis_date: str
|
| 18 |
+
field_size_hectares: float
|
| 19 |
+
farmer_context: Dict[str, Any]
|
| 20 |
+
|
| 21 |
+
@app.get("/")
|
| 22 |
+
def home():
|
| 23 |
+
return {"status": "running", "message": "Sentinel-2 Crop Stress Pipeline API"}
|
| 24 |
+
|
| 25 |
+
@app.post("/analyze")
|
| 26 |
+
async def analyze_crop(request: AnalysisRequest):
|
| 27 |
+
try:
|
| 28 |
+
results = pipeline.run(
|
| 29 |
+
center_lat=request.center_lat,
|
| 30 |
+
center_lon=request.center_lon,
|
| 31 |
+
crop_type=request.crop_type,
|
| 32 |
+
analysis_date=request.analysis_date,
|
| 33 |
+
field_size_hectares=request.field_size_hectares,
|
| 34 |
+
farmer_context=request.farmer_context
|
| 35 |
+
)
|
| 36 |
+
return results
|
| 37 |
+
except Exception as e:
|
| 38 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 39 |
+
|
| 40 |
+
if __name__ == "__main__":
|
| 41 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
crop_stress_pipeline.py
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Crop Stress Detection Pipeline - Production Version
|
| 3 |
+
====================================================
|
| 4 |
+
|
| 5 |
+
Complete pipeline for crop monitoring using Sentinel-2 satellite data.
|
| 6 |
+
Includes vegetation indices calculation, deep learning stress detection,
|
| 7 |
+
and LLM-powered analysis.
|
| 8 |
+
|
| 9 |
+
Author: SIH ML Team
|
| 10 |
+
Version: 1.0
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
import sys
|
| 15 |
+
import json
|
| 16 |
+
import logging
|
| 17 |
+
import numpy as np
|
| 18 |
+
from datetime import datetime, timedelta, timezone
|
| 19 |
+
from dotenv import load_dotenv
|
| 20 |
+
|
| 21 |
+
from sentinelhub import (
|
| 22 |
+
SHConfig, BBox, CRS, DataCollection, SentinelHubRequest,
|
| 23 |
+
MimeType, bbox_to_dimensions, SentinelHubCatalog
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# Import custom modules
|
| 27 |
+
from vegetation_indices import calculate_indices_temporal, get_summary_report, get_temporal_statistics
|
| 28 |
+
from stress_detection_preprocessing import preprocess_for_model
|
| 29 |
+
from stress_detection_model import StressDetectionModel, prepare_llm_context, get_stress_category
|
| 30 |
+
from llm_analysis import analyze_with_llm
|
| 31 |
+
|
| 32 |
+
# Configure logging
|
| 33 |
+
logging.basicConfig(
|
| 34 |
+
level=logging.INFO,
|
| 35 |
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
| 36 |
+
handlers=[
|
| 37 |
+
logging.FileHandler('crop_stress_pipeline.log'),
|
| 38 |
+
logging.StreamHandler()
|
| 39 |
+
]
|
| 40 |
+
)
|
| 41 |
+
logger = logging.getLogger(__name__)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class CropStressPipeline:
|
| 45 |
+
"""
|
| 46 |
+
Production pipeline for crop stress detection and analysis.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def __init__(self, config_path: str = None):
|
| 50 |
+
"""
|
| 51 |
+
Initialize pipeline with configuration.
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
config_path: Path to .env file with credentials
|
| 55 |
+
"""
|
| 56 |
+
# Load environment variables
|
| 57 |
+
if config_path:
|
| 58 |
+
load_dotenv(config_path)
|
| 59 |
+
else:
|
| 60 |
+
load_dotenv()
|
| 61 |
+
|
| 62 |
+
# Configure Sentinel Hub
|
| 63 |
+
self.config = SHConfig()
|
| 64 |
+
self.config.sh_client_id = os.environ.get('SH_CLIENT_ID')
|
| 65 |
+
self.config.sh_client_secret = os.environ.get('SH_CLIENT_SECRET')
|
| 66 |
+
self.config.sh_base_url = 'https://sh.dataspace.copernicus.eu'
|
| 67 |
+
self.config.sh_token_url = 'https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token'
|
| 68 |
+
|
| 69 |
+
if not self.config.sh_client_id or not self.config.sh_client_secret:
|
| 70 |
+
raise ValueError('Sentinel Hub credentials not found in environment variables')
|
| 71 |
+
|
| 72 |
+
# Define custom CDSE data collection
|
| 73 |
+
self.SENTINEL2_L2A_CDSE = DataCollection.define(
|
| 74 |
+
"SENTINEL2_L2A_CDSE",
|
| 75 |
+
api_id="sentinel-2-l2a",
|
| 76 |
+
service_url="https://sh.dataspace.copernicus.eu",
|
| 77 |
+
collection_type="Sentinel-2",
|
| 78 |
+
is_timeless=False
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
logger.info("Pipeline initialized successfully")
|
| 82 |
+
logger.info(f"Sentinel Hub: {self.config.sh_base_url}")
|
| 83 |
+
logger.info(f"Client ID: {self.config.sh_client_id[:20]}...")
|
| 84 |
+
|
| 85 |
+
def create_evalscript(self) -> str:
|
| 86 |
+
"""Create evalscript for Sentinel-2 data retrieval."""
|
| 87 |
+
return """
|
| 88 |
+
//VERSION=3
|
| 89 |
+
function setup() {
|
| 90 |
+
return {
|
| 91 |
+
input: [{
|
| 92 |
+
bands: ["B01", "B02", "B03", "B04", "B05", "B06", "B07", "B08", "B8A", "B09", "B11", "B12", "SCL"],
|
| 93 |
+
units: "DN"
|
| 94 |
+
}],
|
| 95 |
+
output: {
|
| 96 |
+
bands: 13,
|
| 97 |
+
sampleType: "FLOAT32"
|
| 98 |
+
}
|
| 99 |
+
};
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
function evaluatePixel(sample) {
|
| 103 |
+
return [
|
| 104 |
+
sample.B01 / 10000,
|
| 105 |
+
sample.B02 / 10000,
|
| 106 |
+
sample.B03 / 10000,
|
| 107 |
+
sample.B04 / 10000,
|
| 108 |
+
sample.B05 / 10000,
|
| 109 |
+
sample.B06 / 10000,
|
| 110 |
+
sample.B07 / 10000,
|
| 111 |
+
sample.B08 / 10000,
|
| 112 |
+
sample.B8A / 10000,
|
| 113 |
+
sample.B09 / 10000,
|
| 114 |
+
sample.B11 / 10000,
|
| 115 |
+
sample.B12 / 10000,
|
| 116 |
+
sample.SCL
|
| 117 |
+
];
|
| 118 |
+
}
|
| 119 |
+
"""
|
| 120 |
+
|
| 121 |
+
def fetch_satellite_data(self, center_lat: float, center_lon: float,
|
| 122 |
+
analysis_date: str, num_images: int = 10,
|
| 123 |
+
resolution: int = 10) -> tuple:
|
| 124 |
+
"""
|
| 125 |
+
Fetch Sentinel-2 satellite data for the specified location and date range.
|
| 126 |
+
|
| 127 |
+
Args:
|
| 128 |
+
center_lat: Latitude of field center
|
| 129 |
+
center_lon: Longitude of field center
|
| 130 |
+
analysis_date: Target analysis date (YYYY-MM-DD)
|
| 131 |
+
num_images: Number of temporal images to fetch
|
| 132 |
+
resolution: Spatial resolution in meters
|
| 133 |
+
|
| 134 |
+
Returns:
|
| 135 |
+
Tuple of (all_images, selected_dates, bbox, size)
|
| 136 |
+
"""
|
| 137 |
+
logger.info("=" * 60)
|
| 138 |
+
logger.info("FETCHING SATELLITE DATA")
|
| 139 |
+
logger.info("=" * 60)
|
| 140 |
+
|
| 141 |
+
# Create bounding box
|
| 142 |
+
coords_wgs84 = BBox(
|
| 143 |
+
bbox=[center_lon - 0.001, center_lat - 0.001,
|
| 144 |
+
center_lon + 0.001, center_lat + 0.001],
|
| 145 |
+
crs=CRS.WGS84
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
size = bbox_to_dimensions(coords_wgs84, resolution=resolution)
|
| 149 |
+
logger.info(f"AOI size: {size[0]}x{size[1]} pixels at {resolution}m resolution")
|
| 150 |
+
|
| 151 |
+
# Search for cloud-free images
|
| 152 |
+
target_date = datetime.strptime(analysis_date, '%Y-%m-%d').replace(tzinfo=timezone.utc)
|
| 153 |
+
search_start = target_date - timedelta(days=90)
|
| 154 |
+
search_end = target_date + timedelta(days=30)
|
| 155 |
+
|
| 156 |
+
logger.info(f"Searching for {num_images} cloud-free images...")
|
| 157 |
+
logger.info(f"Date range: {search_start.date()} to {search_end.date()}")
|
| 158 |
+
|
| 159 |
+
catalog = SentinelHubCatalog(config=self.config)
|
| 160 |
+
search_iterator = catalog.search(
|
| 161 |
+
DataCollection.SENTINEL2_L2A,
|
| 162 |
+
bbox=coords_wgs84,
|
| 163 |
+
time=(search_start, search_end),
|
| 164 |
+
filter='eo:cloud_cover < 20'
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
all_timestamps = []
|
| 168 |
+
for item in search_iterator:
|
| 169 |
+
timestamp = item['properties']['datetime']
|
| 170 |
+
cloud_cover = item['properties'].get('eo:cloud_cover', 0)
|
| 171 |
+
all_timestamps.append((timestamp, cloud_cover))
|
| 172 |
+
|
| 173 |
+
# Sort by proximity to target date
|
| 174 |
+
all_timestamps.sort(key=lambda x: abs((datetime.fromisoformat(x[0].replace('Z', '+00:00')) - target_date).days))
|
| 175 |
+
selected_dates = [t[0] for t in all_timestamps[:num_images]]
|
| 176 |
+
|
| 177 |
+
logger.info(f"Found {len(selected_dates)} suitable images:")
|
| 178 |
+
for i, (date, cloud) in enumerate(all_timestamps[:num_images]):
|
| 179 |
+
logger.info(f" [{i+1}] {date[:10]} (Cloud: {cloud:.1f}%)")
|
| 180 |
+
|
| 181 |
+
# Fetch data for all timestamps
|
| 182 |
+
logger.info("Fetching satellite data...")
|
| 183 |
+
all_images = []
|
| 184 |
+
evalscript = self.create_evalscript()
|
| 185 |
+
|
| 186 |
+
for i, date in enumerate(selected_dates):
|
| 187 |
+
request = SentinelHubRequest(
|
| 188 |
+
evalscript=evalscript,
|
| 189 |
+
input_data=[SentinelHubRequest.input_data(
|
| 190 |
+
data_collection=self.SENTINEL2_L2A_CDSE,
|
| 191 |
+
time_interval=(date, date)
|
| 192 |
+
)],
|
| 193 |
+
responses=[SentinelHubRequest.output_response('default', MimeType.TIFF)],
|
| 194 |
+
bbox=coords_wgs84,
|
| 195 |
+
size=size,
|
| 196 |
+
config=self.config
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
data = request.get_data()[0]
|
| 200 |
+
valid_pct = 100 * np.sum(data[:,:,12] > 0) / (data.shape[0] * data.shape[1])
|
| 201 |
+
all_images.append(data)
|
| 202 |
+
logger.info(f" [{i+1}/{len(selected_dates)}] {date[:10]} - {valid_pct:.1f}% valid pixels")
|
| 203 |
+
|
| 204 |
+
all_images = np.array(all_images)
|
| 205 |
+
logger.info(f"Data shape: {all_images.shape} (time, height, width, bands)")
|
| 206 |
+
logger.info("=" * 60)
|
| 207 |
+
|
| 208 |
+
return all_images, selected_dates, coords_wgs84, size
|
| 209 |
+
|
| 210 |
+
def calculate_vegetation_indices(self, all_images: np.ndarray,
|
| 211 |
+
selected_dates: list) -> tuple:
|
| 212 |
+
"""
|
| 213 |
+
Calculate all 13 vegetation indices and temporal statistics.
|
| 214 |
+
|
| 215 |
+
Args:
|
| 216 |
+
all_images: Satellite imagery array
|
| 217 |
+
selected_dates: List of image dates
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
Tuple of (indices_data, summary_report, temporal_stats)
|
| 221 |
+
"""
|
| 222 |
+
logger.info("=" * 60)
|
| 223 |
+
logger.info("CALCULATING VEGETATION INDICES")
|
| 224 |
+
logger.info("=" * 60)
|
| 225 |
+
|
| 226 |
+
indices_data = calculate_indices_temporal(all_images)
|
| 227 |
+
logger.info(f"Calculated {len(indices_data)} indices:")
|
| 228 |
+
for index_name in indices_data.keys():
|
| 229 |
+
logger.info(f" - {index_name}")
|
| 230 |
+
|
| 231 |
+
summary_report = get_summary_report(indices_data, selected_dates)
|
| 232 |
+
temporal_stats = get_temporal_statistics(indices_data)
|
| 233 |
+
|
| 234 |
+
logger.info("\nSummary Statistics:")
|
| 235 |
+
logger.info(f"Analysis Period: {selected_dates[0][:10]} to {selected_dates[-1][:10]}")
|
| 236 |
+
logger.info(f"Number of Images: {summary_report['num_images']}")
|
| 237 |
+
|
| 238 |
+
for index_name, stats in summary_report['indices'].items():
|
| 239 |
+
logger.info(f"\n{index_name}:")
|
| 240 |
+
logger.info(f" Latest Mean: {stats['latest']['mean']:.4f}")
|
| 241 |
+
logger.info(f" Max in Field: {stats['max_in_field']:.4f}")
|
| 242 |
+
logger.info(f" Min in Field: {stats['min_in_field']:.4f}")
|
| 243 |
+
logger.info(f" Temporal Change: {stats['change']:+.4f}")
|
| 244 |
+
|
| 245 |
+
logger.info("=" * 60)
|
| 246 |
+
return indices_data, summary_report, temporal_stats
|
| 247 |
+
|
| 248 |
+
def run_stress_detection(self, all_images: np.ndarray,
|
| 249 |
+
selected_dates: list,
|
| 250 |
+
n_clusters: int = 3) -> tuple:
|
| 251 |
+
"""
|
| 252 |
+
Run deep learning stress detection pipeline.
|
| 253 |
+
|
| 254 |
+
Args:
|
| 255 |
+
all_images: Satellite imagery array
|
| 256 |
+
selected_dates: List of image dates
|
| 257 |
+
n_clusters: Number of stress clusters (default: 3)
|
| 258 |
+
|
| 259 |
+
Returns:
|
| 260 |
+
Tuple of (stress_results, stress_llm_context, patches, patch_coords, metadata)
|
| 261 |
+
"""
|
| 262 |
+
logger.info("=" * 60)
|
| 263 |
+
logger.info("STRESS DETECTION PIPELINE")
|
| 264 |
+
logger.info("=" * 60)
|
| 265 |
+
|
| 266 |
+
# Preprocess data
|
| 267 |
+
logger.info("Preprocessing data for stress detection...")
|
| 268 |
+
patches, patch_coords, metadata = preprocess_for_model(
|
| 269 |
+
all_images,
|
| 270 |
+
patch_size=8,
|
| 271 |
+
stride=4
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
logger.info(f"Original shape: {metadata['original_shape']}")
|
| 275 |
+
logger.info(f"Selected bands: {metadata['selected_bands']}")
|
| 276 |
+
logger.info(f"Number of patches: {metadata['num_patches']}")
|
| 277 |
+
logger.info(f"Patch shape: {patches.shape}")
|
| 278 |
+
|
| 279 |
+
# Build and run stress detection model
|
| 280 |
+
logger.info("Building stress detection model...")
|
| 281 |
+
stress_model = StressDetectionModel(
|
| 282 |
+
patch_size=metadata['patch_size'],
|
| 283 |
+
num_bands=metadata['num_bands'],
|
| 284 |
+
num_timestamps=len(selected_dates),
|
| 285 |
+
spatial_embedding_dim=128,
|
| 286 |
+
temporal_embedding_dim=128
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
logger.info(f"Running stress detection with {n_clusters} clusters...")
|
| 290 |
+
stress_results = stress_model.predict(patches, n_clusters=n_clusters, contamination=0.1)
|
| 291 |
+
|
| 292 |
+
logger.info("\nStress Detection Results:")
|
| 293 |
+
logger.info(f" Spatial embeddings: {stress_results['spatial_embeddings'].shape}")
|
| 294 |
+
logger.info(f" Temporal embeddings: {stress_results['temporal_embeddings'].shape}")
|
| 295 |
+
logger.info(f" Stress scores: min={stress_results['stress_scores'].min():.3f}, "
|
| 296 |
+
f"max={stress_results['stress_scores'].max():.3f}, "
|
| 297 |
+
f"mean={stress_results['stress_scores'].mean():.3f}")
|
| 298 |
+
logger.info(f" Clusters: {n_clusters}")
|
| 299 |
+
logger.info(f" Anomalies: {np.sum(stress_results['anomaly_labels'] == -1)}")
|
| 300 |
+
|
| 301 |
+
# Prepare context for LLM
|
| 302 |
+
logger.info("Preparing stress detection context for LLM...")
|
| 303 |
+
stress_llm_context = prepare_llm_context(
|
| 304 |
+
stress_results,
|
| 305 |
+
patch_coords,
|
| 306 |
+
patches,
|
| 307 |
+
metadata
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
# Log stress distribution
|
| 311 |
+
stress_categories = [get_stress_category(score) for score in stress_results['stress_scores']]
|
| 312 |
+
unique_categories, counts = np.unique(stress_categories, return_counts=True)
|
| 313 |
+
|
| 314 |
+
logger.info("\nStress Category Distribution:")
|
| 315 |
+
for category, count in zip(unique_categories, counts):
|
| 316 |
+
pct = 100 * count / len(stress_categories)
|
| 317 |
+
logger.info(f" {category}: {count} patches ({pct:.1f}%)")
|
| 318 |
+
|
| 319 |
+
logger.info(f"\nOverall Field Stress Score: {stress_results['stress_scores'].mean():.3f}")
|
| 320 |
+
logger.info(f"Field Stress Category: {get_stress_category(stress_results['stress_scores'].mean())}")
|
| 321 |
+
logger.info("=" * 60)
|
| 322 |
+
|
| 323 |
+
return stress_results, stress_llm_context, patches, patch_coords, metadata
|
| 324 |
+
|
| 325 |
+
def run_llm_analysis(self, summary_report: dict, temporal_stats: dict,
|
| 326 |
+
stress_llm_context: dict, crop_type: str,
|
| 327 |
+
farmer_context: dict, center_lat: float,
|
| 328 |
+
center_lon: float, field_size_hectares: float) -> dict:
|
| 329 |
+
"""
|
| 330 |
+
Run LLM analysis on vegetation indices and stress detection results.
|
| 331 |
+
|
| 332 |
+
Args:
|
| 333 |
+
summary_report: Vegetation indices summary
|
| 334 |
+
temporal_stats: Temporal statistics
|
| 335 |
+
stress_llm_context: Stress detection context
|
| 336 |
+
crop_type: Type of crop
|
| 337 |
+
farmer_context: Farmer profile information
|
| 338 |
+
center_lat: Field latitude
|
| 339 |
+
center_lon: Field longitude
|
| 340 |
+
field_size_hectares: Field size in hectares
|
| 341 |
+
|
| 342 |
+
Returns:
|
| 343 |
+
LLM analysis results dictionary
|
| 344 |
+
"""
|
| 345 |
+
logger.info("=" * 60)
|
| 346 |
+
logger.info("LLM ANALYSIS")
|
| 347 |
+
logger.info("=" * 60)
|
| 348 |
+
|
| 349 |
+
logger.info("Analyzing with LLM (Gemini)...")
|
| 350 |
+
llm_analysis = analyze_with_llm(
|
| 351 |
+
summary_report=summary_report,
|
| 352 |
+
crop_type=crop_type,
|
| 353 |
+
farmer_context=farmer_context,
|
| 354 |
+
center_lat=center_lat,
|
| 355 |
+
center_lon=center_lon,
|
| 356 |
+
field_size_hectares=field_size_hectares,
|
| 357 |
+
temporal_stats=temporal_stats,
|
| 358 |
+
stress_context=stress_llm_context
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
logger.info("\nLLM Analysis Results:")
|
| 362 |
+
logger.info(f" Soil Moisture: {llm_analysis['soil_moisture']['level']}")
|
| 363 |
+
logger.info(f" Soil Salinity: {llm_analysis['soil_salinity']['level']}")
|
| 364 |
+
logger.info(f" Organic Matter: {llm_analysis['organic_matter']['level']}")
|
| 365 |
+
logger.info(f" Soil Fertility: {llm_analysis['soil_fertility']['level']}")
|
| 366 |
+
logger.info(f" Vegetation Stress: {llm_analysis['vegetation_stress']['level']}")
|
| 367 |
+
logger.info(f" Photosynthetic Stress: {llm_analysis['photosynthetic_stress']['level']}")
|
| 368 |
+
logger.info(f" Overall Health: {llm_analysis['overall_health']['status']}")
|
| 369 |
+
logger.info("=" * 60)
|
| 370 |
+
|
| 371 |
+
return llm_analysis
|
| 372 |
+
|
| 373 |
+
def run(self, center_lat: float, center_lon: float, crop_type: str,
|
| 374 |
+
analysis_date: str, field_size_hectares: float,
|
| 375 |
+
farmer_context: dict, output_path: str = None) -> dict:
|
| 376 |
+
"""
|
| 377 |
+
Run complete crop stress detection pipeline.
|
| 378 |
+
|
| 379 |
+
Args:
|
| 380 |
+
center_lat: Field center latitude
|
| 381 |
+
center_lon: Field center longitude
|
| 382 |
+
crop_type: Type of crop being analyzed
|
| 383 |
+
analysis_date: Target analysis date (YYYY-MM-DD)
|
| 384 |
+
field_size_hectares: Field size in hectares
|
| 385 |
+
farmer_context: Dictionary with farmer profile information
|
| 386 |
+
output_path: Path to save results JSON (optional)
|
| 387 |
+
|
| 388 |
+
Returns:
|
| 389 |
+
Complete analysis results dictionary
|
| 390 |
+
"""
|
| 391 |
+
logger.info("\n" + "=" * 60)
|
| 392 |
+
logger.info("CROP STRESS DETECTION PIPELINE - STARTING")
|
| 393 |
+
logger.info("=" * 60)
|
| 394 |
+
logger.info(f"Crop Type: {crop_type}")
|
| 395 |
+
logger.info(f"Analysis Date: {analysis_date}")
|
| 396 |
+
logger.info(f"Location: ({center_lat:.4f}, {center_lon:.4f})")
|
| 397 |
+
logger.info(f"Field Size: {field_size_hectares} hectares")
|
| 398 |
+
logger.info("=" * 60)
|
| 399 |
+
|
| 400 |
+
try:
|
| 401 |
+
# Step 1: Fetch satellite data
|
| 402 |
+
all_images, selected_dates, bbox, size = self.fetch_satellite_data(
|
| 403 |
+
center_lat, center_lon, analysis_date
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
# Step 2: Calculate vegetation indices
|
| 407 |
+
indices_data, summary_report, temporal_stats = self.calculate_vegetation_indices(
|
| 408 |
+
all_images, selected_dates
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
# Step 3: Run stress detection
|
| 412 |
+
stress_results, stress_llm_context, patches, patch_coords, metadata = self.run_stress_detection(
|
| 413 |
+
all_images, selected_dates, n_clusters=3
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
# Step 4: Run LLM analysis
|
| 417 |
+
llm_analysis = self.run_llm_analysis(
|
| 418 |
+
summary_report, temporal_stats, stress_llm_context,
|
| 419 |
+
crop_type, farmer_context, center_lat, center_lon,
|
| 420 |
+
field_size_hectares
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
# Compile results
|
| 424 |
+
results = {
|
| 425 |
+
'metadata': {
|
| 426 |
+
'crop_type': crop_type,
|
| 427 |
+
'analysis_date': analysis_date,
|
| 428 |
+
'location': {'lat': center_lat, 'lon': center_lon},
|
| 429 |
+
'field_size_hectares': field_size_hectares,
|
| 430 |
+
'farmer_context': farmer_context,
|
| 431 |
+
'num_images': len(selected_dates),
|
| 432 |
+
'date_range': [selected_dates[0][:10], selected_dates[-1][:10]]
|
| 433 |
+
},
|
| 434 |
+
'vegetation_indices_summary': summary_report,
|
| 435 |
+
'stress_detection': stress_llm_context,
|
| 436 |
+
'llm_analysis': llm_analysis
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
# Save results
|
| 440 |
+
if output_path:
|
| 441 |
+
with open(output_path, 'w') as f:
|
| 442 |
+
json.dump(results, f, indent=2)
|
| 443 |
+
logger.info(f"\nResults saved to: {output_path}")
|
| 444 |
+
|
| 445 |
+
logger.info("\n" + "=" * 60)
|
| 446 |
+
logger.info("PIPELINE COMPLETED SUCCESSFULLY")
|
| 447 |
+
logger.info("=" * 60)
|
| 448 |
+
|
| 449 |
+
return results
|
| 450 |
+
|
| 451 |
+
except Exception as e:
|
| 452 |
+
logger.error(f"Pipeline failed with error: {str(e)}", exc_info=True)
|
| 453 |
+
raise
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
if __name__ == "__main__":
|
| 457 |
+
# Example usage
|
| 458 |
+
pipeline = CropStressPipeline()
|
| 459 |
+
|
| 460 |
+
# Define field parameters
|
| 461 |
+
params = {
|
| 462 |
+
'center_lat': 30.2300,
|
| 463 |
+
'center_lon': 75.8300,
|
| 464 |
+
'crop_type': 'Wheat',
|
| 465 |
+
'analysis_date': '2024-01-15',
|
| 466 |
+
'field_size_hectares': 0.04,
|
| 467 |
+
'farmer_context': {
|
| 468 |
+
'role': 'Owner-Operator',
|
| 469 |
+
'years_farming': 15,
|
| 470 |
+
'irrigation_method': 'Drip Irrigation',
|
| 471 |
+
'farming_goal': 'Maximize yield while maintaining soil health'
|
| 472 |
+
},
|
| 473 |
+
'output_path': 'crop_analysis_results.json'
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
# Run pipeline
|
| 477 |
+
results = pipeline.run(**params)
|
example_usage.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Example usage script for Crop Stress Detection Pipeline
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from crop_stress_pipeline import CropStressPipeline
|
| 6 |
+
import json
|
| 7 |
+
|
| 8 |
+
def main():
|
| 9 |
+
"""
|
| 10 |
+
Example: Analyze a wheat field in Punjab, India
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
# Initialize pipeline
|
| 14 |
+
print("Initializing pipeline...")
|
| 15 |
+
pipeline = CropStressPipeline()
|
| 16 |
+
|
| 17 |
+
# Define field parameters
|
| 18 |
+
field_params = {
|
| 19 |
+
'center_lat': 30.2300,
|
| 20 |
+
'center_lon': 75.8300,
|
| 21 |
+
'crop_type': 'Wheat',
|
| 22 |
+
'analysis_date': '2024-01-15',
|
| 23 |
+
'field_size_hectares': 0.04,
|
| 24 |
+
'farmer_context': {
|
| 25 |
+
'role': 'Owner-Operator',
|
| 26 |
+
'years_farming': 15,
|
| 27 |
+
'irrigation_method': 'Drip Irrigation',
|
| 28 |
+
'farming_goal': 'Maximize yield while maintaining soil health'
|
| 29 |
+
},
|
| 30 |
+
'output_path': 'example_results.json'
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
print("\nField Information:")
|
| 34 |
+
print(f" Location: ({field_params['center_lat']}, {field_params['center_lon']})")
|
| 35 |
+
print(f" Crop: {field_params['crop_type']}")
|
| 36 |
+
print(f" Date: {field_params['analysis_date']}")
|
| 37 |
+
print(f" Size: {field_params['field_size_hectares']} hectares")
|
| 38 |
+
|
| 39 |
+
# Run pipeline
|
| 40 |
+
print("\nRunning pipeline...")
|
| 41 |
+
try:
|
| 42 |
+
results = pipeline.run(**field_params)
|
| 43 |
+
|
| 44 |
+
print("\n" + "="*60)
|
| 45 |
+
print("PIPELINE COMPLETED SUCCESSFULLY")
|
| 46 |
+
print("="*60)
|
| 47 |
+
|
| 48 |
+
# Display key results
|
| 49 |
+
print("\nKey Results:")
|
| 50 |
+
print(f" Overall Health: {results['llm_analysis']['overall_health']['status'].upper()}")
|
| 51 |
+
print(f" Soil Moisture: {results['llm_analysis']['soil_moisture']['level'].upper()}")
|
| 52 |
+
print(f" Vegetation Stress: {results['llm_analysis']['vegetation_stress']['level'].upper()}")
|
| 53 |
+
|
| 54 |
+
print(f"\nFull results saved to: {field_params['output_path']}")
|
| 55 |
+
|
| 56 |
+
# Pretty print a sample of the results
|
| 57 |
+
print("\nSample Output (Vegetation Indices):")
|
| 58 |
+
ndvi_stats = results['vegetation_indices_summary']['indices']['NDVI']
|
| 59 |
+
print(f" NDVI Latest Mean: {ndvi_stats['latest']['mean']:.4f}")
|
| 60 |
+
print(f" NDVI Change: {ndvi_stats['change']:+.4f}")
|
| 61 |
+
|
| 62 |
+
return results
|
| 63 |
+
|
| 64 |
+
except Exception as e:
|
| 65 |
+
print(f"\nERROR: Pipeline failed - {str(e)}")
|
| 66 |
+
print("Check crop_stress_pipeline.log for details")
|
| 67 |
+
raise
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
if __name__ == "__main__":
|
| 71 |
+
results = main()
|
llm_analysis.py
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LLM Integration for Vegetation Indices Analysis
|
| 3 |
+
================================================
|
| 4 |
+
|
| 5 |
+
This module integrates with Google Gemini to analyze vegetation indices
|
| 6 |
+
and provide comprehensive soil and crop insights.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
import numpy as np
|
| 12 |
+
import google.generativeai as genai
|
| 13 |
+
from typing import Dict, Any
|
| 14 |
+
|
| 15 |
+
def configure_gemini():
|
| 16 |
+
"""Configure Gemini API with key from environment."""
|
| 17 |
+
api_key = os.environ.get("GEMINI_API_KEY")
|
| 18 |
+
if not api_key:
|
| 19 |
+
raise ValueError("GEMINI_API_KEY not found in environment variables")
|
| 20 |
+
genai.configure(api_key=api_key, transport='rest')
|
| 21 |
+
return genai.GenerativeModel('gemini-flash-latest')
|
| 22 |
+
|
| 23 |
+
def prepare_indices_context(summary_report: Dict, crop_type: str, farmer_context: Dict,
|
| 24 |
+
temporal_stats: Dict = None) -> str:
|
| 25 |
+
"""
|
| 26 |
+
Prepare a comprehensive context string for the LLM including temporal statistics.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
summary_report: Dictionary with all indices data
|
| 30 |
+
crop_type: Type of crop being analyzed
|
| 31 |
+
farmer_context: Farmer profile information
|
| 32 |
+
temporal_stats: Dictionary with temporal statistics (optional)
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
Formatted context string
|
| 36 |
+
"""
|
| 37 |
+
context = f"""
|
| 38 |
+
CROP MONITORING ANALYSIS REQUEST
|
| 39 |
+
|
| 40 |
+
CROP INFORMATION:
|
| 41 |
+
- Crop Type: {crop_type}
|
| 42 |
+
- Analysis Period: {summary_report['dates'][0]} to {summary_report['dates'][-1]}
|
| 43 |
+
- Number of Images Analyzed: {summary_report['num_images']}
|
| 44 |
+
|
| 45 |
+
FARMER CONTEXT:
|
| 46 |
+
- Role: {farmer_context['role']}
|
| 47 |
+
- Experience: {farmer_context['years_farming']} years
|
| 48 |
+
- Irrigation Method: {farmer_context['irrigation_method']}
|
| 49 |
+
- Farming Goal: {farmer_context['farming_goal']}
|
| 50 |
+
|
| 51 |
+
VEGETATION INDICES DATA (ALL 13 INDICES):
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
for index_name, stats in summary_report['indices'].items():
|
| 55 |
+
context += f"\n{index_name}:"
|
| 56 |
+
context += f"\n - Latest Mean Value: {stats['latest']['mean']:.4f}"
|
| 57 |
+
context += f"\n - Maximum in Field: {stats['max_in_field']:.4f}"
|
| 58 |
+
context += f"\n - Minimum in Field: {stats['min_in_field']:.4f}"
|
| 59 |
+
context += f"\n - Temporal Change (Latest - Oldest): {stats['change']:+.4f}"
|
| 60 |
+
context += f"\n - Temporal Trend (All Values): {stats['mean_values_over_time']}"
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# Add temporal statistics if provided
|
| 64 |
+
if temporal_stats:
|
| 65 |
+
context += "\n\nTEMPORAL STATISTICS (FEATURE ENGINEERING):\n"
|
| 66 |
+
for index_name, t_stats in temporal_stats.items():
|
| 67 |
+
context += f"\n{index_name} Temporal Features:"
|
| 68 |
+
|
| 69 |
+
# Mean and std over time
|
| 70 |
+
mean_spatial = float(np.nanmean(t_stats['mean_over_time']))
|
| 71 |
+
std_spatial = float(np.nanmean(t_stats['std_over_time']))
|
| 72 |
+
context += f"\n - Spatial Mean (averaged over time): {mean_spatial:.4f}"
|
| 73 |
+
context += f"\n - Spatial Std (averaged over time): {std_spatial:.4f}"
|
| 74 |
+
|
| 75 |
+
# Max and min over time
|
| 76 |
+
max_val = float(np.nanmax(t_stats['max_over_time']))
|
| 77 |
+
min_val = float(np.nanmin(t_stats['min_over_time']))
|
| 78 |
+
range_val = float(np.nanmean(t_stats['range']))
|
| 79 |
+
context += f"\n - Maximum Value Over Time: {max_val:.4f}"
|
| 80 |
+
context += f"\n - Minimum Value Over Time: {min_val:.4f}"
|
| 81 |
+
context += f"\n - Average Range (Max-Min): {range_val:.4f}"
|
| 82 |
+
|
| 83 |
+
# Temporal trend
|
| 84 |
+
trend_mean = float(np.nanmean(t_stats['temporal_trend']))
|
| 85 |
+
context += f"\n - Average Temporal Trend: {trend_mean:+.4f}"
|
| 86 |
+
|
| 87 |
+
# Rolling average if available
|
| 88 |
+
if 'rolling_avg_3' in t_stats:
|
| 89 |
+
latest_rolling = float(np.nanmean(t_stats['rolling_avg_3'][-1]))
|
| 90 |
+
context += f"\n - Latest Rolling Average (3-period): {latest_rolling:.4f}"
|
| 91 |
+
|
| 92 |
+
return context
|
| 93 |
+
|
| 94 |
+
def format_stress_context(stress_context: Dict) -> str:
|
| 95 |
+
"""
|
| 96 |
+
Format stress detection results for LLM prompt.
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
stress_context: Dictionary with stress detection results
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
Formatted string with stress patterns, clusters, and anomalies
|
| 103 |
+
"""
|
| 104 |
+
if not stress_context:
|
| 105 |
+
return ""
|
| 106 |
+
|
| 107 |
+
c = "\nDEEP LEARNING STRESS DETECTION RESULTS:\n"
|
| 108 |
+
c += "=======================================\n"
|
| 109 |
+
|
| 110 |
+
# Field Statistics
|
| 111 |
+
fs = stress_context.get('field_statistics', {})
|
| 112 |
+
c += f"Overall Field Stress Score: {fs.get('overall_stress', {}).get('mean', 0):.3f} (0=Healthy, 1=Severe Stress)\n"
|
| 113 |
+
c += f"Stress Category Distribution: {fs.get('stress_distribution', {})}\n"
|
| 114 |
+
|
| 115 |
+
# Cluster Statistics (Patterns)
|
| 116 |
+
c += "\nIDENTIFIED CLUSTERING PATTERNS (SPATIAL-TEMPORAL BEHAVIOR):\n"
|
| 117 |
+
for cluster in stress_context.get('cluster_statistics', []):
|
| 118 |
+
c += f" * Cluster {cluster['cluster_id']} ({cluster['percentage']:.1f}% of field):\n"
|
| 119 |
+
c += f" - Average Stress Score: {cluster['stress_score']['mean']:.3f}\n"
|
| 120 |
+
c += f" - Stress Variability (Std): {cluster['stress_score']['std']:.3f}\n"
|
| 121 |
+
# Add key band stats if available to explain *why* it's a cluster
|
| 122 |
+
if 'band_statistics' in cluster:
|
| 123 |
+
c += " - Key Spectral Characteristics:\n"
|
| 124 |
+
# Just show a few key bands to keep it concise
|
| 125 |
+
for band in ['B04', 'B08', 'B11']: # Red, NIR, SWIR
|
| 126 |
+
if band in cluster['band_statistics']:
|
| 127 |
+
val = cluster['band_statistics'][band]['mean']
|
| 128 |
+
c += f" {band}: {val:.4f}\n"
|
| 129 |
+
|
| 130 |
+
# Add temporal trends if available
|
| 131 |
+
if 'temporal_trends' in cluster:
|
| 132 |
+
c += " - Temporal Trends (Change over analysis period):\n"
|
| 133 |
+
for band in ['B04', 'B08', 'B11']: # Red, NIR, SWIR
|
| 134 |
+
if band in cluster['temporal_trends']:
|
| 135 |
+
trend = cluster['temporal_trends'][band]
|
| 136 |
+
c += f" {band}: {trend['trend_direction']} ({trend['change']:+.4f})\n"
|
| 137 |
+
|
| 138 |
+
# Anomaly Information
|
| 139 |
+
anom = stress_context.get('anomaly_information', {})
|
| 140 |
+
c += f"\nANOMALY DETECTION (UNUSUAL PATTERNS):\n"
|
| 141 |
+
c += f"- Total Anomalies Detected: {anom.get('total_anomalies', 0)} patches ({anom.get('anomaly_percentage', 0):.1f}% of field)\n"
|
| 142 |
+
if anom.get('anomaly_patches'):
|
| 143 |
+
c += "- Sample Anomalies:\n"
|
| 144 |
+
for p in anom['anomaly_patches'][:3]:
|
| 145 |
+
c += f" * Patch at {p['coordinates']}: Stress={p['stress_score']:.3f}, Category={p['stress_category']}\n"
|
| 146 |
+
|
| 147 |
+
return c
|
| 148 |
+
|
| 149 |
+
def analyze_with_llm(summary_report: Dict, crop_type: str, farmer_context: Dict,
|
| 150 |
+
center_lat: float, center_lon: float, field_size_hectares: float,
|
| 151 |
+
temporal_stats: Dict = None, stress_context: Dict = None) -> Dict[str, Any]:
|
| 152 |
+
"""
|
| 153 |
+
Analyze vegetation indices using Gemini LLM and extract soil insights.
|
| 154 |
+
|
| 155 |
+
Args:
|
| 156 |
+
summary_report: Dictionary with all indices data
|
| 157 |
+
crop_type: Type of crop
|
| 158 |
+
farmer_context: Farmer profile information
|
| 159 |
+
center_lat: Latitude
|
| 160 |
+
center_lon: Longitude
|
| 161 |
+
field_size_hectares: Field size
|
| 162 |
+
temporal_stats: Dictionary with temporal statistics
|
| 163 |
+
stress_context: Dictionary with stress detection results (clustering, anomalies)
|
| 164 |
+
|
| 165 |
+
Returns:
|
| 166 |
+
Dictionary with structured LLM analysis results
|
| 167 |
+
"""
|
| 168 |
+
model = configure_gemini()
|
| 169 |
+
|
| 170 |
+
# Prepare context with temporal statistics
|
| 171 |
+
indices_context = prepare_indices_context(summary_report, crop_type, farmer_context, temporal_stats)
|
| 172 |
+
|
| 173 |
+
# Prepare stress context
|
| 174 |
+
stress_text = format_stress_context(stress_context)
|
| 175 |
+
|
| 176 |
+
# Create prompt for LLM
|
| 177 |
+
prompt = f"""
|
| 178 |
+
{indices_context}
|
| 179 |
+
|
| 180 |
+
{stress_text}
|
| 181 |
+
|
| 182 |
+
FIELD METADATA:
|
| 183 |
+
- Location: Latitude {center_lat:.4f}, Longitude {center_lon:.4f}
|
| 184 |
+
- Field Size: {field_size_hectares:.2f} hectares
|
| 185 |
+
|
| 186 |
+
Based on the vegetation indices data AND the deep learning stress detection results above,
|
| 187 |
+
provide a comprehensive analysis.
|
| 188 |
+
|
| 189 |
+
Use the cluster patterns to identify distinct zones in the field.
|
| 190 |
+
Use the anomaly detection results to pinpoint specific problem areas.
|
| 191 |
+
Analyze the temporal trends in each cluster to determine if stress is worsening or recovering.
|
| 192 |
+
Combine the spectral indices (NDVI, NDWI, etc.) with the stress scores to explain the *cause* of stress.
|
| 193 |
+
|
| 194 |
+
You MUST respond with a valid JSON object (no markdown, no code blocks) with EXACTLY this structure:
|
| 195 |
+
|
| 196 |
+
{{
|
| 197 |
+
"soil_moisture": {{
|
| 198 |
+
"level": "low" or "moderate" or "high",
|
| 199 |
+
"maximum_value": <float>,
|
| 200 |
+
"minimum_value": <float>,
|
| 201 |
+
"analysis": "Brief explanation of soil moisture status"
|
| 202 |
+
}},
|
| 203 |
+
"soil_salinity": {{
|
| 204 |
+
"level": "low" or "moderate" or "high",
|
| 205 |
+
"trend": "Four word description of overall trend by considering SASI index given",
|
| 206 |
+
"analysis": "Brief explanation of salinity status"
|
| 207 |
+
}},
|
| 208 |
+
"organic_matter": {{
|
| 209 |
+
"level": "low" or "moderate" or "high",
|
| 210 |
+
"status": "Four word analysis based on SOMI index values",
|
| 211 |
+
"analysis": "Brief explanation of organic matter status"
|
| 212 |
+
}},
|
| 213 |
+
"soil_fertility": {{
|
| 214 |
+
"level": "low" or "moderate" or "high",
|
| 215 |
+
"status": "Four words about soil health based on SFI",
|
| 216 |
+
"analysis": "Brief explanation of soil fertility status"
|
| 217 |
+
}},
|
| 218 |
+
"vegetation_stress": {{
|
| 219 |
+
"level": "low" or "moderate" or "high",
|
| 220 |
+
"status": "Four word description not necessarily a sentence",
|
| 221 |
+
"analysis": "Brief explanation based on NDVI, EVI, NDRE trends"
|
| 222 |
+
}},
|
| 223 |
+
"photosynthetic_stress": {{
|
| 224 |
+
"level": "low" or "moderate" or "high",
|
| 225 |
+
"status": "Four word description not necessarily a sentence",
|
| 226 |
+
"analysis": "Brief explanation based on PRI, PSRI, chlorophyll indices"
|
| 227 |
+
}},
|
| 228 |
+
"hotspot_detection": {{
|
| 229 |
+
"description": "Direction and intensity of stress spreading in less than 6 words",
|
| 230 |
+
"analysis": "Brief explanation of spatial stress patterns"
|
| 231 |
+
}},
|
| 232 |
+
"moisture_zones": {{
|
| 233 |
+
"description": "Moisture variation and trend in not more than 6 words",
|
| 234 |
+
"analysis": "Brief explanation of moisture distribution patterns"
|
| 235 |
+
}},
|
| 236 |
+
"overall_health": {{
|
| 237 |
+
"status": "poor" or "fair" or "good" or "excellent",
|
| 238 |
+
"key_concerns": ["concern1", "concern2"],
|
| 239 |
+
"recommendations": ["recommendation1", "recommendation2"]
|
| 240 |
+
}}
|
| 241 |
+
}}
|
| 242 |
+
|
| 243 |
+
IMPORTANT GUIDELINES:
|
| 244 |
+
- For soil_moisture.maximum_value and minimum_value, use the SMI index values from the data
|
| 245 |
+
- For soil_salinity.trend, provide EXACTLY four words describing the trend based on SASI values
|
| 246 |
+
- For organic_matter.status, provide EXACTLY four words based on SOMI index values
|
| 247 |
+
- For soil_fertility.status, provide EXACTLY four words (not a sentence) about soil health based on SFI values
|
| 248 |
+
- For vegetation_stress.status, provide EXACTLY four words based on NDVI, EVI, NDRE temporal patterns
|
| 249 |
+
- For photosynthetic_stress.status, provide EXACTLY four words based on PRI, PSRI, RECI values
|
| 250 |
+
- For hotspot_detection.description, provide LESS THAN 6 words about stress direction and intensity
|
| 251 |
+
- For moisture_zones.description, provide NOT MORE THAN 6 words about moisture variation and trend
|
| 252 |
+
- Use spatial statistics (max, min, range) to identify hotspots and zones
|
| 253 |
+
- Consider temporal trends to detect spreading patterns
|
| 254 |
+
- Base your analysis on the actual index values provided
|
| 255 |
+
- Provide actionable insights relevant to the farmer's context
|
| 256 |
+
|
| 257 |
+
Return ONLY the JSON object, no additional text.
|
| 258 |
+
"""
|
| 259 |
+
|
| 260 |
+
# Get LLM response
|
| 261 |
+
response = model.generate_content(prompt)
|
| 262 |
+
response_text = response.text.strip()
|
| 263 |
+
|
| 264 |
+
# Remove markdown code blocks if present
|
| 265 |
+
if response_text.startswith("```"):
|
| 266 |
+
lines = response_text.split("\n")
|
| 267 |
+
response_text = "\n".join(lines[1:-1])
|
| 268 |
+
if response_text.startswith("json"):
|
| 269 |
+
response_text = response_text[4:].strip()
|
| 270 |
+
|
| 271 |
+
# Parse JSON response
|
| 272 |
+
try:
|
| 273 |
+
analysis = json.loads(response_text)
|
| 274 |
+
return analysis
|
| 275 |
+
except json.JSONDecodeError as e:
|
| 276 |
+
print(f"Error parsing LLM response: {e}")
|
| 277 |
+
print(f"Response text: {response_text}")
|
| 278 |
+
# Return fallback structure
|
| 279 |
+
return {
|
| 280 |
+
"soil_moisture": {
|
| 281 |
+
"level": "moderate",
|
| 282 |
+
"maximum_value": summary_report['indices']['SMI']['max_in_field'],
|
| 283 |
+
"minimum_value": summary_report['indices']['SMI']['min_in_field'],
|
| 284 |
+
"analysis": "Unable to parse LLM response"
|
| 285 |
+
},
|
| 286 |
+
"soil_salinity": {
|
| 287 |
+
"level": "moderate",
|
| 288 |
+
"trend": "Unable to determine trend",
|
| 289 |
+
"analysis": "Unable to parse LLM response"
|
| 290 |
+
},
|
| 291 |
+
"organic_matter": {
|
| 292 |
+
"level": "moderate",
|
| 293 |
+
"status": "Unable to determine status",
|
| 294 |
+
"analysis": "Unable to parse LLM response"
|
| 295 |
+
},
|
| 296 |
+
"soil_fertility": {
|
| 297 |
+
"level": "moderate",
|
| 298 |
+
"status": "Unable to determine status",
|
| 299 |
+
"analysis": "Unable to parse LLM response"
|
| 300 |
+
},
|
| 301 |
+
"vegetation_stress": {
|
| 302 |
+
"level": "moderate",
|
| 303 |
+
"status": "Unable to determine status",
|
| 304 |
+
"analysis": "Unable to parse LLM response"
|
| 305 |
+
},
|
| 306 |
+
"photosynthetic_stress": {
|
| 307 |
+
"level": "moderate",
|
| 308 |
+
"status": "Unable to determine status",
|
| 309 |
+
"analysis": "Unable to parse LLM response"
|
| 310 |
+
},
|
| 311 |
+
"hotspot_detection": {
|
| 312 |
+
"description": "Unable to determine",
|
| 313 |
+
"analysis": "Unable to parse LLM response"
|
| 314 |
+
},
|
| 315 |
+
"moisture_zones": {
|
| 316 |
+
"description": "Unable to determine",
|
| 317 |
+
"analysis": "Unable to parse LLM response"
|
| 318 |
+
},
|
| 319 |
+
"overall_health": {
|
| 320 |
+
"status": "fair",
|
| 321 |
+
"key_concerns": ["Analysis unavailable"],
|
| 322 |
+
"recommendations": ["Please review indices manually"]
|
| 323 |
+
}
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
def format_llm_output(analysis: Dict) -> str:
|
| 327 |
+
"""
|
| 328 |
+
Format LLM analysis into a readable report.
|
| 329 |
+
|
| 330 |
+
Args:
|
| 331 |
+
analysis: Dictionary with LLM analysis results
|
| 332 |
+
|
| 333 |
+
Returns:
|
| 334 |
+
Formatted string report
|
| 335 |
+
"""
|
| 336 |
+
report = """
|
| 337 |
+
╔════════════════════════════════════════════════════════════════╗
|
| 338 |
+
║ LLM ANALYSIS - SOIL & CROP INSIGHTS ║
|
| 339 |
+
╚════════════════════════════════════════════════════════════════╝
|
| 340 |
+
|
| 341 |
+
SOIL MOISTURE ANALYSIS:
|
| 342 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━��━━━━━━━━━━
|
| 343 |
+
"""
|
| 344 |
+
|
| 345 |
+
sm = analysis['soil_moisture']
|
| 346 |
+
report += f" Level: {sm['level'].upper()}\n"
|
| 347 |
+
report += f" Maximum Value: {sm['maximum_value']:.4f}\n"
|
| 348 |
+
report += f" Minimum Value: {sm['minimum_value']:.4f}\n"
|
| 349 |
+
report += f" Analysis: {sm['analysis']}\n"
|
| 350 |
+
|
| 351 |
+
report += """
|
| 352 |
+
SOIL SALINITY ANALYSIS:
|
| 353 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 354 |
+
"""
|
| 355 |
+
|
| 356 |
+
ss = analysis['soil_salinity']
|
| 357 |
+
report += f" Level: {ss['level'].upper()}\n"
|
| 358 |
+
report += f" Overall Trend: {ss['trend']}\n"
|
| 359 |
+
report += f" Analysis: {ss['analysis']}\n"
|
| 360 |
+
|
| 361 |
+
report += """
|
| 362 |
+
ORGANIC MATTER ANALYSIS:
|
| 363 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 364 |
+
"""
|
| 365 |
+
|
| 366 |
+
om = analysis['organic_matter']
|
| 367 |
+
report += f" Level: {om['level'].upper()}\n"
|
| 368 |
+
report += f" Status: {om['status']}\n"
|
| 369 |
+
report += f" Analysis: {om['analysis']}\n"
|
| 370 |
+
|
| 371 |
+
report += """
|
| 372 |
+
SOIL FERTILITY ANALYSIS:
|
| 373 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 374 |
+
"""
|
| 375 |
+
|
| 376 |
+
sf = analysis['soil_fertility']
|
| 377 |
+
report += f" Level: {sf['level'].upper()}\n"
|
| 378 |
+
report += f" Status: {sf['status']}\n"
|
| 379 |
+
report += f" Analysis: {sf['analysis']}\n"
|
| 380 |
+
|
| 381 |
+
report += """
|
| 382 |
+
VEGETATION STRESS ANALYSIS:
|
| 383 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 384 |
+
"""
|
| 385 |
+
|
| 386 |
+
vs = analysis['vegetation_stress']
|
| 387 |
+
report += f" Level: {vs['level'].upper()}\n"
|
| 388 |
+
report += f" Status: {vs['status']}\n"
|
| 389 |
+
report += f" Analysis: {vs['analysis']}\n"
|
| 390 |
+
|
| 391 |
+
report += """
|
| 392 |
+
PHOTOSYNTHETIC STRESS ANALYSIS:
|
| 393 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 394 |
+
"""
|
| 395 |
+
|
| 396 |
+
ps = analysis['photosynthetic_stress']
|
| 397 |
+
report += f" Level: {ps['level'].upper()}\n"
|
| 398 |
+
report += f" Status: {ps['status']}\n"
|
| 399 |
+
report += f" Analysis: {ps['analysis']}\n"
|
| 400 |
+
|
| 401 |
+
report += """
|
| 402 |
+
HOTSPOT DETECTION:
|
| 403 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 404 |
+
"""
|
| 405 |
+
|
| 406 |
+
hd = analysis['hotspot_detection']
|
| 407 |
+
report += f" Description: {hd['description']}\n"
|
| 408 |
+
report += f" Analysis: {hd['analysis']}\n"
|
| 409 |
+
|
| 410 |
+
report += """
|
| 411 |
+
MOISTURE ZONES:
|
| 412 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 413 |
+
"""
|
| 414 |
+
|
| 415 |
+
mz = analysis['moisture_zones']
|
| 416 |
+
report += f" Description: {mz['description']}\n"
|
| 417 |
+
report += f" Analysis: {mz['analysis']}\n"
|
| 418 |
+
|
| 419 |
+
report += """
|
| 420 |
+
OVERALL CROP HEALTH:
|
| 421 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 422 |
+
"""
|
| 423 |
+
|
| 424 |
+
oh = analysis['overall_health']
|
| 425 |
+
report += f" Status: {oh['status'].upper()}\n"
|
| 426 |
+
report += f"\n Key Concerns:\n"
|
| 427 |
+
for concern in oh['key_concerns']:
|
| 428 |
+
report += f" • {concern}\n"
|
| 429 |
+
report += f"\n Recommendations:\n"
|
| 430 |
+
for rec in oh['recommendations']:
|
| 431 |
+
report += f" • {rec}\n"
|
| 432 |
+
|
| 433 |
+
report += "\n" + "═" * 64 + "\n"
|
| 434 |
+
|
| 435 |
+
return report
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy>=1.24.0
|
| 2 |
+
pandas>=2.0.0
|
| 3 |
+
matplotlib>=3.7.0
|
| 4 |
+
scikit-learn>=1.3.0
|
| 5 |
+
tensorflow>=2.13.0
|
| 6 |
+
sentinelhub>=3.9.0
|
| 7 |
+
python-dotenv>=1.0.0
|
| 8 |
+
google-generativeai>=0.3.0
|
| 9 |
+
fastapi>=0.100.0
|
| 10 |
+
uvicorn>=0.23.0
|
| 11 |
+
python-multipart>=0.0.6
|
stress_detection_model.py
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stress Detection Model
|
| 3 |
+
=======================
|
| 4 |
+
|
| 5 |
+
Deep learning model for crop stress detection using spatial-temporal encoding.
|
| 6 |
+
Architecture: Spatial CNN → Temporal LSTM → Clustering → Anomaly Detection
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
import tensorflow as tf
|
| 11 |
+
from tensorflow import keras
|
| 12 |
+
from tensorflow.keras import layers
|
| 13 |
+
from sklearn.cluster import KMeans
|
| 14 |
+
from sklearn.ensemble import IsolationForest
|
| 15 |
+
from sklearn.preprocessing import StandardScaler
|
| 16 |
+
from sklearn.metrics import silhouette_score
|
| 17 |
+
from typing import Tuple, Dict, List
|
| 18 |
+
import warnings
|
| 19 |
+
warnings.filterwarnings('ignore')
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class SpatialEncoder(keras.Model):
|
| 23 |
+
"""
|
| 24 |
+
CNN-based spatial feature extractor.
|
| 25 |
+
Processes each timestamp independently to extract spatial features.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
def __init__(self, embedding_dim=128):
|
| 29 |
+
super(SpatialEncoder, self).__init__()
|
| 30 |
+
|
| 31 |
+
# Convolutional layers
|
| 32 |
+
self.conv1 = layers.Conv2D(32, (3, 3), activation='relu', padding='same')
|
| 33 |
+
self.bn1 = layers.BatchNormalization()
|
| 34 |
+
self.pool1 = layers.MaxPooling2D((2, 2))
|
| 35 |
+
self.dropout1 = layers.Dropout(0.25)
|
| 36 |
+
|
| 37 |
+
self.conv2 = layers.Conv2D(64, (3, 3), activation='relu', padding='same')
|
| 38 |
+
self.bn2 = layers.BatchNormalization()
|
| 39 |
+
self.pool2 = layers.MaxPooling2D((2, 2))
|
| 40 |
+
self.dropout2 = layers.Dropout(0.25)
|
| 41 |
+
|
| 42 |
+
self.conv3 = layers.Conv2D(128, (3, 3), activation='relu', padding='same')
|
| 43 |
+
self.bn3 = layers.BatchNormalization()
|
| 44 |
+
|
| 45 |
+
# Global pooling and dense layers
|
| 46 |
+
self.global_pool = layers.GlobalAveragePooling2D()
|
| 47 |
+
self.dense1 = layers.Dense(256, activation='relu')
|
| 48 |
+
self.dropout3 = layers.Dropout(0.3)
|
| 49 |
+
self.dense2 = layers.Dense(embedding_dim, activation='relu')
|
| 50 |
+
|
| 51 |
+
def call(self, x, training=False):
|
| 52 |
+
# x shape: (batch, height, width, channels)
|
| 53 |
+
x = self.conv1(x)
|
| 54 |
+
x = self.bn1(x, training=training)
|
| 55 |
+
x = self.pool1(x)
|
| 56 |
+
x = self.dropout1(x, training=training)
|
| 57 |
+
|
| 58 |
+
x = self.conv2(x)
|
| 59 |
+
x = self.bn2(x, training=training)
|
| 60 |
+
x = self.pool2(x)
|
| 61 |
+
x = self.dropout2(x, training=training)
|
| 62 |
+
|
| 63 |
+
x = self.conv3(x)
|
| 64 |
+
x = self.bn3(x, training=training)
|
| 65 |
+
|
| 66 |
+
x = self.global_pool(x)
|
| 67 |
+
x = self.dense1(x)
|
| 68 |
+
x = self.dropout3(x, training=training)
|
| 69 |
+
x = self.dense2(x)
|
| 70 |
+
|
| 71 |
+
return x # (batch, embedding_dim)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class TemporalEncoder(keras.Model):
|
| 75 |
+
"""
|
| 76 |
+
LSTM-based temporal feature extractor.
|
| 77 |
+
Processes sequence of spatial embeddings to capture temporal patterns.
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
def __init__(self, embedding_dim=128, lstm_units=64):
|
| 81 |
+
super(TemporalEncoder, self).__init__()
|
| 82 |
+
|
| 83 |
+
self.lstm = layers.Bidirectional(
|
| 84 |
+
layers.LSTM(lstm_units, return_sequences=False, dropout=0.2)
|
| 85 |
+
)
|
| 86 |
+
self.dense = layers.Dense(embedding_dim, activation='relu')
|
| 87 |
+
|
| 88 |
+
def call(self, x, training=False):
|
| 89 |
+
# x shape: (batch, time, spatial_embedding_dim)
|
| 90 |
+
x = self.lstm(x, training=training)
|
| 91 |
+
x = self.dense(x)
|
| 92 |
+
return x # (batch, embedding_dim)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class StressDetectionModel:
|
| 96 |
+
"""
|
| 97 |
+
Complete stress detection pipeline with spatial-temporal encoding,
|
| 98 |
+
clustering, and anomaly detection.
|
| 99 |
+
"""
|
| 100 |
+
|
| 101 |
+
def __init__(self, patch_size=16, num_bands=8, num_timestamps=10,
|
| 102 |
+
spatial_embedding_dim=128, temporal_embedding_dim=128):
|
| 103 |
+
self.patch_size = patch_size
|
| 104 |
+
self.num_bands = num_bands
|
| 105 |
+
self.num_timestamps = num_timestamps
|
| 106 |
+
self.spatial_embedding_dim = spatial_embedding_dim
|
| 107 |
+
self.temporal_embedding_dim = temporal_embedding_dim
|
| 108 |
+
|
| 109 |
+
# Build encoders
|
| 110 |
+
self.spatial_encoder = SpatialEncoder(embedding_dim=spatial_embedding_dim)
|
| 111 |
+
self.temporal_encoder = TemporalEncoder(
|
| 112 |
+
embedding_dim=temporal_embedding_dim,
|
| 113 |
+
lstm_units=64
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
# Build spatial encoder input
|
| 117 |
+
self.spatial_encoder.build((None, patch_size, patch_size, num_bands))
|
| 118 |
+
|
| 119 |
+
# Clustering and anomaly detection (fitted during inference)
|
| 120 |
+
self.kmeans = None
|
| 121 |
+
self.anomaly_detector = None
|
| 122 |
+
self.scaler = StandardScaler()
|
| 123 |
+
|
| 124 |
+
def encode_spatial_features(self, patches: np.ndarray) -> np.ndarray:
|
| 125 |
+
"""
|
| 126 |
+
Extract spatial features from all patches and timestamps.
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
patches: Array of shape (num_patches, time, height, width, bands)
|
| 130 |
+
|
| 131 |
+
Returns:
|
| 132 |
+
spatial_embeddings: Array of shape (num_patches, time, spatial_embedding_dim)
|
| 133 |
+
"""
|
| 134 |
+
num_patches, time, height, width, bands = patches.shape
|
| 135 |
+
|
| 136 |
+
# Reshape to process all patches and timestamps together
|
| 137 |
+
# (num_patches * time, height, width, bands)
|
| 138 |
+
reshaped = patches.reshape(-1, height, width, bands)
|
| 139 |
+
|
| 140 |
+
# Extract spatial features
|
| 141 |
+
spatial_features = self.spatial_encoder(reshaped, training=False).numpy()
|
| 142 |
+
|
| 143 |
+
# Reshape back to (num_patches, time, embedding_dim)
|
| 144 |
+
spatial_embeddings = spatial_features.reshape(
|
| 145 |
+
num_patches, time, self.spatial_embedding_dim
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
return spatial_embeddings
|
| 149 |
+
|
| 150 |
+
def encode_temporal_features(self, spatial_embeddings: np.ndarray) -> np.ndarray:
|
| 151 |
+
"""
|
| 152 |
+
Extract temporal features from spatial embeddings.
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
spatial_embeddings: Array of shape (num_patches, time, spatial_embedding_dim)
|
| 156 |
+
|
| 157 |
+
Returns:
|
| 158 |
+
temporal_embeddings: Array of shape (num_patches, temporal_embedding_dim)
|
| 159 |
+
"""
|
| 160 |
+
temporal_embeddings = self.temporal_encoder(
|
| 161 |
+
spatial_embeddings, training=False
|
| 162 |
+
).numpy()
|
| 163 |
+
|
| 164 |
+
return temporal_embeddings
|
| 165 |
+
|
| 166 |
+
def cluster_stress_patterns(self, embeddings: np.ndarray, n_clusters=4) -> Tuple[np.ndarray, np.ndarray]:
|
| 167 |
+
"""
|
| 168 |
+
Cluster embeddings into stress categories and compute stress scores.
|
| 169 |
+
|
| 170 |
+
Args:
|
| 171 |
+
embeddings: Array of shape (num_patches, embedding_dim)
|
| 172 |
+
n_clusters: Number of clusters (4: high, moderate, low, noise)
|
| 173 |
+
|
| 174 |
+
Returns:
|
| 175 |
+
cluster_labels: Cluster assignment for each patch
|
| 176 |
+
stress_scores: Normalized stress scores in [0, 1]
|
| 177 |
+
"""
|
| 178 |
+
# Standardize embeddings
|
| 179 |
+
embeddings_scaled = self.scaler.fit_transform(embeddings)
|
| 180 |
+
|
| 181 |
+
# K-Means clustering
|
| 182 |
+
self.kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
|
| 183 |
+
cluster_labels = self.kmeans.fit_predict(embeddings_scaled)
|
| 184 |
+
|
| 185 |
+
# Compute stress scores based on distance to cluster centers
|
| 186 |
+
distances = self.kmeans.transform(embeddings_scaled)
|
| 187 |
+
|
| 188 |
+
# For each patch, compute stress score as weighted distance to all clusters
|
| 189 |
+
# Normalize to [0, 1] range
|
| 190 |
+
stress_scores = np.min(distances, axis=1) # Distance to nearest cluster
|
| 191 |
+
stress_scores = 1 - (stress_scores - stress_scores.min()) / (stress_scores.max() - stress_scores.min() + 1e-10)
|
| 192 |
+
|
| 193 |
+
# Alternative: Use cluster centers to assign stress levels
|
| 194 |
+
# Identify which cluster represents highest stress (largest distance from origin)
|
| 195 |
+
cluster_stress_levels = np.linalg.norm(self.kmeans.cluster_centers_, axis=1)
|
| 196 |
+
cluster_stress_levels = (cluster_stress_levels - cluster_stress_levels.min()) / \
|
| 197 |
+
(cluster_stress_levels.max() - cluster_stress_levels.min() + 1e-10)
|
| 198 |
+
|
| 199 |
+
# Assign stress score based on cluster membership
|
| 200 |
+
stress_scores = cluster_stress_levels[cluster_labels]
|
| 201 |
+
|
| 202 |
+
return cluster_labels, stress_scores
|
| 203 |
+
|
| 204 |
+
def detect_anomalies(self, embeddings: np.ndarray, contamination=0.1) -> Tuple[np.ndarray, np.ndarray]:
|
| 205 |
+
"""
|
| 206 |
+
Detect anomalous stress patterns using Isolation Forest.
|
| 207 |
+
|
| 208 |
+
Args:
|
| 209 |
+
embeddings: Array of shape (num_patches, embedding_dim)
|
| 210 |
+
contamination: Expected proportion of anomalies
|
| 211 |
+
|
| 212 |
+
Returns:
|
| 213 |
+
anomaly_labels: 1 for normal, -1 for anomaly
|
| 214 |
+
anomaly_scores: Anomaly scores (lower = more anomalous)
|
| 215 |
+
"""
|
| 216 |
+
self.anomaly_detector = IsolationForest(
|
| 217 |
+
contamination=contamination,
|
| 218 |
+
random_state=42
|
| 219 |
+
)
|
| 220 |
+
anomaly_labels = self.anomaly_detector.fit_predict(embeddings)
|
| 221 |
+
anomaly_scores = self.anomaly_detector.score_samples(embeddings)
|
| 222 |
+
|
| 223 |
+
return anomaly_labels, anomaly_scores
|
| 224 |
+
|
| 225 |
+
def predict(self, patches: np.ndarray, n_clusters=4, contamination=0.1) -> Dict:
|
| 226 |
+
"""
|
| 227 |
+
Complete stress detection pipeline.
|
| 228 |
+
|
| 229 |
+
Args:
|
| 230 |
+
patches: Array of shape (num_patches, time, height, width, bands)
|
| 231 |
+
n_clusters: Number of stress clusters
|
| 232 |
+
contamination: Expected proportion of anomalies
|
| 233 |
+
|
| 234 |
+
Returns:
|
| 235 |
+
results: Dictionary with all predictions and embeddings
|
| 236 |
+
"""
|
| 237 |
+
# Step 1: Spatial encoding
|
| 238 |
+
spatial_embeddings = self.encode_spatial_features(patches)
|
| 239 |
+
|
| 240 |
+
# Step 2: Temporal encoding
|
| 241 |
+
temporal_embeddings = self.encode_temporal_features(spatial_embeddings)
|
| 242 |
+
|
| 243 |
+
# Step 3: Clustering
|
| 244 |
+
cluster_labels, stress_scores = self.cluster_stress_patterns(
|
| 245 |
+
temporal_embeddings, n_clusters=n_clusters
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
# Step 4: Anomaly detection
|
| 249 |
+
anomaly_labels, anomaly_scores = self.detect_anomalies(temporal_embeddings, contamination=contamination)
|
| 250 |
+
|
| 251 |
+
return {
|
| 252 |
+
'spatial_embeddings': spatial_embeddings,
|
| 253 |
+
'temporal_embeddings': temporal_embeddings,
|
| 254 |
+
'cluster_labels': cluster_labels,
|
| 255 |
+
'stress_scores': stress_scores,
|
| 256 |
+
'anomaly_labels': anomaly_labels,
|
| 257 |
+
'anomaly_scores': anomaly_scores,
|
| 258 |
+
'cluster_centers': self.kmeans.cluster_centers_,
|
| 259 |
+
'n_clusters': n_clusters
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def get_stress_category(stress_score: float) -> str:
|
| 264 |
+
"""Convert stress score to category label."""
|
| 265 |
+
if stress_score < 0.25:
|
| 266 |
+
return "Low Stress"
|
| 267 |
+
elif stress_score < 0.5:
|
| 268 |
+
return "Moderate Stress"
|
| 269 |
+
elif stress_score < 0.75:
|
| 270 |
+
return "High Stress"
|
| 271 |
+
else:
|
| 272 |
+
return "Severe Stress"
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def find_optimal_clusters(embeddings: np.ndarray,
|
| 276 |
+
min_clusters: int = 2,
|
| 277 |
+
max_clusters: int = 10) -> Tuple[int, Dict]:
|
| 278 |
+
"""
|
| 279 |
+
Find optimal number of clusters using elbow method and silhouette score.
|
| 280 |
+
|
| 281 |
+
Args:
|
| 282 |
+
embeddings: Array of shape (num_samples, embedding_dim)
|
| 283 |
+
min_clusters: Minimum number of clusters to test
|
| 284 |
+
max_clusters: Maximum number of clusters to test
|
| 285 |
+
|
| 286 |
+
Returns:
|
| 287 |
+
optimal_k: Optimal number of clusters
|
| 288 |
+
metrics: Dictionary with inertia and silhouette scores
|
| 289 |
+
"""
|
| 290 |
+
print("\nFinding optimal number of clusters...")
|
| 291 |
+
|
| 292 |
+
scaler = StandardScaler()
|
| 293 |
+
embeddings_scaled = scaler.fit_transform(embeddings)
|
| 294 |
+
|
| 295 |
+
inertias = []
|
| 296 |
+
silhouette_scores = []
|
| 297 |
+
k_range = range(min_clusters, max_clusters + 1)
|
| 298 |
+
|
| 299 |
+
for k in k_range:
|
| 300 |
+
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
|
| 301 |
+
labels = kmeans.fit_predict(embeddings_scaled)
|
| 302 |
+
|
| 303 |
+
inertias.append(kmeans.inertia_)
|
| 304 |
+
|
| 305 |
+
# Calculate silhouette score (higher is better)
|
| 306 |
+
if k > 1:
|
| 307 |
+
sil_score = silhouette_score(embeddings_scaled, labels)
|
| 308 |
+
silhouette_scores.append(sil_score)
|
| 309 |
+
else:
|
| 310 |
+
silhouette_scores.append(0)
|
| 311 |
+
|
| 312 |
+
print(f" k={k}: Inertia={kmeans.inertia_:.2f}, Silhouette={silhouette_scores[-1]:.3f}")
|
| 313 |
+
|
| 314 |
+
# Find elbow using rate of change
|
| 315 |
+
inertia_diffs = np.diff(inertias)
|
| 316 |
+
inertia_diffs_2 = np.diff(inertia_diffs)
|
| 317 |
+
|
| 318 |
+
# Optimal k is where second derivative is maximum (elbow point)
|
| 319 |
+
elbow_k = min_clusters + np.argmax(np.abs(inertia_diffs_2)) + 1
|
| 320 |
+
|
| 321 |
+
# Also consider silhouette score
|
| 322 |
+
best_silhouette_k = min_clusters + np.argmax(silhouette_scores)
|
| 323 |
+
|
| 324 |
+
# Use silhouette score as primary metric, elbow as secondary
|
| 325 |
+
optimal_k = best_silhouette_k
|
| 326 |
+
|
| 327 |
+
print(f"\n[OK] Optimal clusters: {optimal_k} (Elbow: {elbow_k}, Best Silhouette: {best_silhouette_k})")
|
| 328 |
+
|
| 329 |
+
metrics = {
|
| 330 |
+
'k_range': list(k_range),
|
| 331 |
+
'inertias': inertias,
|
| 332 |
+
'silhouette_scores': silhouette_scores,
|
| 333 |
+
'optimal_k': optimal_k,
|
| 334 |
+
'elbow_k': elbow_k,
|
| 335 |
+
'best_silhouette_k': best_silhouette_k
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
return optimal_k, metrics
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
def prepare_llm_context(results: Dict,
|
| 342 |
+
patch_coords: List,
|
| 343 |
+
patches: np.ndarray,
|
| 344 |
+
metadata: Dict) -> Dict:
|
| 345 |
+
"""
|
| 346 |
+
Prepare comprehensive context for LLM including cluster statistics and anomaly information.
|
| 347 |
+
|
| 348 |
+
Args:
|
| 349 |
+
results: Dictionary from model.predict()
|
| 350 |
+
patch_coords: List of (h, w) coordinates for each patch
|
| 351 |
+
patches: Original patches array
|
| 352 |
+
metadata: Preprocessing metadata
|
| 353 |
+
|
| 354 |
+
Returns:
|
| 355 |
+
context: Dictionary with cluster-wise and anomaly statistics
|
| 356 |
+
"""
|
| 357 |
+
cluster_labels = results['cluster_labels']
|
| 358 |
+
stress_scores = results['stress_scores']
|
| 359 |
+
anomaly_labels = results['anomaly_labels']
|
| 360 |
+
temporal_embeddings = results['temporal_embeddings']
|
| 361 |
+
|
| 362 |
+
# Get anomaly scores (distance from decision boundary)
|
| 363 |
+
anomaly_scores = results.get('anomaly_scores',
|
| 364 |
+
results['anomaly_labels'].astype(float))
|
| 365 |
+
|
| 366 |
+
# Cluster-wise statistics
|
| 367 |
+
cluster_stats = []
|
| 368 |
+
for cluster_id in range(results['n_clusters']):
|
| 369 |
+
cluster_mask = cluster_labels == cluster_id
|
| 370 |
+
cluster_patches = patches[cluster_mask]
|
| 371 |
+
cluster_stress = stress_scores[cluster_mask]
|
| 372 |
+
cluster_embeddings = temporal_embeddings[cluster_mask]
|
| 373 |
+
|
| 374 |
+
# Calculate statistics for this cluster
|
| 375 |
+
stats = {
|
| 376 |
+
'cluster_id': int(cluster_id),
|
| 377 |
+
'num_patches': int(np.sum(cluster_mask)),
|
| 378 |
+
'percentage': float(100 * np.sum(cluster_mask) / len(cluster_labels)),
|
| 379 |
+
'stress_score': {
|
| 380 |
+
'mean': float(cluster_stress.mean()),
|
| 381 |
+
'std': float(cluster_stress.std()),
|
| 382 |
+
'min': float(cluster_stress.min()),
|
| 383 |
+
'max': float(cluster_stress.max())
|
| 384 |
+
},
|
| 385 |
+
'embedding_stats': {
|
| 386 |
+
'mean_norm': float(np.linalg.norm(cluster_embeddings.mean(axis=0))),
|
| 387 |
+
'std_norm': float(np.linalg.norm(cluster_embeddings.std(axis=0)))
|
| 388 |
+
},
|
| 389 |
+
'band_statistics': {}
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
# Calculate per-band statistics for this cluster
|
| 393 |
+
for band_idx, band_name in enumerate(metadata['selected_bands']):
|
| 394 |
+
band_data = cluster_patches[:, :, :, :, band_idx] # (patches, time, h, w)
|
| 395 |
+
stats['band_statistics'][band_name] = {
|
| 396 |
+
'mean': float(np.nanmean(band_data)),
|
| 397 |
+
'std': float(np.nanstd(band_data)),
|
| 398 |
+
'min': float(np.nanmin(band_data)),
|
| 399 |
+
'max': float(np.nanmax(band_data))
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
cluster_stats.append(stats)
|
| 403 |
+
|
| 404 |
+
# Calculate temporal trends for this cluster
|
| 405 |
+
# Shape: (num_patches, time, h, w, bands) -> (time, bands)
|
| 406 |
+
if cluster_patches.shape[0] > 0:
|
| 407 |
+
cluster_time_series = np.nanmean(cluster_patches, axis=(0, 2, 3))
|
| 408 |
+
|
| 409 |
+
stats['temporal_trends'] = {}
|
| 410 |
+
for band_idx, band_name in enumerate(metadata['selected_bands']):
|
| 411 |
+
series = cluster_time_series[:, band_idx]
|
| 412 |
+
if len(series) > 1:
|
| 413 |
+
change = float(series[-1] - series[0])
|
| 414 |
+
trend_direction = "stable"
|
| 415 |
+
if change > 0.05: trend_direction = "increasing"
|
| 416 |
+
elif change < -0.05: trend_direction = "decreasing"
|
| 417 |
+
|
| 418 |
+
stats['temporal_trends'][band_name] = {
|
| 419 |
+
'change': change,
|
| 420 |
+
'trend_direction': trend_direction,
|
| 421 |
+
'latest_value': float(series[-1]),
|
| 422 |
+
'earliest_value': float(series[0])
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
# Anomaly information
|
| 426 |
+
anomaly_mask = anomaly_labels == -1
|
| 427 |
+
anomaly_indices = np.where(anomaly_mask)[0]
|
| 428 |
+
|
| 429 |
+
anomaly_info = {
|
| 430 |
+
'total_anomalies': int(np.sum(anomaly_mask)),
|
| 431 |
+
'anomaly_percentage': float(100 * np.sum(anomaly_mask) / len(anomaly_labels)),
|
| 432 |
+
'anomaly_patches': []
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
# Detailed info for each anomaly patch
|
| 436 |
+
for idx in anomaly_indices[:20]: # Limit to first 20 anomalies
|
| 437 |
+
patch_info = {
|
| 438 |
+
'patch_id': int(idx),
|
| 439 |
+
'coordinates': patch_coords[idx],
|
| 440 |
+
'stress_score': float(stress_scores[idx]),
|
| 441 |
+
'stress_category': get_stress_category(stress_scores[idx]),
|
| 442 |
+
'cluster_id': int(cluster_labels[idx]),
|
| 443 |
+
'anomaly_score': float(anomaly_scores[idx]) if hasattr(anomaly_scores, '__getitem__') else -1.0,
|
| 444 |
+
'embedding_norm': float(np.linalg.norm(temporal_embeddings[idx]))
|
| 445 |
+
}
|
| 446 |
+
anomaly_info['anomaly_patches'].append(patch_info)
|
| 447 |
+
|
| 448 |
+
# Overall field statistics
|
| 449 |
+
field_stats = {
|
| 450 |
+
'total_patches': len(cluster_labels),
|
| 451 |
+
'patch_size': metadata['patch_size'],
|
| 452 |
+
'num_bands': metadata['num_bands'],
|
| 453 |
+
'selected_bands': metadata['selected_bands'],
|
| 454 |
+
'overall_stress': {
|
| 455 |
+
'mean': float(stress_scores.mean()),
|
| 456 |
+
'std': float(stress_scores.std()),
|
| 457 |
+
'min': float(stress_scores.min()),
|
| 458 |
+
'max': float(stress_scores.max())
|
| 459 |
+
},
|
| 460 |
+
'stress_distribution': {
|
| 461 |
+
'low': int(np.sum(stress_scores < 0.25)),
|
| 462 |
+
'moderate': int(np.sum((stress_scores >= 0.25) & (stress_scores < 0.5))),
|
| 463 |
+
'high': int(np.sum((stress_scores >= 0.5) & (stress_scores < 0.75))),
|
| 464 |
+
'severe': int(np.sum(stress_scores >= 0.75))
|
| 465 |
+
}
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
context = {
|
| 469 |
+
'field_statistics': field_stats,
|
| 470 |
+
'cluster_statistics': cluster_stats,
|
| 471 |
+
'anomaly_information': anomaly_info
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
return context
|
| 475 |
+
|
stress_detection_preprocessing.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stress Detection Preprocessing Module
|
| 3 |
+
======================================
|
| 4 |
+
|
| 5 |
+
Prepares Sentinel-2 multi-spectral data for stress detection model.
|
| 6 |
+
Includes band harmonization and normalization.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
from typing import Tuple, List
|
| 11 |
+
|
| 12 |
+
# Band indices in the 12-band Sentinel-2 data
|
| 13 |
+
BAND_INDICES = {
|
| 14 |
+
'B02': 1, # Blue
|
| 15 |
+
'B03': 2, # Green
|
| 16 |
+
'B04': 3, # Red
|
| 17 |
+
'B05': 4, # Red Edge 1
|
| 18 |
+
'B08': 7, # NIR
|
| 19 |
+
'B8A': 8, # NIR Narrow
|
| 20 |
+
'B11': 10, # SWIR1
|
| 21 |
+
'B12': 11 # SWIR2
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
# Selected bands for stress detection (8 major bands)
|
| 25 |
+
SELECTED_BANDS = ['B02', 'B03', 'B04', 'B05', 'B08', 'B8A', 'B11', 'B12']
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def extract_major_bands(all_images: np.ndarray) -> np.ndarray:
|
| 29 |
+
"""
|
| 30 |
+
Extract 8 major bands from 12-band Sentinel-2 data.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
all_images: Array of shape (time, height, width, 12)
|
| 34 |
+
|
| 35 |
+
Returns:
|
| 36 |
+
Array of shape (time, height, width, 8) with selected bands
|
| 37 |
+
"""
|
| 38 |
+
band_idx = [BAND_INDICES[band] for band in SELECTED_BANDS]
|
| 39 |
+
return all_images[:, :, :, band_idx]
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def harmonize_bands(images: np.ndarray) -> np.ndarray:
|
| 43 |
+
"""
|
| 44 |
+
Harmonize band data to same scale [0, 1].
|
| 45 |
+
|
| 46 |
+
Sentinel-2 reflectance values are already in [0, 1] range after DN/10000 conversion.
|
| 47 |
+
This function ensures all bands are properly normalized and handles any outliers.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
images: Array of shape (time, height, width, bands)
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
Harmonized array with values clipped to [0, 1]
|
| 54 |
+
"""
|
| 55 |
+
# Clip to [0, 1] range to handle any outliers
|
| 56 |
+
harmonized = np.clip(images, 0, 1)
|
| 57 |
+
|
| 58 |
+
# Additional per-band normalization to ensure uniform scale
|
| 59 |
+
# Use percentile-based normalization to handle outliers
|
| 60 |
+
time, height, width, bands = harmonized.shape
|
| 61 |
+
|
| 62 |
+
for b in range(bands):
|
| 63 |
+
band_data = harmonized[:, :, :, b]
|
| 64 |
+
|
| 65 |
+
# Calculate 2nd and 98th percentiles to handle outliers
|
| 66 |
+
p2 = np.nanpercentile(band_data, 2)
|
| 67 |
+
p98 = np.nanpercentile(band_data, 98)
|
| 68 |
+
|
| 69 |
+
# Normalize to [0, 1] using percentiles
|
| 70 |
+
if p98 > p2:
|
| 71 |
+
harmonized[:, :, :, b] = np.clip((band_data - p2) / (p98 - p2), 0, 1)
|
| 72 |
+
|
| 73 |
+
return harmonized
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def handle_nan_values(images: np.ndarray, method='mean') -> np.ndarray:
|
| 77 |
+
"""
|
| 78 |
+
Handle NaN values in the data.
|
| 79 |
+
|
| 80 |
+
Args:
|
| 81 |
+
images: Array of shape (time, height, width, bands)
|
| 82 |
+
method: 'mean', 'zero', or 'interpolate'
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
Array with NaN values handled
|
| 86 |
+
"""
|
| 87 |
+
if method == 'zero':
|
| 88 |
+
return np.nan_to_num(images, nan=0.0)
|
| 89 |
+
elif method == 'mean':
|
| 90 |
+
# Replace NaN with temporal mean for each pixel
|
| 91 |
+
return np.where(np.isnan(images),
|
| 92 |
+
np.nanmean(images, axis=0, keepdims=True),
|
| 93 |
+
images)
|
| 94 |
+
elif method == 'interpolate':
|
| 95 |
+
# Simple linear interpolation along time axis
|
| 96 |
+
result = images.copy()
|
| 97 |
+
time, height, width, bands = images.shape
|
| 98 |
+
|
| 99 |
+
for h in range(height):
|
| 100 |
+
for w in range(width):
|
| 101 |
+
for b in range(bands):
|
| 102 |
+
pixel_series = result[:, h, w, b]
|
| 103 |
+
if np.any(np.isnan(pixel_series)):
|
| 104 |
+
# Interpolate NaN values
|
| 105 |
+
mask = ~np.isnan(pixel_series)
|
| 106 |
+
if np.any(mask):
|
| 107 |
+
indices = np.arange(time)
|
| 108 |
+
result[:, h, w, b] = np.interp(
|
| 109 |
+
indices, indices[mask], pixel_series[mask]
|
| 110 |
+
)
|
| 111 |
+
else:
|
| 112 |
+
result[:, h, w, b] = 0.0
|
| 113 |
+
return result
|
| 114 |
+
else:
|
| 115 |
+
return images
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def create_patches(images: np.ndarray, patch_size: int = 16, stride: int = 8) -> Tuple[np.ndarray, List]:
|
| 119 |
+
"""
|
| 120 |
+
Create overlapping patches from images for spatial analysis.
|
| 121 |
+
|
| 122 |
+
Args:
|
| 123 |
+
images: Array of shape (time, height, width, bands)
|
| 124 |
+
patch_size: Size of each patch
|
| 125 |
+
stride: Stride for patch extraction
|
| 126 |
+
|
| 127 |
+
Returns:
|
| 128 |
+
patches: Array of shape (num_patches, time, patch_size, patch_size, bands)
|
| 129 |
+
patch_coords: List of (h_start, w_start) coordinates for each patch
|
| 130 |
+
"""
|
| 131 |
+
time, height, width, bands = images.shape
|
| 132 |
+
patches = []
|
| 133 |
+
patch_coords = []
|
| 134 |
+
|
| 135 |
+
for h in range(0, height - patch_size + 1, stride):
|
| 136 |
+
for w in range(0, width - patch_size + 1, stride):
|
| 137 |
+
patch = images[:, h:h+patch_size, w:w+patch_size, :]
|
| 138 |
+
|
| 139 |
+
# Only include patches with sufficient valid data
|
| 140 |
+
valid_ratio = np.sum(~np.isnan(patch)) / patch.size
|
| 141 |
+
if valid_ratio > 0.5: # At least 50% valid data
|
| 142 |
+
patches.append(patch)
|
| 143 |
+
patch_coords.append((h, w))
|
| 144 |
+
|
| 145 |
+
if len(patches) == 0:
|
| 146 |
+
# If no valid patches, create at least one from center
|
| 147 |
+
h_center = (height - patch_size) // 2
|
| 148 |
+
w_center = (width - patch_size) // 2
|
| 149 |
+
patch = images[:, h_center:h_center+patch_size, w_center:w_center+patch_size, :]
|
| 150 |
+
patches.append(patch)
|
| 151 |
+
patch_coords.append((h_center, w_center))
|
| 152 |
+
|
| 153 |
+
return np.array(patches), patch_coords
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def preprocess_for_model(all_images: np.ndarray,
|
| 157 |
+
patch_size: int = 16,
|
| 158 |
+
stride: int = 8) -> Tuple[np.ndarray, List, dict]:
|
| 159 |
+
"""
|
| 160 |
+
Complete preprocessing pipeline for stress detection model.
|
| 161 |
+
|
| 162 |
+
Args:
|
| 163 |
+
all_images: Raw images of shape (time, height, width, 12)
|
| 164 |
+
patch_size: Size of patches for spatial analysis
|
| 165 |
+
stride: Stride for patch extraction
|
| 166 |
+
|
| 167 |
+
Returns:
|
| 168 |
+
patches: Preprocessed patches ready for model
|
| 169 |
+
patch_coords: Coordinates of each patch
|
| 170 |
+
metadata: Dictionary with preprocessing information
|
| 171 |
+
"""
|
| 172 |
+
print("Preprocessing data for stress detection model...")
|
| 173 |
+
|
| 174 |
+
# Step 1: Extract major bands
|
| 175 |
+
print(" [1/4] Extracting 8 major bands...")
|
| 176 |
+
major_bands = extract_major_bands(all_images)
|
| 177 |
+
|
| 178 |
+
# Step 2: Harmonize bands to same scale
|
| 179 |
+
print(" [2/4] Harmonizing bands to [0, 1] scale...")
|
| 180 |
+
harmonized = harmonize_bands(major_bands)
|
| 181 |
+
|
| 182 |
+
# Step 3: Handle NaN values
|
| 183 |
+
print(" [3/4] Handling NaN values...")
|
| 184 |
+
clean_data = handle_nan_values(harmonized, method='mean')
|
| 185 |
+
|
| 186 |
+
# Step 4: Create patches
|
| 187 |
+
print(" [4/4] Creating spatial patches...")
|
| 188 |
+
patches, patch_coords = create_patches(clean_data, patch_size, stride)
|
| 189 |
+
|
| 190 |
+
metadata = {
|
| 191 |
+
'original_shape': all_images.shape,
|
| 192 |
+
'selected_bands': SELECTED_BANDS,
|
| 193 |
+
'num_bands': len(SELECTED_BANDS),
|
| 194 |
+
'patch_size': patch_size,
|
| 195 |
+
'stride': stride,
|
| 196 |
+
'num_patches': len(patches),
|
| 197 |
+
'harmonized': True
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
print(f"[OK] Preprocessing complete. Created {len(patches)} patches.")
|
| 201 |
+
print(f" Patch shape: {patches.shape}")
|
| 202 |
+
|
| 203 |
+
return patches, patch_coords, metadata
|
vegetation_indices.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Vegetation Indices Calculator
|
| 3 |
+
==============================
|
| 4 |
+
|
| 5 |
+
This module provides functions to calculate 10 vegetation indices from Sentinel-2 data
|
| 6 |
+
and perform temporal analysis.
|
| 7 |
+
|
| 8 |
+
Indices:
|
| 9 |
+
- NDVI: Normalized Difference Vegetation Index
|
| 10 |
+
- EVI: Enhanced Vegetation Index
|
| 11 |
+
- NDWI: Normalized Difference Water Index
|
| 12 |
+
- NDRE: Normalized Difference Red Edge
|
| 13 |
+
- RECI: Red Edge Chlorophyll Index
|
| 14 |
+
- SMI: Soil Moisture Index
|
| 15 |
+
- NDSI: Normalized Difference Snow Index
|
| 16 |
+
- PRI: Photochemical Reflectance Index
|
| 17 |
+
- PSRI: Plant Senescence Reflectance Index
|
| 18 |
+
- MCARI: Modified Chlorophyll Absorption Ratio Index
|
| 19 |
+
- SASI: Salinity Index
|
| 20 |
+
- SOMI: Soil Organic Matter Index
|
| 21 |
+
- SFI: Soil Fertility Index
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
import numpy as np
|
| 25 |
+
from typing import Dict, List, Tuple
|
| 26 |
+
import datetime
|
| 27 |
+
from datetime import timedelta
|
| 28 |
+
|
| 29 |
+
# ==================== INDEX CALCULATION FUNCTIONS ====================
|
| 30 |
+
|
| 31 |
+
def calculate_ndvi(img: np.ndarray) -> np.ndarray:
|
| 32 |
+
"""NDVI = (NIR - RED) / (NIR + RED)"""
|
| 33 |
+
nir = img[:, :, 7] # B08
|
| 34 |
+
red = img[:, :, 3] # B04
|
| 35 |
+
return (nir - red) / (nir + red + 1e-10)
|
| 36 |
+
|
| 37 |
+
def calculate_evi(img: np.ndarray) -> np.ndarray:
|
| 38 |
+
"""EVI = 2.5 * ((NIR - RED) / (NIR + 6*RED - 7.5*BLUE + 1))"""
|
| 39 |
+
nir = img[:, :, 7] # B08
|
| 40 |
+
red = img[:, :, 3] # B04
|
| 41 |
+
blue = img[:, :, 1] # B02
|
| 42 |
+
return 2.5 * ((nir - red) / (nir + 6*red - 7.5*blue + 1))
|
| 43 |
+
|
| 44 |
+
def calculate_ndwi(img: np.ndarray) -> np.ndarray:
|
| 45 |
+
"""NDWI = (GREEN - NIR) / (GREEN + NIR)"""
|
| 46 |
+
green = img[:, :, 2] # B03
|
| 47 |
+
nir = img[:, :, 7] # B08
|
| 48 |
+
return (green - nir) / (green + nir + 1e-10)
|
| 49 |
+
|
| 50 |
+
def calculate_ndre(img: np.ndarray) -> np.ndarray:
|
| 51 |
+
"""NDRE = (NIR - RedEdge) / (NIR + RedEdge)"""
|
| 52 |
+
nir = img[:, :, 7] # B08
|
| 53 |
+
red_edge = img[:, :, 4] # B05
|
| 54 |
+
return (nir - red_edge) / (nir + red_edge + 1e-10)
|
| 55 |
+
|
| 56 |
+
def calculate_reci(img: np.ndarray) -> np.ndarray:
|
| 57 |
+
"""RECI = (NIR / RedEdge) - 1"""
|
| 58 |
+
nir = img[:, :, 7] # B08
|
| 59 |
+
red_edge = img[:, :, 4] # B05
|
| 60 |
+
return (nir / (red_edge + 1e-10)) - 1
|
| 61 |
+
|
| 62 |
+
def calculate_smi(img: np.ndarray) -> np.ndarray:
|
| 63 |
+
"""SMI = (SWIR1 - SWIR2) / (SWIR1 + SWIR2)"""
|
| 64 |
+
swir1 = img[:, :, 10] # B11
|
| 65 |
+
swir2 = img[:, :, 11] # B12
|
| 66 |
+
return (swir1 - swir2) / (swir1 + swir2 + 1e-10)
|
| 67 |
+
|
| 68 |
+
def calculate_ndsi(img: np.ndarray) -> np.ndarray:
|
| 69 |
+
"""NDSI = (GREEN - SWIR1) / (GREEN + SWIR1)"""
|
| 70 |
+
green = img[:, :, 2] # B03
|
| 71 |
+
swir1 = img[:, :, 10] # B11
|
| 72 |
+
return (green - swir1) / (green + swir1 + 1e-10)
|
| 73 |
+
|
| 74 |
+
def calculate_pri(img: np.ndarray) -> np.ndarray:
|
| 75 |
+
"""PRI = (B02 - B03) / (B02 + B03)"""
|
| 76 |
+
b02 = img[:, :, 1] # B02
|
| 77 |
+
b03 = img[:, :, 2] # B03
|
| 78 |
+
return (b02 - b03) / (b02 + b03 + 1e-10)
|
| 79 |
+
|
| 80 |
+
def calculate_psri(img: np.ndarray) -> np.ndarray:
|
| 81 |
+
"""PSRI = (RED - GREEN) / NIR"""
|
| 82 |
+
red = img[:, :, 3] # B04
|
| 83 |
+
green = img[:, :, 2] # B03
|
| 84 |
+
nir = img[:, :, 7] # B08
|
| 85 |
+
return (red - green) / (nir + 1e-10)
|
| 86 |
+
|
| 87 |
+
def calculate_mcari(img: np.ndarray) -> np.ndarray:
|
| 88 |
+
"""MCARI = ((B05 - B04) - 0.2 * (B05 - B03)) * (B05 / B04)"""
|
| 89 |
+
b03 = img[:, :, 2] # B03
|
| 90 |
+
b04 = img[:, :, 3] # B04
|
| 91 |
+
b05 = img[:, :, 4] # B05
|
| 92 |
+
return ((b05 - b04) - 0.2 * (b05 - b03)) * (b05 / (b04 + 1e-10))
|
| 93 |
+
|
| 94 |
+
def calculate_sasi(img: np.ndarray) -> np.ndarray:
|
| 95 |
+
"""SASI (Salinity Index) = SQRT(B11 * B04)"""
|
| 96 |
+
swir1 = img[:, :, 10] # B11
|
| 97 |
+
red = img[:, :, 3] # B04
|
| 98 |
+
return np.sqrt(swir1 * red)
|
| 99 |
+
|
| 100 |
+
def calculate_somi(img: np.ndarray) -> np.ndarray:
|
| 101 |
+
"""SOMI (Soil Organic Matter Index) = (B08 + B04) / (B11 + B12)"""
|
| 102 |
+
nir = img[:, :, 7] # B08
|
| 103 |
+
red = img[:, :, 3] # B04
|
| 104 |
+
swir1 = img[:, :, 10] # B11
|
| 105 |
+
swir2 = img[:, :, 11] # B12
|
| 106 |
+
return (nir + red) / (swir1 + swir2 + 1e-10)
|
| 107 |
+
|
| 108 |
+
def calculate_sfi(img: np.ndarray) -> np.ndarray:
|
| 109 |
+
"""SFI (Soil Fertility Index) = (NDVI * SOMI) / SASI
|
| 110 |
+
Combines vegetation health, organic matter, and salinity"""
|
| 111 |
+
ndvi = calculate_ndvi(img)
|
| 112 |
+
somi = calculate_somi(img)
|
| 113 |
+
sasi = calculate_sasi(img)
|
| 114 |
+
return (ndvi * somi) / (sasi + 1e-10)
|
| 115 |
+
|
| 116 |
+
# Index registry
|
| 117 |
+
INDEX_FUNCTIONS = {
|
| 118 |
+
'NDVI': calculate_ndvi,
|
| 119 |
+
'EVI': calculate_evi,
|
| 120 |
+
'NDWI': calculate_ndwi,
|
| 121 |
+
'NDRE': calculate_ndre,
|
| 122 |
+
'RECI': calculate_reci,
|
| 123 |
+
'SMI': calculate_smi,
|
| 124 |
+
'NDSI': calculate_ndsi,
|
| 125 |
+
'PRI': calculate_pri,
|
| 126 |
+
'PSRI': calculate_psri,
|
| 127 |
+
'MCARI': calculate_mcari,
|
| 128 |
+
'SASI': calculate_sasi,
|
| 129 |
+
'SOMI': calculate_somi,
|
| 130 |
+
'SFI': calculate_sfi
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
# ==================== BATCH CALCULATION ====================
|
| 134 |
+
|
| 135 |
+
def calculate_all_indices(img: np.ndarray) -> Dict[str, np.ndarray]:
|
| 136 |
+
"""
|
| 137 |
+
Calculate all 10 vegetation indices for a single image.
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
img: Image array of shape (height, width, 12) with reflectance values
|
| 141 |
+
|
| 142 |
+
Returns:
|
| 143 |
+
Dictionary mapping index names to 2D arrays
|
| 144 |
+
"""
|
| 145 |
+
indices = {}
|
| 146 |
+
for name, func in INDEX_FUNCTIONS.items():
|
| 147 |
+
indices[name] = func(img)
|
| 148 |
+
return indices
|
| 149 |
+
|
| 150 |
+
def calculate_indices_temporal(images: np.ndarray) -> Dict[str, np.ndarray]:
|
| 151 |
+
"""
|
| 152 |
+
Calculate all indices for multiple time steps.
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
images: Array of shape (time, height, width, 12)
|
| 156 |
+
|
| 157 |
+
Returns:
|
| 158 |
+
Dictionary mapping index names to 3D arrays (time, height, width)
|
| 159 |
+
"""
|
| 160 |
+
indices_data = {}
|
| 161 |
+
|
| 162 |
+
for index_name, calc_func in INDEX_FUNCTIONS.items():
|
| 163 |
+
index_series = []
|
| 164 |
+
for img in images:
|
| 165 |
+
index_map = calc_func(img)
|
| 166 |
+
index_series.append(index_map)
|
| 167 |
+
indices_data[index_name] = np.array(index_series)
|
| 168 |
+
|
| 169 |
+
return indices_data
|
| 170 |
+
|
| 171 |
+
# ==================== STATISTICS ====================
|
| 172 |
+
|
| 173 |
+
def get_field_statistics(index_map: np.ndarray) -> Dict[str, float]:
|
| 174 |
+
"""
|
| 175 |
+
Calculate statistics for a single index map.
|
| 176 |
+
|
| 177 |
+
Returns:
|
| 178 |
+
Dictionary with mean, std, min, max, median
|
| 179 |
+
"""
|
| 180 |
+
return {
|
| 181 |
+
'mean': float(np.nanmean(index_map)),
|
| 182 |
+
'std': float(np.nanstd(index_map)),
|
| 183 |
+
'min': float(np.nanmin(index_map)),
|
| 184 |
+
'max': float(np.nanmax(index_map)),
|
| 185 |
+
'median': float(np.nanmedian(index_map)),
|
| 186 |
+
'valid_pixels': int(np.sum(~np.isnan(index_map)))
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
def get_temporal_statistics(indices_temporal: Dict[str, np.ndarray]) -> Dict[str, Dict]:
|
| 190 |
+
"""
|
| 191 |
+
Calculate temporal statistics for all indices.
|
| 192 |
+
|
| 193 |
+
Args:
|
| 194 |
+
indices_temporal: Dictionary with index names and 3D arrays (time, height, width)
|
| 195 |
+
|
| 196 |
+
Returns:
|
| 197 |
+
Dictionary with temporal stats for each index
|
| 198 |
+
"""
|
| 199 |
+
temporal_stats = {}
|
| 200 |
+
|
| 201 |
+
for index_name, data in indices_temporal.items():
|
| 202 |
+
stats = {
|
| 203 |
+
'mean_over_time': np.nanmean(data, axis=0),
|
| 204 |
+
'std_over_time': np.nanstd(data, axis=0),
|
| 205 |
+
'max_over_time': np.nanmax(data, axis=0),
|
| 206 |
+
'min_over_time': np.nanmin(data, axis=0),
|
| 207 |
+
'range': np.nanmax(data, axis=0) - np.nanmin(data, axis=0),
|
| 208 |
+
'temporal_trend': data[-1] - data[0] if len(data) >= 2 else np.zeros_like(data[0]),
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
# Rolling average (window size = 3)
|
| 212 |
+
if len(data) >= 3:
|
| 213 |
+
rolling_avg = np.array([np.nanmean(data[max(0, i-2):i+1], axis=0)
|
| 214 |
+
for i in range(len(data))])
|
| 215 |
+
stats['rolling_avg_3'] = rolling_avg
|
| 216 |
+
|
| 217 |
+
temporal_stats[index_name] = stats
|
| 218 |
+
|
| 219 |
+
return temporal_stats
|
| 220 |
+
|
| 221 |
+
def get_summary_report(indices_temporal: Dict[str, np.ndarray],
|
| 222 |
+
dates: List[str]) -> Dict:
|
| 223 |
+
"""
|
| 224 |
+
Generate a comprehensive summary report.
|
| 225 |
+
|
| 226 |
+
Returns:
|
| 227 |
+
Dictionary with summary statistics and temporal trends
|
| 228 |
+
"""
|
| 229 |
+
report = {
|
| 230 |
+
'dates': dates,
|
| 231 |
+
'num_images': len(dates),
|
| 232 |
+
'indices': {}
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
for index_name, data in indices_temporal.items():
|
| 236 |
+
index_report = {
|
| 237 |
+
'latest': get_field_statistics(data[-1]),
|
| 238 |
+
'oldest': get_field_statistics(data[0]),
|
| 239 |
+
'mean_values_over_time': [float(np.nanmean(data[i])) for i in range(len(dates))],
|
| 240 |
+
'change': float(np.nanmean(data[-1]) - np.nanmean(data[0])),
|
| 241 |
+
'max_in_field': float(np.nanmax(data)),
|
| 242 |
+
'min_in_field': float(np.nanmin(data))
|
| 243 |
+
}
|
| 244 |
+
report['indices'][index_name] = index_report
|
| 245 |
+
|
| 246 |
+
return report
|