From Raw Satellite Pixels to a Working NDVI Alert Pipeline
A production walkthrough of Sentinel-2 L2A acquisition, cloud masking, median composites, crop-stress detection, spatial cross-validation, and alert delivery.
A cooperative managing 12,000 acres across 47 commercial farms came to us with a direct operational bottleneck: field scouts spent three days a week driving between distant parcels, and visible crop stress often meant yield damage was already irreversible. They needed a way to prioritize scouting before symptoms became visible to the naked eye.
Within 48 hours of kickoff, we deployed an end-to-end automated pipeline delivering parcel-level NDVI anomaly alerts. This is the technical walkthrough: the physical choices, processing patterns, validation design, and lessons learned.
Raw satellite pixels become actionable only when sensor physics, atmospheric corrections, crop phenology, and decision workflows are engineered as one cohesive system.
1. Acquire Sentinel-2 Level-2A Imagery
Sentinel-2 provides multispectral observations with Band 4 (red) and Band 8 (near-infrared) at 10 m spatial resolution. We strictly use Level-2A products because bottom-of-atmosphere (BOA) surface reflectance eliminates atmospheric path radiance variations when comparing vegetation through time.
Modern data access uses the Copernicus Data Space Ecosystem openEO client:
import openeo
connection = openeo.connect("openeo.dataspace.copernicus.eu").authenticate_oidc()
cube = connection.load_collection(
"SENTINEL2_L2A",
spatial_extent={"west": 73.70, "south": 31.00, "east": 74.55, "north": 31.70},
temporal_extent=["2025-04-01", "2025-07-15"],
bands=["B04", "B08", "SCL"],
max_cloud_cover=20,
)2. Mask Cloud, Cirrus, Snow, and Shadow with SCL
Cloud contamination is the fastest way to destroy trust in an automated NDVI system. The Sentinel-2 Level-2A Scene Classification Layer (SCL) identifies clouds, cirrus, and cloud shadows.
import numpy as np
import rasterio
from rasterio.enums import Resampling
def load_masked_bands(scl_path, red_path, nir_path):
with rasterio.open(scl_path) as src:
scl = src.read(1, out_shape=(src.height * 2, src.width * 2), resampling=Resampling.nearest)
valid = np.isin(scl, [4, 5]) # Vegetation and bare soil only
with rasterio.open(red_path) as red_src, rasterio.open(nir_path) as nir_src:
red = red_src.read(1).astype("float32")
nir = nir_src.read(1).astype("float32")
red[~valid] = np.nan
nir[~valid] = np.nan
return red, nir, validWhen an initial pass excluded clouds but forgot cloud shadow (SCL Class 3), affected canopy pixels dropped by ~0.15 NDVI units, generating spurious alerts. Masking shadows is mandatory for production reliability.
3. Use Robust Temporal Composites
A composite compresses multiple acquisitions into one clean value per pixel. We use a 30-day median composite requiring at least 4 valid cloud-free observations.
⬡ Interactive Comparison · Compositing Method
Median tracks the seasonal curve while resisting residual values at both tails.
def median_composite(ndvi_stack, min_observations=4):
valid_count = np.sum(~np.isnan(ndvi_stack), axis=0)
composite = np.nanmedian(ndvi_stack, axis=0)
composite[valid_count < min_observations] = np.nan
return composite, valid_count4. Compute NDVI & Compare with Crop-Stage Baselines
NDVI is calculated as (NIR − Red) / (NIR + Red). Rather than using brittle static cutoffs, we calculate standardized z-scores against historical phenological medians for each crop variety.
z_score = (current_ndvi - historical_median) / historical_std
alert_mask = z_score < -1.5 # Flag anomalies 1.5 standard deviations below normal◈ Interactive Teaching Model · Alert Sensitivity
5. Add a Lightweight CNN for Change Detection
We pair NDVI thresholding with a compact PyTorch CNN trained on multispectral 64×64 patches. Farm-level spatial cross-validation guarantees the model generalizes across unvisited parcels.
import torch.nn as nn
class CropStressCNN(nn.Module):
def __init__(self):
super().__init__()
self.encoder = nn.Sequential(
nn.Conv2d(6, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(64, 64, 3, padding=1), nn.ReLU(),
nn.AdaptiveAvgPool2d(1),
)
self.head = nn.Sequential(
nn.Flatten(), nn.Linear(64, 32), nn.ReLU(),
nn.Linear(32, 1), nn.Sigmoid(),
)
def forward(self, inputs):
return self.head(self.encoder(inputs))6. Deliver Alerts Inside the Field Workflow
Detection has no operational value if it stays trapped in a desktop GIS layer. The pipeline posts alerts to mobile push notifications and field management systems within 8 minutes of satellite scene ingest.
from firebase_admin import messaging
def send_alert(field_id, z_score, stress_probability, device_token):
severity = "SEVERE" if z_score < -2.0 else "MODERATE"
message = messaging.Message(
notification=messaging.Notification(
title=f"{severity}: Inspect Field {field_id}",
body=f"NDVI is {abs(z_score):.1f}σ below baseline (confidence {stress_probability:.0%}).",
),
data={"field_id": field_id, "event": "crop_stress_review"},
token=device_token,
)
return messaging.send(message)Field Results & Operational Impact
Deploy Remote Sensing in Production
Build Automated Satellite Alert Pipelines
Infryne TechWorks develops end-to-end satellite computer vision, change detection, and precision agriculture pipelines from raw pixels to operational APIs.