AgriTech için Uydu ve Hava Durumu API'leri: Sentinel-2 ve Python ile Tahmine Dayalı Veriler
2024'te Avrupa Uzay Ajansı'nın satın aldığı 1,5 petabayt veri her gün Copernicus programının Sentinel uydularından. Bunlardan tarımsal izlemeye ayrılan kısım Avrupa'da en büyük ve stratejik açıdan en uygun segmenti temsil etmektedir. Yine de büyük çoğunluk Teknolojik açıdan en gelişmiş olanları da dahil olmak üzere, İtalyan tarım şirketlerinin yüzde 50'si henüz bu halka açık, ücretsiz ve bilimsel olarak doğrulanmış altyapıdır.
Bunun nedeni veri eksikliği değil; ona erişmenin, onu işlemenin ve dönüştürmenin karmaşıklığıdır. somut tarımsal kararlar. Bir Sentinel-2 uydusu her 5 günde bir bağınızın üzerinden geçer. Çözünürlük piksel başına 10 metredir. Her geçişte sağlığı kodlayan 13 spektral bant üretilir bitki örtüsü, su stresi, klorofil içeriği, parazitlerin varlığı. Verilerle birleştirilmiş hava durumu, IoT yer sensörleri ve tahmine dayalı modeller sayesinde bu veriler radikal biçimde dönüştürücü olabilir tarımsal kararların kalitesi, girdi maliyetlerinin %15-25 oranında azaltılması ve verimin arttırılması %10-20'ye kadar.
Küresel Yer Gözlem pazarı buna değer 2025'te 10,07 milyar dolar Straits Research'e göre büyümenin 2033 yılına kadar 17,20 milyara ulaşması bekleniyor (CAGR %6,92). Tarım segmenti başvuruların %21'ini temsil ediyor ve en yüksek oranda büyüyor. hassas tarım, verim izleme ve sulama optimizasyonuna olan talepten kaynaklanmaktadır. Bu makalede, Sentinel-2'ye erişim için adım adım eksiksiz bir Python işlem hattı oluşturuyoruz CDSE (Copernicus Veri Alanı Ekosistemi) aracılığıyla NDVI ve diğer bitki örtüsü endekslerini hesaplayın, entegre edin Open-Meteo'dan alınan meteorolojik veriler ve su stresi için tahmine dayalı bir model besliyor.
Bu Makalede Neler Öğreneceksiniz?
- Copernicus programının mimarisi ve CDSE aracılığıyla Sentinel-2'ye ücretsiz erişim
- Bitki örtüsü endeksleri: NDVI, EVI, SAVI, LAI - İtalyan mahsulleri için matematiksel formüller, yorumlama ve eşikler
- Eksiksiz Python uygulaması: CDSE kimlik doğrulaması, bant indirme, rasterio ve numpy ile NDVI hesaplaması
- Karşılaştırılan hava durumu API'leri: Open-Meteo, OpenWeatherMap, Tomorrow.io ve uydu veri entegrasyonu
- Vektör ve raster verileri için GeoPandas, rasterio ve PostGIS içeren jeouzamsal boru hattı
- Su stresi tahmin modeli: Sentinel-2 + hava durumu + IoT'nin scikit-learn ile birleştirilmesi
- Pratik vaka çalışması: Puglia'daki bağ, mevsimsel NDVI izleme, sulama optimizasyonu
- İtalyan mevzuatı: AGEA, SIAN, açık veri CAP ve dijital CAP
FoodTech Serisi - Tüm Makaleler
| # | Öğe | Seviye | Durum |
|---|---|---|---|
| 1 | Python ve MQTT ile Hassas Tarım için IoT Pipeline | Gelişmiş | Mevcut |
| 2 | Mahsul İzleme için ML Edge: Tarlalarda Bilgisayarlı Görme | Gelişmiş | Mevcut |
| 3 | AgriTech için Uydu ve Hava Durumu API'leri: Tahmine Dayalı Veriler (şu anda buradasınız) | Gelişmiş | Akım |
| 4 | Gıdada Blockchain izlenebilirliği: Tarladan süpermarkete | Orta seviye | Yakında gelecek |
| 5 | Gıda Endüstrisinde Kalite Kontrol için Bilgisayarlı Görme | Gelişmiş | Yakında gelecek |
| 6 | FSMA ve Dijital Uyumluluk: Düzenleyici Süreçlerin Otomasyonu | Orta seviye | Yakında gelecek |
| 7 | Dikey Tarım: IoT ve ML ile Çevresel Kontrol | Gelişmiş | Yakında gelecek |
| 8 | Prophet ve LightGBM ile Gıda Perakendesinde Talep Tahmini | Orta seviye | Yakında gelecek |
| 9 | Çiftlik Zekası Kontrol Paneli: Grafana ile Gerçek Zamanlı Analiz | Orta seviye | Yakında gelecek |
| 10 | Tedarik Zinciri Gıda Optimizasyonu: Atıkların Azaltılması için ML | Orta seviye | Yakında gelecek |
Kopernik Programı: Avrupa'nın Dünya Gözlem Altyapısı
Avrupa Birliği'nin Copernicus programı dünyadaki en büyük gözlem altyapısıdır. Arazi asla inşa edilmedi. ESA (Avrupa Uzay Ajansı) ve Avrupa Komisyonu tarafından yönetilmektedir. EUMETSAT'ın operasyonel desteği olan Copernicus, adlandırılmış uydulardan oluşan bir takımyıldızı işletiyor Her biri belirli izleme görevleri için tasarlanmış "Nöbetçi". Tarım için, mutlak referans uydusu e Nöbetçi-2.
Sentinel-2 takımyıldızı iki ikiz uydudan (Sentinel-2A ve Sentinel-2B) oluşur. 786 km yükseklikte güneşle senkronize yörüngede. İki uydu konfigürasyonu, zaman 5 günlük inceleme ekvatorda ve Avrupa enlemlerinde 2-3 gün (İtalya dahil). Her uydu, üzerinde bir süpürge olan MSI (MultiSpectral Instrument) sensörünü taşır. 290 km kaydırma genişliği ve 13 spektral bant ile veri toplayan tarayıcı üç uzamsal çözünürlüğe dağıtılmıştır: 10 metre, 20 metre ve 60 metre.
Sentinel-2'nin en stratejik özelliği tamamen ücretsiz: 2014'ten beri tüm Copernicus verileri ticari veya ticari olmayan her türlü kullanım için açık erişimde mevcuttur ticari. 2023'ten itibaren yeni erişim portalı ve Kopernik Veri Alanı Ekosistemi (CDSE)2010 yılında hizmet dışı bırakılan önceki Copernicus Açık Erişim Merkezinin (SciHub) yerini alan 2023. CDSE, edinme gecikmesiyle Sentinel-2 arşivinin tamamına anında erişim sunuyor En son görüntüler için yaklaşık 3 saat kullanılabilir.
Satellite Sentinel-2: MSI Sensör Teknik Özellikleri
| Bant | Dalga boyu (nm) | Çözünürlük | Ana Uygulama |
|---|---|---|---|
| B1 - Kıyı aerosolleri | 443 | 60 m | hava kalitesi, atmosferik düzeltme |
| B2 - Mavi | 490 | 10 m | Bitki örtüsü/toprak ayrımı, haritalama |
| B3 - Yeşil | 560 | 10 m | Bitki örtüsünün gücü, yaprak rengi |
| B4 -Kırmızı | 665 | 10 m | Klorofil, NDVI (kırmızı bant) |
| B5 - Bitki Örtüsü Kırmızı Kenar | 705 | 20 m | Bitki örtüsü stresi, Red Edge NDVI |
| B6 - Bitki Örtüsü Kırmızı Kenar | 740 | 20 m | Klorofil içeriği, tür sınıflandırması |
| B7 - Bitki Örtüsü Kırmızı Kenar | 783 | 20 m | Biyokütle, LAI |
| B8 - NIR | 842 | 10 m | NDVI (NIR bandı), biyokütle, LAI |
| B8a - Bitki Örtüsü Kırmızı Kenar | 865 | 20 m | Kanopi yapısı, NDVI dar |
| B9 - Su buharı | 940 | 60 m | Atmosferdeki su buharı düzeltmesi |
| B10 - SWIR - Cirrus | 1375 | 60 m | Cirrus bulut tespiti |
| B11 - SWIR | 1610 | 20 m | Su stresi, yaprak su içeriği |
| B12 - SWIR | 2190 | 20 m | İleri su stresi, yaşlanma |
Tarımla ilgili temel olarak Sentinel-2 veri işlemenin iki düzeyi vardır: Seviye-1C (L1C) - geometrik düzeltmeyle üst atmosfer parlaklığı, e Seviye-2A (L2A) - atmosferin tabanındaki yansıma (Atmosferin Tabanı, BOA) Sen2Cor algoritması aracılığıyla uygulanan atmosferik düzeltme ile. Doğrudan tarımsal uygulamalar için, Seviye-2A, atmosferin etkisine göre zaten düzeltilmiş olduğundan önerilen seviyedir. Farklı tarihler ve farklı coğrafi konumlar arasındaki karşılaştırılabilir yansıma değerleri.
AgriTech için Uydu Platformları: Tam Karşılaştırma 2025
Sentinel-2 mevcut tek seçenek değil. Tarım için uydu veri pazarı Planet Labs gibi ticari sağlayıcıları, Landsat (NASA/USGS) gibi yerleşik firmaları ve Google Earth Engine gibi entegre bulut platformları. Seçim karmaşık bir değiş tokuşa bağlıdır mekansal çözünürlük, edinme sıklığı, yönetilen bulut kapsamı ve kullanılabilir bütçe arasında.
Hassas Tarıma Yönelik Uydu Platformlarının Karşılaştırması - 2025
| platformu | Uzamsal Çözünürlük | Tekrar Ziyaret Sıklığı | Spektral Bantlar | Maliyet | Veri Gecikmesi | API'ler |
|---|---|---|---|---|---|---|
| Sentinel-2 (ESA/Kopernik) | 10m (vis/NIR), 20m (SWIR) | 5 gün (AB'de 2-3) | 13 MSI grubu | Ücretsiz (açık veri) | Alındıktan sonra ~3 saat | CDSE OData, STAC, SentinelHub, OpenEO |
| Gezegen Gezegen Kapsamı | 3,7 m | Günlük (yarı günlük) | 8 bant (PS2.SD) | Ticari Abonelik; araştırma için ücretsiz | 24 saat | Planet API v1, Abonelikler API'sı |
| Landsat 8/9 (NASA/USGS) | 30 m (multispektral), 15 m (pan) | 16 gün | 11 bant OLI/TIRS | Ücretsiz (açık veri) | 24-48 saat | USGS EarthExplorer, Google Earth Motoru |
| MODIS (NASA Dünya/Su) | 250m / 500m / 1km | 1-2 gün | 36 bant | Ücretsiz (açık veri) | 24-48 saat | NASA EarthData STAC, Google Earth Motoru |
| Google Earth Motoru | Veri setine bağlıdır (10m-1km) | Veri kümesine bağlıdır | Entegre çoklu veri kümeleri | Ücretsiz, ticari olmayan; ücretli ticari | Anında bulut işleme | Python ee, JavaScript API'si |
| Maxar WorldView-3 | 0,3 m (tava), 1,24 m (çoklu) | 1-4 gün | 29 bant (CAVIS) | ~25-40 USD/km2 | 4-8 saat | Maxar Akış API'si |
| Airbus Ülker Neo | 0,3 m | 1-2 gün | 6 multispektral bant | ~15-30 EUR/km2 | 24-48 saat | OneAtlas API'si |
İtalya'daki tarımsal uygulamaların büyük çoğunluğu için, Sentinel-2 ve seçim optimal: 0,5 hektardan büyük parseller için 10 metrelik çözünürlük yeterlidir (altında piksel istatistiklerinin güvenilmez hale geldiği eşik), tekrar ziyaret sıklığı İtalya'da 2-3 günlük ve mevsimsel izleme için yeterli ve tamamen ücretsiz Evlat edinmenin önündeki her türlü ekonomik engeli ortadan kaldırır. Planet Labs yalnızca aşağıdakilerle ilgili hale gelir: neredeyse günlük izleme gerektiren uygulamalar (örneğin, hastalığın erken başlangıcını tespit etmek) yoğun meyve ve sebzeler gibi yüksek değerli ürünlerde su stresi) veya 10 metrenin altındaki çözünürlük. MODIS ve Landsat, geniş alanların ve çok yıllık zaman serilerinin analizleri için kullanışlı olmaya devam ediyor.
Bitki Örtüsü Endeksleri: İtalyan mahsulleri için NDVI, EVI, SAVI ve LAI
Bitki Örtüsü Endeksleri (VI), matematiksel olarak türetilen ölçümlerdir. Belirli bitki örtüsü özelliklerini ölçen spektral bantların birleşimi. Sömürüyorlar klorofilin kırmızı bantta (665 nm) güçlü bir şekilde soğurulması ve güçlü bir şekilde yansıtılması gerçeği yakın kızılötesinde (842 nm): bu bantlar arasındaki oran, dalganın yoğunluğunu ve canlılığını kodlar. matematiksel zarafete sahip bitki örtüsü.
NDVI - Normalleştirilmiş Fark Bitki Örtüsü İndeksi
NDVI, Rouse ve diğerleri tarafından tanıtılan, dünyada en çok kullanılan bitki örtüsü indeksidir. 1973'te. Matematiksel formül şöyledir:
NDVI formülü
NDVI = (NIR - Red) / (NIR + Red)
Su Sentinel-2:
NDVI = (B8 - B4) / (B8 + B4)
Range: da -1.0 a +1.0
NDVI değerleri standartlaştırılmış bir ölçeğe göre yorumlanır ancak optimum eşikler farklılık gösterir mahsul ve fenolojik aşamaya göre. Aşağıdaki tabloda referans eşikleri gösterilmektedir: Bilimsel literatürden ve ampirik doğrulamalardan elde edilen başlıca İtalyan bitkileri tipik bölgelerde (Pianura Padana, Puglia, Sicilya, Toskana):
İtalyan Mahsulleri için NDVI Yorumu
| NDVI Aralığı | Genel Yorum | Buğday (Triticum) | Asma (Vitis) | Domates (Solanum) | Mısır (Zea mays) | Zeytin (Olea) |
|---|---|---|---|---|---|---|
| < 0,1 | Çıplak zemin, kaya, kar | Ekilmemiş arazi | Kış, tomurcuklanma öncesi | Hasat sonrası | Ekim öncesi | Sıralar arasında çıplak zemin |
| 0,1 - 0,2 | Çok seyrek veya kuru bitki örtüsü | İlk acil durum | Uyuşukluk/şiddetli stres | Son nakil | Acil Durum (VE) | Şiddetli su/termal stres |
| 0,2 - 0,4 | Seyrek bitki örtüsü, orta düzeyde stres | İlk kardeşlenme | Çiçeklenme öncesi, hafif stres | İlk bitkisel büyüme | Aşamalar V1-V3 | Yetersiz bitki örtüsü, stres |
| 0,4 - 0,6 | Orta derecede bitki örtüsü, iyi sağlık | Tam yükselen | Çiçeklenme öncesi / meyve tutumu | Aktif bitkisel gelişim | Aşamalar V4-V8 | Normal yeşillik, iyi sulanmış |
| 0,6 - 0,8 | Yoğun bitki örtüsü, mükemmel canlılık | Yön (mevsimsel zirve) | Tam yapraklanma, ben düşme | Maksimum kapsama alanı (çiçeklenme) | Aşamaları V10-VT (çiçeklenme) | Yoğun bitki örtüsü, mükemmel durum |
| > 0,8 | Çok yoğun bitki örtüsü, orman | Nadir (tarla kenarı ormanları) | Çitler ve sınır ağaçları | Toplam kapsama alanına sahip seralar | Nadir optimal koşullar | Yüksek yoğunluklu zeytinlikler |
EVI - Gelişmiş Bitki Örtüsü İndeksi
Huete ve arkadaşları tarafından geliştirilen EVI. MODIS sensörü için alanlardaki NDVI sınırlarını düzeltir yüksek bitki örtüsü yoğunluğuna sahip (0,7'nin üzerinde NDVI doygunluğu) ve açık topraklara sahip alanlarda (Zemin yansımasından kaynaklanan NDVI hataları). Formül:
EVI = G * (NIR - Red) / (NIR + C1*Red - C2*Blue + L)
Su Sentinel-2:
EVI = 2.5 * (B8 - B4) / (B8 + 6*B4 - 7.5*B2 + 1)
Costanti standard:
G = 2.5 (guadagno)
C1 = 6.0 (coefficiente resistenza aerosol)
C2 = 7.5 (coefficiente resistenza aerosol)
L = 1.0 (fattore aggiustamento canopy)
Range: -1.0 a +1.0 (tipicamente 0 a 0.9 per vegetazione)
SAVI - Toprağa Ayarlanmış Bitki Örtüsü İndeksi
Huete tarafından 1988'de önerilen SAVI, özellikle aşağıdaki gibi yarı kurak ortamlarda faydalıdır: Açıkta kalan toprağın spektral tepkiyi önemli ölçüde etkilediği Puglia veya Sicilya. Düzeltme faktörü L zeminin etkisini azaltır:
SAVI = (NIR - Red) * (1 + L) / (NIR + Red + L)
Su Sentinel-2:
SAVI = (B8 - B4) * (1 + 0.5) / (B8 + B4 + 0.5)
L = 0.5 (valore standard per vegetazione media)
L = 1.0 per vegetazione molto rada
L = 0.25 per vegetazione densa
Vantaggioso rispetto a NDVI quando:
- Copertura vegetale < 40%
- Suoli chiari e brillanti (calcari, sabbie)
- Areali semi-aridi del Sud Italia
LAI - Yaprak Alanı İndeksi
LAI (Yaprak Alanı İndeksi), yüzey alanı birimi başına toplam yaprak yüzeyini ölçer toprak. Potansiyel verimlilikle ilgili temel bir tarımsal parametredir ve terleme. Sentinel-2 ampirik veya fiziksel algoritmalar kullanarak LAI'yi tahmin etmenize olanak tanır (SNAP Biyofiziksel İşlemci):
# Stima LAI empirica da NDVI (relazione di Boegh et al., corretta per Sentinel-2)
# Valida per cereali e colture erbacee europee
LAI_stima = -0.3 + 10.2 * NDVI # valida per NDVI in [0.2, 0.8]
# Per la vite (relazione specifica da letteratura italiana):
LAI_vite = 0.57 * EXP(3.28 * NDVI) # Dalla et al., 2019
# Soglie LAI indicative per colture italiane:
# Grano: LAI ottimale 3-5 m2/m2 alla spigatura
# Mais: LAI ottimale 3-5 m2/m2 alla fioritura
# Vite: LAI ottimale 1.5-2.5 m2/m2 durante invaiatura
# Pomodoro: LAI ottimale 3-4 m2/m2 durante copertura massima
Uydu Bitki Örtüsü İndekslerinin Sınırlamaları
- Bulutluluk: Sentinel-2 ve optik bulutlar görüntüyü kullanılamaz hale getirir. İtalya'da ortalama bulut örtüsü kışın %48, yazın ise %15'tir. QA60 bandı otomatik bulut maskelemeye olanak tanır.
- NDVI doygunluğu: NDVI ~0,7-0,8'in üzerinde hassasiyet büyük ölçüde azalır. Yüksek yoğunluklu ürünler için EVI kullanın.
- Piksel karışımı: 10 m çözünürlükte bir piksel hem bitki örtüsünü hem de toprağı içerebilir. Küçük parseller için (< 0,5 ha) ortalama NDVI daha az temsil edicidir.
- Fenolojik mevsimsellik: NDVI'yi farklı tarihler arasında karşılaştırmak, yalnızca mutlak değerin değil, fenolojik aşamanın da hesaba katılmasını gerektirir.
- Yıllar arası değişkenlik: NDVI ayrıca tarımsal uygulamalardan bağımsız olarak mevsimsel hava koşullarına (sıcaklık, yağış) bağlı olarak da değişir.
Python Uygulaması: CDSE Erişimi ve Sentinel-2'yi İndirme
Copernicus Veri Alanı Ekosistemi (CDSE), Sentinel verilerine erişim için tek portal haline geldi 2023'ten beri. Dört ana API sunar: OData API'si (ürünleri arayın ve indirin), STAC API'si (SpatioTemporal Varlık Kataloğu), OpenEO API'si (işleniyor standartlaştırılmış) e Sentinel Hub API'si (hizmet olarak işleniyor). Başlamak için, dataspace.copernicus.eu'da ücretsiz bir hesap oluşturun ve OAuth2 kimlik bilgileri oluşturun.
Python Ortam Kurulumu ve Bağımlılıkları
# Installa le librerie necessarie
pip install sentinelhub rasterio numpy pandas geopandas shapely \
requests python-dotenv matplotlib scipy scikit-learn \
openmeteo-requests retry-requests xarray
# requirements.txt con versioni validate per Python 3.11+
# sentinelhub==3.11.4 - interfaccia CDSE e SentinelHub
# rasterio==1.4.0 - lettura/scrittura raster geospaziali
# numpy==2.1.0 - calcolo matriciale per bande
# geopandas==1.0.1 - dati vettoriali e spatial operations
# shapely==2.0.6 - geometrie per AOI (Area of Interest)
# openmeteo-requests==0.3.3 - client Open-Meteo API
# scikit-learn==1.5.2 - modello predittivo stress idrico
OAuth2 ile CDSE kimlik doğrulaması
import os
import requests
from datetime import datetime, timedelta
from dotenv import load_dotenv
load_dotenv()
# Configurazione credenziali CDSE (da variabili d'ambiente)
CDSE_USERNAME = os.getenv("CDSE_USERNAME")
CDSE_PASSWORD = os.getenv("CDSE_PASSWORD")
CDSE_CLIENT_ID = "cdse-public"
CDSE_TOKEN_URL = "https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token"
def get_cdse_access_token(username: str, password: str) -> str:
"""
Ottieni access token OAuth2 dal CDSE Identity Server.
Il token ha durata di 600 secondi (10 minuti).
"""
response = requests.post(
CDSE_TOKEN_URL,
data={
"grant_type": "password",
"client_id": CDSE_CLIENT_ID,
"username": username,
"password": password,
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=30,
)
response.raise_for_status()
token_data = response.json()
return token_data["access_token"]
class CDSESession:
"""Sessione autenticata con gestione automatica del refresh token."""
def __init__(self, username: str, password: str):
self._username = username
self._password = password
self._token: str | None = None
self._token_expiry: datetime | None = None
def _refresh_token(self) -> None:
self._token = get_cdse_access_token(self._username, self._password)
# Margine di 60 secondi prima della scadenza reale (600s)
self._token_expiry = datetime.utcnow() + timedelta(seconds=540)
def get_headers(self) -> dict:
if self._token is None or datetime.utcnow() >= self._token_expiry:
self._refresh_token()
return {"Authorization": f"Bearer {self._token}"}
# Inizializza sessione
session = CDSESession(CDSE_USERNAME, CDSE_PASSWORD)
OData API aracılığıyla Sentinel-2 Ürünlerini Arayın
from shapely.geometry import box
import json
ODATA_BASE_URL = "https://catalogue.dataspace.copernicus.eu/odata/v1"
def search_sentinel2_products(
bbox: tuple[float, float, float, float], # (lon_min, lat_min, lon_max, lat_max)
start_date: str, # formato: "2024-05-01T00:00:00.000Z"
end_date: str, # formato: "2024-05-31T23:59:59.000Z"
cloud_cover_max: float = 20.0,
product_type: str = "S2MSI2A", # L2A = Bottom of Atmosphere, preferire per agricoltura
max_results: int = 20,
) -> list[dict]:
"""
Cerca prodotti Sentinel-2 nel catalogo CDSE.
Args:
bbox: Bounding box nell'ordine (lon_min, lat_min, lon_max, lat_max)
start_date: Data inizio (formato ISO 8601)
end_date: Data fine (formato ISO 8601)
cloud_cover_max: Percentuale massima di copertura nuvolosa (0-100)
product_type: S2MSI2A (L2A, consigliato) o S2MSI1C (L1C)
max_results: Numero massimo di risultati
Returns:
Lista di dizionari con metadata prodotto
"""
lon_min, lat_min, lon_max, lat_max = bbox
aoi_wkt = f"POLYGON(( {lon_min} {lat_min}, {lon_max} {lat_min}, {lon_max} {lat_max}, {lon_min} {lat_max}, {lon_min} {lat_min} ))"
filter_query = (
f"Collection/Name eq 'SENTINEL-2' "
f"and Attributes/OData.CSC.DoubleAttribute/any(att:att/Name eq 'cloudCover' "
f" and att/OData.CSC.DoubleAttribute/Value le {cloud_cover_max}) "
f"and Attributes/OData.CSC.StringAttribute/any(att:att/Name eq 'productType' "
f" and att/OData.CSC.StringAttribute/Value eq '{product_type}') "
f"and ContentDate/Start ge {start_date} "
f"and ContentDate/Start lt {end_date} "
f"and OData.CSC.Intersects(area=geography'SRID=4326;{aoi_wkt}')"
)
params = {
"$filter": filter_query,
"$orderby": "ContentDate/Start desc",
"$top": max_results,
"$expand": "Attributes",
}
response = requests.get(
f"{ODATA_BASE_URL}/Products",
params=params,
timeout=60,
)
response.raise_for_status()
data = response.json()
return data.get("value", [])
# Esempio: cerca immagini per la vigna di Manduria (Puglia)
# Campagna estiva: maggio-settembre 2024
bbox_manduria = (17.4, 40.3, 17.7, 40.5) # Zona DOC Primitivo di Manduria
products = search_sentinel2_products(
bbox=bbox_manduria,
start_date="2024-05-01T00:00:00.000Z",
end_date="2024-09-30T23:59:59.000Z",
cloud_cover_max=10.0,
product_type="S2MSI2A",
)
print(f"Trovati {len(products)} prodotti Sentinel-2 L2A con copertura nuvolosa <= 10%")
for p in products[:5]:
cloud = next(
(a["Value"] for a in p.get("Attributes", []) if a["Name"] == "cloudCover"),
"N/A",
)
print(f" {p['Name']} | Cloud: {cloud:.1f}% | Date: {p['ContentDate']['Start'][:10]}")
Sentinel Hub Python ile Bantları İndirin ve Erişin
Tüm sahneyi indirmeden bant işleme için (her biri 800 MB - 1,2 GB ağırlığındadır) L2A ürünü), yalnızca ilgilenilen bölgede gerekli olan bantlar:
from sentinelhub import (
SHConfig,
SentinelHubRequest,
DataCollection,
MimeType,
BBox,
CRS,
bbox_to_dimensions,
)
import numpy as np
# Configurazione SentinelHub via CDSE
config = SHConfig()
config.sh_client_id = os.getenv("SH_CLIENT_ID") # da apps.sentinel-hub.com
config.sh_client_secret = os.getenv("SH_CLIENT_SECRET")
config.sh_base_url = "https://sh.dataspace.copernicus.eu" # endpoint CDSE
# Definizione Area of Interest (AOI) - Vigna Manduria 100 ha circa
aoi_bbox = BBox(
bbox=[17.42, 40.34, 17.52, 40.42],
crs=CRS.WGS84,
)
# Calcola dimensioni raster a 10m di risoluzione
size = bbox_to_dimensions(aoi_bbox, resolution=10)
print(f"Dimensioni raster: {size[0]} x {size[1]} pixel a 10m")
# Evalscript per scaricare B04 (Red) e B08 (NIR) per calcolo NDVI
evalscript_ndvi_bands = """
//VERSION=3
function setup() {
return {
input: [{
bands: ["B04", "B08", "CLM"], // Red, NIR, Cloud Mask
units: "REFLECTANCE"
}],
output: {
bands: 3,
sampleType: "FLOAT32"
}
};
}
function evaluatePixel(sample) {
// Restituisce [Red, NIR, CloudMask]
return [sample.B04, sample.B08, sample.CLM];
}
"""
# Richiesta dati
request = SentinelHubRequest(
evalscript=evalscript_ndvi_bands,
input_data=[
SentinelHubRequest.input_data(
data_collection=DataCollection.SENTINEL2_L2A.define_from(
"s2l2a",
service_url=config.sh_base_url,
),
time_interval=("2024-07-15", "2024-07-20"),
other_args={"dataFilter": {"maxCloudCoverage": 10}},
)
],
responses=[SentinelHubRequest.output_response("default", MimeType.TIFF)],
bbox=aoi_bbox,
size=size,
config=config,
)
# Download dati (restituisce array numpy [H, W, Bande])
images = request.get_data()
band_data = images[0] # shape: (height, width, 3)
red_band = band_data[:, :, 0].astype(np.float32) # B04 - Red
nir_band = band_data[:, :, 1].astype(np.float32) # B08 - NIR
cloud_mask = band_data[:, :, 2] # CLM - 0=clear, 1=cloud
print(f"Bande scaricate - Shape: {red_band.shape}")
print(f"Red B04 - Min: {red_band.min():.4f}, Max: {red_band.max():.4f}, Mean: {red_band.mean():.4f}")
print(f"NIR B08 - Min: {nir_band.min():.4f}, Max: {nir_band.max():.4f}, Mean: {nir_band.mean():.4f}")
print(f"Pixel nuvolosi: {cloud_mask.sum()} su {cloud_mask.size} totali ({100*cloud_mask.mean():.1f}%)")
NDVI hesaplaması ve GeoTIFF tasarrufu
import rasterio
from rasterio.transform import from_bounds
from rasterio.crs import CRS as RasterioCRS
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
def calculate_ndvi(
red: np.ndarray,
nir: np.ndarray,
cloud_mask: np.ndarray | None = None,
) -> np.ndarray:
"""
Calcola NDVI dalla banda rossa e NIR.
Gestisce divisione per zero con np.errstate.
Maschera i pixel nuvolosi con np.nan.
Returns:
Array NDVI con float32, range [-1, 1], NaN per pixel invalidi
"""
with np.errstate(divide="ignore", invalid="ignore"):
ndvi = np.where(
(nir + red) == 0,
np.nan,
(nir - red) / (nir + red),
).astype(np.float32)
# Maschera nuvole
if cloud_mask is not None:
ndvi = np.where(cloud_mask == 1, np.nan, ndvi)
# Clip valori fisicamente impossibili (artefatti strumentali)
ndvi = np.clip(ndvi, -1.0, 1.0)
return ndvi
def save_ndvi_geotiff(
ndvi: np.ndarray,
bbox_wgs84: tuple[float, float, float, float],
output_path: str,
) -> None:
"""
Salva l'array NDVI come GeoTIFF georeferenziato in WGS84.
Args:
ndvi: Array NDVI 2D (H, W)
bbox_wgs84: (lon_min, lat_min, lon_max, lat_max)
output_path: Path del file output
"""
height, width = ndvi.shape
lon_min, lat_min, lon_max, lat_max = bbox_wgs84
transform = from_bounds(lon_min, lat_min, lon_max, lat_max, width, height)
with rasterio.open(
output_path,
"w",
driver="GTiff",
height=height,
width=width,
count=1,
dtype=rasterio.float32,
crs=RasterioCRS.from_epsg(4326),
transform=transform,
compress="lzw",
nodata=np.nan,
) as dst:
dst.write(ndvi, 1)
dst.update_tags(
NDVI_COMPUTED_AT=datetime.utcnow().isoformat(),
SENTINEL2_PRODUCT_TYPE="S2MSI2A",
FORMULA="(B08-B04)/(B08+B04)",
)
# Calcola e salva NDVI
ndvi_map = calculate_ndvi(red_band, nir_band, cloud_mask)
# Statistiche NDVI per l'AOI (escluso NaN)
valid_pixels = ndvi_map[~np.isnan(ndvi_map)]
print(f"\nStatistiche NDVI - Vigna Manduria (luglio 2024)")
print(f" Pixel validi: {len(valid_pixels)} su {ndvi_map.size}")
print(f" NDVI medio: {valid_pixels.mean():.3f}")
print(f" NDVI mediano: {np.median(valid_pixels):.3f}")
print(f" NDVI p10: {np.percentile(valid_pixels, 10):.3f} (zone a stress)")
print(f" NDVI p90: {np.percentile(valid_pixels, 90):.3f} (zone ottimali)")
bbox_wgs84 = (17.42, 40.34, 17.52, 40.42)
save_ndvi_geotiff(ndvi_map, bbox_wgs84, "ndvi_manduria_20240717.tif")
print("GeoTIFF NDVI salvato: ndvi_manduria_20240717.tif")
AgriTech için Hava Durumu API'leri: Karşılaştırma ve Uygulama
Hava durumu verileri hassas tarımın ikinci kritik veri kaynağıdır. Yerdeki NDVI ve IoT verileriyle entegre olarak, tahmine dayalı modeller oluşturmanıza olanak tanır. su stresi, hastalık riski, buharlaşma ve verim tahmini. Panorama 2025'te hava durumu API'lerinin yüzdesi ücretsiz açık kaynak sağlayıcılar, ticari sağlayıcılar olarak ikiye bölünecek İtalyan ARPA'nın ücretsiz katmanları ve bölgesel hizmetleriyle.
AgriTech için Hava Durumu API'leri - Karşılaştırma 2025
| Sağlayıcılar | Fiyat | Uzamsal Çözünürlük | Tahmin etmek | Tarımsal değişkenler | Tarihsel Arşiv | API'ler |
|---|---|---|---|---|---|---|
| Açık Hava | Ücretsiz (ticari olmayan); ~15$/ay reklam | 1-11 km (çoklu model) | 16 gün | ET0, toprak sıcaklığı, toprak nemi, buhar basıncı | 1940-günümüz (ERA5) | REST JSON, Python istemcisi |
| OpenWeather Haritası | Dakikada 60 çağrıya kadar ücretsiz; $40/ay profesyonel | 2,5 km (SİMGE) | 5 gün (ücretsiz), 16 gün (pro) | Sınırlı, ET0 yok | Yalnızca ücretli geçmiş | REST JSON, Python SDK'sı |
| yarın.io | Ücretsiz 500 arama/gün; 199$/ay çekirdek | 1km | 21 gün | Toprak Nemi, ET0, püskürtme koşulları, haşere riski | 6 yıl | REST, WebSocket, gRPC |
| ARPA Lombardiya / Piedmont / Veneto | Ücretsiz (bölgesel açık veriler) | Dakik istasyonlar (İtalya'da ağ ~1500 istasyon) | Hayır (yalnızca gözlemlendi) | Tüm tarımsal hava durumu değişkenleri | Tam arşiv (on yıllar) | WMS, REST, CSV/JSON indirme |
| Meteoam / CFS/ECMWF | ECMWF ücretsiz açık veriler; Ücretsiz NOAA CFS | 9-25 km | 10-45 gün | Hava durumuna dayalı değişkenler | 1940'tan günümüze ERA5 aracılığıyla | ECMWF API'si (Python), CDS API'si |
| Görsel Geçiş | Ücretsiz 1000 kayıt/gün; 35$/ay standart | İstasyonlardan enterpolasyonlu | 15 gün | ET0, Yetiştirme Derecesi Günleri, don riski | Sınırsız ücretli | REST JSON, CSV, SQL benzeri |
Üretimdeki AgriTech uygulamaları için tavsiye, Açık Hava nasıl temel (ücretsiz, açık kaynak, tarihçiler için ERA5 verileri, 16 günlük tahmin) Yerel kalibrasyon için ARPA istasyon ağları. Open-Meteo tarımsal meteorolojik değişkenleri içerir ET0 (referans buharlaşma-terleme), birden fazla derinlikteki toprak nemi ve buhar gibi kritik konular ticari sağlayıcılardan her zaman ücretsiz olarak temin edilemeyen sulu.
Önbellek ve Yeniden Deneme ile Open-Meteo İstemcisi uygulaması
import openmeteo_requests
import requests_cache
import pandas as pd
from retry_requests import retry
from dataclasses import dataclass
from typing import Optional
@dataclass
class WeatherForecast:
"""Dati meteo giornalieri per decisioni agronomiche."""
date: pd.DatetimeIndex
temperature_max: np.ndarray # Celsius
temperature_min: np.ndarray # Celsius
precipitation: np.ndarray # mm
et0: np.ndarray # mm/giorno - evapotraspirazione di riferimento (FAO-56)
wind_speed_max: np.ndarray # km/h
solar_radiation: np.ndarray # MJ/m2/giorno
soil_moisture_0_7cm: np.ndarray # m3/m3 (0-7 cm profondità)
soil_moisture_7_28cm: np.ndarray # m3/m3 (7-28 cm profondità)
soil_temp_0_7cm: np.ndarray # Celsius
def get_weather_forecast(
latitude: float,
longitude: float,
start_date: Optional[str] = None, # "YYYY-MM-DD" per archivio
end_date: Optional[str] = None,
forecast_days: int = 16,
) -> WeatherForecast:
"""
Recupera dati meteo da Open-Meteo con cache locale (1 ora) e retry automatico.
Combina forecast (16 giorni) con archivio storico ERA5 se start_date specificato.
Args:
latitude: Latitudine WGS84
longitude: Longitudine WGS84
start_date: Se fornito, richiede dati storici (non forecast)
end_date: Fine periodo storico
forecast_days: Giorni di forecast (1-16)
Returns:
WeatherForecast con tutti i parametri agrometeorologici
"""
# Cache HTTP locale (1 ora di validita) + retry su errori transitori
cache_session = requests_cache.CachedSession(".cache/open_meteo", expire_after=3600)
retry_session = retry(cache_session, retries=5, backoff_factor=0.2)
client = openmeteo_requests.Client(session=retry_session)
base_url = (
"https://archive-api.open-meteo.com/v1/archive"
if start_date
else "https://api.open-meteo.com/v1/forecast"
)
params = {
"latitude": latitude,
"longitude": longitude,
"daily": [
"temperature_2m_max",
"temperature_2m_min",
"precipitation_sum",
"et0_fao_evapotranspiration",
"wind_speed_10m_max",
"shortwave_radiation_sum",
"soil_moisture_0_to_7cm",
"soil_moisture_7_to_28cm",
"soil_temperature_0_to_7cm",
],
"timezone": "Europe/Rome", # Fuso orario italiano
"wind_speed_unit": "kmh",
}
if start_date:
params["start_date"] = start_date
params["end_date"] = end_date
else:
params["forecast_days"] = forecast_days
# Open-Meteo usa ERA5 per archivio storico (1940-oggi), eccellente per agricoltura
if start_date:
# Archivio ERA5-Land: alta risoluzione 9 km per Italia
params["models"] = "era5_land"
responses = client.weather_api(base_url, params=params)
r = responses[0] # primo (e unico) punto
daily = r.Daily()
return WeatherForecast(
date=pd.date_range(
start=pd.to_datetime(daily.Time(), unit="s", utc=True),
end=pd.to_datetime(daily.TimeEnd(), unit="s", utc=True),
freq=pd.Timedelta(seconds=daily.Interval()),
inclusive="left",
),
temperature_max = daily.Variables(0).ValuesAsNumpy(),
temperature_min = daily.Variables(1).ValuesAsNumpy(),
precipitation = daily.Variables(2).ValuesAsNumpy(),
et0 = daily.Variables(3).ValuesAsNumpy(),
wind_speed_max = daily.Variables(4).ValuesAsNumpy(),
solar_radiation = daily.Variables(5).ValuesAsNumpy(),
soil_moisture_0_7cm = daily.Variables(6).ValuesAsNumpy(),
soil_moisture_7_28cm = daily.Variables(7).ValuesAsNumpy(),
soil_temp_0_7cm = daily.Variables(8).ValuesAsNumpy(),
)
# Esempio: meteo vigna Manduria (campagna 2024)
lat_manduria, lon_manduria = 40.38, 17.47
weather_storico = get_weather_forecast(
latitude=lat_manduria,
longitude=lon_manduria,
start_date="2024-04-01",
end_date="2024-09-30",
)
weather_forecast = get_weather_forecast(
latitude=lat_manduria,
longitude=lon_manduria,
forecast_days=16,
)
df_meteo = pd.DataFrame({
"data": weather_storico.date,
"t_max_C": weather_storico.temperature_max,
"t_min_C": weather_storico.temperature_min,
"pioggia_mm": weather_storico.precipitation,
"et0_mm": weather_storico.et0,
"rad_MJ": weather_storico.solar_radiation,
"sw_0_7cm": weather_storico.soil_moisture_0_7cm,
"sw_7_28cm": weather_storico.soil_moisture_7_28cm,
})
print(df_meteo.describe())
print(f"\nET0 totale campagna apr-set 2024: {df_meteo['et0_mm'].sum():.1f} mm")
print(f"Pioggia totale campagna apr-set 2024: {df_meteo['pioggia_mm'].sum():.1f} mm")
deficit_idrico = df_meteo["et0_mm"].sum() - df_meteo["pioggia_mm"].sum()
print(f"Deficit idrico stagionale (ET0 - pioggia): {deficit_idrico:.1f} mm")
Eksiksiz Jeo-uzaysal Boru Hattı: GeoPandas, rasterio ve PostGIS
Profesyonel uydu veri yönetimi kapsamlı bir coğrafi altyapı gerektirir Raster verilerini (NDVI uydu görüntüleri, hava durumu haritaları) vektör verileriyle (sınırlar) birleştiren parseller, yönetim alanları, numune alma noktaları). Burada tanımladığımız boru hattı, Jeouzaysal için fiili standart Python yığını: rasteryo rasterler için, GeoPandalar vektörler için, PostGIS uzaysal veritabanı olarak e GDAL temel format çeviri motoru olarak.
Jeo-uzaysal Boru Hattı Mimarisi
AgriTech Jeo-uzaysal Boru Hattı Teknoloji Yığını
┌─────────────────────────────────────────────────────────────────────────┐
│ DATI DI INPUT │
│ [Sentinel-2 GeoTIFF] [Shapefile Appezzamenti] [CSV Meteo/IoT] │
│ │ │ │ │
│ rasterio geopandas pandas │
└─────────┼──────────────────────┼───────────────────────┼────────────────┘
│ │ │
┌─────────▼──────────────────────▼───────────────────────▼────────────────┐
│ PROCESSING LAYER (Python) │
│ [NDVI Calculation] [Zonal Statistics] [Spatial Join] │
│ numpy/rasterio rasterstats geopandas │
│ │ │ │ │
└─────────┼──────────────────────┼───────────────────────┼────────────────┘
│ │ │
└──────────────────────▼───────────────────────┘
│
┌────────────────────────────────▼────────────────────────────────────────┐
│ STORAGE LAYER (PostGIS + S3) │
│ [PostGIS - geometrie + attributi] [S3/MinIO - GeoTIFF raster] │
│ - Tabella parcelle con NDVI medio - File raster originali │
│ - Serie storica per parcella - Archivio scene satellite │
│ - Spatial index GIST - Formato Cloud Optimized GeoTIFF │
└────────────────────────────────┬────────────────────────────────────────┘
│
┌────────────────────────────────▼────────────────────────────────────────┐
│ SERVING LAYER │
│ [REST API - FastAPI] [Dashboard - Grafana] [ML Models] │
│ NDVI per appezzamento Mappe NDVI tempo reale Predizione stress │
└─────────────────────────────────────────────────────────────────────────┘
Bölgesel İstatistikler: Rasterstatlarla Parsel başına ortalama NDVI
import geopandas as gpd
from rasterstats import zonal_stats
import rasterio
from sqlalchemy import create_engine, text
from geoalchemy2 import Geometry
import pandas as pd
# Carica shapefile appezzamenti vigna
gdf_parcelle = gpd.read_file("dati/parcelle_vigna_manduria.shp")
gdf_parcelle = gdf_parcelle.to_crs("EPSG:4326") # Riproietta in WGS84
print(f"Appezzamenti caricati: {len(gdf_parcelle)}")
print(gdf_parcelle[["id_parcella", "varieta", "ettari", "anno_impianto"]].head(10))
def compute_zonal_ndvi(
ndvi_raster_path: str,
parcelle_gdf: gpd.GeoDataFrame,
acquisition_date: str,
) -> gpd.GeoDataFrame:
"""
Calcola statistiche NDVI zonali per ogni appezzamento.
Per ogni parcella calcola:
- NDVI medio (indicatore di vigor generale)
- NDVI mediano (robusto agli outlier)
- NDVI std (indica variabilità intra-parcella)
- Percentile 10 (zone a stress all'interno della parcella)
- Percentile 90 (zone ottimali)
- Percentuale pixel validi (esclude NaN da nuvole)
Returns:
GeoDataFrame con colonne NDVI aggiunte
"""
stats = zonal_stats(
vectors=parcelle_gdf.geometry,
raster=ndvi_raster_path,
stats=["mean", "median", "std", "percentile_10", "percentile_90", "count", "nodata"],
nodata=np.nan,
geojson_out=False,
)
df_stats = pd.DataFrame(stats)
df_stats.columns = [
"ndvi_mean", "ndvi_median", "ndvi_std",
"ndvi_p10", "ndvi_p90", "pixel_count", "pixel_nodata",
]
df_stats["acquisition_date"] = pd.to_datetime(acquisition_date)
df_stats["pixel_valid_pct"] = (
df_stats["pixel_count"] /
(df_stats["pixel_count"] + df_stats["pixel_nodata"].fillna(0)) * 100
)
# Classificazione stress basata su NDVI medio (soglie per vite in estate)
df_stats["stress_category"] = pd.cut(
df_stats["ndvi_mean"],
bins=[-1.0, 0.25, 0.40, 0.55, 0.70, 1.0],
labels=["Stress Severo", "Stress Moderato", "Normale", "Buono", "Ottimale"],
)
return gpd.GeoDataFrame(
pd.concat([parcelle_gdf.reset_index(drop=True), df_stats], axis=1),
geometry="geometry",
crs=parcelle_gdf.crs,
)
# Calcola NDVI zonale per tutte le parcelle
gdf_ndvi = compute_zonal_ndvi(
ndvi_raster_path="ndvi_manduria_20240717.tif",
parcelle_gdf=gdf_parcelle,
acquisition_date="2024-07-17",
)
# Parcelle in stress - priorità irrigazione
parcelle_in_stress = gdf_ndvi[
gdf_ndvi["stress_category"].isin(["Stress Severo", "Stress Moderato"])
]
print(f"\nParcelle in stress ({len(parcelle_in_stress)}/{len(gdf_ndvi)}):")
print(parcelle_in_stress[["id_parcella", "varieta", "ndvi_mean", "stress_category", "ettari"]])
# Salva in PostGIS per persistenza e query spaziali
engine = create_engine("postgresql://agritech:pass@localhost:5432/vigna_db")
gdf_ndvi.to_postgis(
name="ndvi_storico",
con=engine,
if_exists="append",
index=False,
dtype={"geometry": Geometry("MULTIPOLYGON", srid=4326)},
)
print("\nDati NDVI salvati in PostGIS (tabella: ndvi_storico)")
Gelişmiş Mekansal Analiz için PostGIS Sorguları
-- Schema PostGIS per sistema AgriTech
CREATE TABLE IF NOT EXISTS parcelle (
id_parcella SERIAL PRIMARY KEY,
codice_catasto VARCHAR(20) UNIQUE NOT NULL, -- codice catastale italiano
varieta VARCHAR(50),
ettari NUMERIC(8, 4),
anno_impianto INTEGER,
sistema_allevamento VARCHAR(30),
geom GEOMETRY(MULTIPOLYGON, 4326)
);
CREATE INDEX idx_parcelle_geom ON parcelle USING GIST(geom);
CREATE TABLE IF NOT EXISTS ndvi_storico (
id SERIAL PRIMARY KEY,
id_parcella INTEGER REFERENCES parcelle(id_parcella),
data_satellite DATE NOT NULL,
ndvi_mean NUMERIC(6, 4),
ndvi_median NUMERIC(6, 4),
ndvi_std NUMERIC(6, 4),
ndvi_p10 NUMERIC(6, 4),
ndvi_p90 NUMERIC(6, 4),
pixel_valid_pct NUMERIC(5, 2),
stress_category VARCHAR(20),
satellite VARCHAR(20) DEFAULT 'Sentinel-2',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_ndvi_parcella_data ON ndvi_storico(id_parcella, data_satellite);
-- Query: tendenza NDVI ultime 4 acquisizioni per parcella
SELECT
p.codice_catasto,
p.varieta,
n.data_satellite,
n.ndvi_mean,
LAG(n.ndvi_mean, 1) OVER (PARTITION BY n.id_parcella ORDER BY n.data_satellite) AS ndvi_prev,
n.ndvi_mean - LAG(n.ndvi_mean, 1) OVER (PARTITION BY n.id_parcella ORDER BY n.data_satellite) AS ndvi_delta
FROM parcelle p
JOIN ndvi_storico n ON p.id_parcella = n.id_parcella
WHERE n.data_satellite >= NOW() - INTERVAL '60 days'
ORDER BY p.id_parcella, n.data_satellite;
-- Query: parcelle con NDVI in calo > 0.1 nell'ultimo mese (alert stress)
SELECT
p.codice_catasto,
p.varieta,
p.ettari,
latest.ndvi_mean AS ndvi_corrente,
prev.ndvi_mean AS ndvi_30gg_fa,
(latest.ndvi_mean - prev.ndvi_mean) AS variazione
FROM parcelle p
JOIN ndvi_storico latest ON p.id_parcella = latest.id_parcella
AND latest.data_satellite = (SELECT MAX(data_satellite) FROM ndvi_storico WHERE id_parcella = p.id_parcella)
JOIN ndvi_storico prev ON p.id_parcella = prev.id_parcella
AND prev.data_satellite = (SELECT MAX(data_satellite) FROM ndvi_storico
WHERE id_parcella = p.id_parcella AND data_satellite < NOW() - INTERVAL '25 days')
WHERE (latest.ndvi_mean - prev.ndvi_mean) < -0.10
ORDER BY variazione ASC;
ML ile Su Stresi Tahmin Modeli
Uydu NDVI'yı meteorolojik ve IoT verileriyle birleştirmek, tahmine dayalı modeller oluşturmanıza olanak tanır Su stresinin 3-7 gün önceden giderilmesi, proaktif sulama planlamasına olanak sağlar. Burada açıklanan model üç veri kaynağını birleştirir: Sentinel-2'den NDVI zaman serisi, ET0 ve yağıştan hesaplanan su dengesi ve IoT sensörlerinden (veya Fiziksel sensörler mevcut değilse Open-Meteo ERA5-Land).
Model İçin Özellik Mühendisliği
import pandas as pd
import numpy as np
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import classification_report
import joblib
from typing import Tuple
def build_stress_features(
df_ndvi: pd.DataFrame, # colonne: data, ndvi_mean, ndvi_std, ndvi_p10
df_meteo: pd.DataFrame, # colonne: data, t_max_C, t_min_C, pioggia_mm, et0_mm, sw_0_7cm
lookback_days: int = 14,
) -> pd.DataFrame:
"""
Costruisce feature set per modello predittivo stress idrico.
Combina NDVI satellite con bilancio idrico e meteo.
Features generate:
- NDVI corrente e variazione a 5/10 giorni
- Deficit idrico cumulato (ET0 - pioggia) a 7/14/21 giorni
- Growing Degree Days (GDD) da inizio stagione
- Soil moisture media e tendenza
- Variabilità termica (t_max - t_min)
"""
df = df_meteo.merge(df_ndvi, on="data", how="left").sort_values("data")
df["ndvi_mean"] = df["ndvi_mean"].interpolate(method="linear", limit=5)
# Bilancio idrico cumulato (mm) - positivo = surplus, negativo = deficit
df["bilancio_idrico"] = df["pioggia_mm"] - df["et0_mm"]
df["deficit_7gg"] = df["bilancio_idrico"].rolling(7).sum()
df["deficit_14gg"] = df["bilancio_idrico"].rolling(14).sum()
df["deficit_21gg"] = df["bilancio_idrico"].rolling(21).sum()
# NDVI trend (variazione puntuale e su finestra)
df["ndvi_delta_5gg"] = df["ndvi_mean"] - df["ndvi_mean"].shift(5)
df["ndvi_delta_10gg"] = df["ndvi_mean"] - df["ndvi_mean"].shift(10)
# Growing Degree Days (base 10 gradi - standard per vite)
df["t_media"] = (df["t_max_C"] + df["t_min_C"]) / 2
df["gdd_giornaliero"] = np.maximum(df["t_media"] - 10, 0)
df["gdd_cumulato"] = df["gdd_giornaliero"].cumsum()
# Variabilità termica (indicatore stress termico)
df["escursione_termica"] = df["t_max_C"] - df["t_min_C"]
# Soil moisture media e trend
df["sm_media_7gg"] = df["sw_0_7cm"].rolling(7).mean()
df["sm_trend"] = df["sw_0_7cm"] - df["sw_0_7cm"].shift(3)
feature_cols = [
"ndvi_mean", "ndvi_std", "ndvi_p10",
"ndvi_delta_5gg", "ndvi_delta_10gg",
"deficit_7gg", "deficit_14gg", "deficit_21gg",
"et0_mm", "pioggia_mm",
"t_max_C", "t_min_C", "escursione_termica",
"gdd_cumulato", "gdd_giornaliero",
"sm_media_7gg", "sm_trend",
]
return df[["data"] + feature_cols].dropna()
def train_stress_model(
features_df: pd.DataFrame,
labels: pd.Series, # 0=no stress, 1=stress moderato, 2=stress severo
) -> Tuple[Pipeline, dict]:
"""
Addestra modello GradientBoosting per classificazione stress idrico.
Usa pipeline sklearn con StandardScaler integrato.
Returns:
(pipeline_addestrata, metriche_validazione)
"""
feature_cols = [c for c in features_df.columns if c != "data"]
X = features_df[feature_cols].values
y = labels.values
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
pipeline = Pipeline([
("scaler", StandardScaler()),
("clf", GradientBoostingClassifier(
n_estimators=200,
learning_rate=0.05,
max_depth=4,
subsample=0.8,
random_state=42,
)),
])
pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)
cv_scores = cross_val_score(pipeline, X, y, cv=5, scoring="f1_weighted")
metriche = {
"f1_cv_mean": cv_scores.mean(),
"f1_cv_std": cv_scores.std(),
"classification_report": classification_report(
y_test, y_pred,
target_names=["No Stress", "Stress Moderato", "Stress Severo"],
),
}
return pipeline, metriche
# Salva modello per deployment
def save_model(pipeline: Pipeline, path: str) -> None:
joblib.dump(pipeline, path)
print(f"Modello salvato: {path}")
def predict_stress_7days(
pipeline: Pipeline,
features_forecast: pd.DataFrame, # features calcolate su dati forecast 7 giorni
) -> pd.DataFrame:
"""
Predici stress idrico per i prossimi 7 giorni.
Usa le features calcolate su dati forecast meteo (Open-Meteo, 16 gg).
Returns:
DataFrame con data, probabilità per classe, classe predetta
"""
feature_cols = [c for c in features_forecast.columns if c != "data"]
X_forecast = features_forecast[feature_cols].values
proba = pipeline.predict_proba(X_forecast)
pred_class = pipeline.predict(X_forecast)
labels = ["no_stress", "stress_moderato", "stress_severo"]
result_df = pd.DataFrame(proba, columns=[f"prob_{l}" for l in labels])
result_df["classe_predetta"] = pred_class
result_df["data"] = features_forecast["data"].values
result_df["label_predetto"] = [labels[c] for c in pred_class]
return result_df[["data", "label_predetto", "prob_no_stress",
"prob_stress_moderato", "prob_stress_severo"]]
Vaka Çalışması: Manduria'daki Primitivo bağında NDVI izleme
Açıklanan kavramları somut hale getirmek için bir uygulamadan ilham alan bir vaka çalışması sunuyoruz. DOC Primitivo di Manduria (Taranto, Puglia) topraklarında gerçek. Bölgenin şartları var Akdeniz bağcılığına özgü: sıcak ve kurak yazlar (yaz yağışları < 50 mm), killi-kireçli topraklar, kontra espalier ve fidan yetiştirme sistemleri.
Vaka Çalışması parametreleri - Vigna Primitivo Manduria
| Parametre | Değer |
|---|---|
| Konum | Manduria (TA), Puglia - enlem 40.38, boylam 17.47 |
| Toplam yüzey alanı | 12 parsele bölünmüş 35 hektar |
| Ana çeşitler | Primitivo (%80), Negroamaro (%20) |
| Yetiştirme sistemi | Apulian fidanı (8 adet), Counter-espalier (4 adet) |
| Bitki yılı | 2003-2015 (karma genç/yetişkin bağ) |
| Sulama | Damla damla (tüm parseller değil) |
| Uydu izleme | Sentinel-2A/B, L2A, 10m çözünürlük |
| Kullanılan satın alma sıklığı | 2024'te kırsal alanda 28 sahne (bulut < %20) |
| Entegre IoT sensörleri | 6 meteoroloji istasyonu, 30/60 cm'de 18 toprak nem sensörü |
| Analiz dönemi | Nisan - Ekim 2024 |
Mevsimsel NDVI Sonuçları ve Sulama Kararları
Mevsimsel NDVI analizi önemli ölçüde farklılaşmış su stresi modellerini ortaya çıkardı sulama yönetimi ve ürün kalitesi üzerinde doğrudan etkileri olan parseller arasında:
Grafiğe Göre NDVI Sonuçları - 2024 Kampanyası
| Komplo | Çeşitlilik | NDVI Nis | NDVI aşağı | NDVI Temmuz (zirve) | NDVI Ağu | NDVI seti | Sulama Uyarısı |
|---|---|---|---|---|---|---|---|
| Park A1 | İlkel/fidan | 0,28 | 0,42 | 0,58 | 0.41 | 0.31 | Orta derecede iğne gerilimi |
| Park A2 | İlkel/fidan | 0,22 | 0,36 | 0,47 | 0,29 | 0,24 | Şiddetli iğne seti gerilimi |
| Park B1 | İlkel/karşı palier | 0.31 | 0,53 | 0,66 | 0,57 | 0,44 | Uyarı yok |
| Park B2 | İlkel/karşı palier | 0,29 | 0,49 | 0,63 | 0,52 | 0.40 | Uyarı yok |
| Park C1 | Negroamaro/ağaç | 0,25 | 0,44 | 0,55 | 0,38 | 0,28 | Orta derecede iğne gerilimi |
| Park C2 | Negroamaro/counterspalliera | 0.30 | 0,51 | 0.61 | 0,50 | 0,38 | Uyarı yok |
Ortaya çıkan model açık ve tarımsal açıdan önemli: fidan arazileri acil sulama, ağustos-eylül aylarında daha belirgin su stresi göstermektedir. damlama sistemli karşı espalier arazileri. Özellikle A2 parselinde Temmuz ve Ağustos 2024 arasında NDVI'nın 0,47'den 0,29'a düştüğünü gösterdi; yağışsız 23 gün ve maksimum sıcaklıklar 36 derecenin üzerinde.
Otomatik Uyarı Komut Dosyası
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from typing import List
def check_ndvi_alerts(
gdf_ndvi: gpd.GeoDataFrame,
stress_threshold: float = 0.35,
min_valid_pixels_pct: float = 70.0,
) -> List[dict]:
"""
Controlla le parcelle con NDVI sotto soglia di stress.
Genera lista di alert per notifica agronomo.
Args:
gdf_ndvi: GeoDataFrame con colonna ndvi_mean e pixel_valid_pct
stress_threshold: Soglia NDVI sotto cui scattare alert (default 0.35 per vite)
min_valid_pixels_pct: Percentuale minima pixel validi per affidabilità
Returns:
Lista di dict con dettagli alert per parcella
"""
alerts = []
for _, row in gdf_ndvi.iterrows():
if row["pixel_valid_pct"] < min_valid_pixels_pct:
continue # Troppi pixel nuvolosi, dato inaffidabile
if row["ndvi_mean"] < stress_threshold:
severity = "SEVERO" if row["ndvi_mean"] < 0.25 else "MODERATO"
alerts.append({
"id_parcella": row["id_parcella"],
"varieta": row.get("varieta", "N/A"),
"ettari": row.get("ettari", 0),
"ndvi_corrente": round(row["ndvi_mean"], 3),
"ndvi_soglia": stress_threshold,
"severity": severity,
"data": row.get("acquisition_date", "N/A"),
"pixel_valid": round(row["pixel_valid_pct"], 1),
})
return sorted(alerts, key=lambda x: x["ndvi_corrente"])
def send_alert_email(
alerts: List[dict],
recipient: str,
sender: str,
smtp_host: str = "smtp.gmail.com",
smtp_port: int = 587,
) -> None:
"""Invia email di alert NDVI all'agronomo."""
if not alerts:
return
body_lines = [
f"Alert Monitoraggio NDVI Satellite - {alerts[0]['data']}\n",
f"Parcelle in stress idrico: {len(alerts)}\n",
"=" * 60,
"",
]
for a in alerts:
body_lines.append(
f" [{a['severity']}] Parcella {a['id_parcella']} ({a['varieta']}, {a['ettari']} ha)\n"
f" NDVI: {a['ndvi_corrente']} (soglia: {a['ndvi_soglia']}) | Pixel validi: {a['pixel_valid']}%\n"
)
msg = MIMEMultipart()
msg["Subject"] = f"[ALERT NDVI] {len(alerts)} parcelle in stress - {alerts[0]['data']}"
msg["From"] = sender
msg["To"] = recipient
msg.attach(MIMEText("\n".join(body_lines), "plain"))
smtp_password = os.getenv("SMTP_PASSWORD")
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
server.login(sender, smtp_password)
server.send_message(msg)
print(f"Alert inviato a {recipient} per {len(alerts)} parcelle in stress")
Uydu İzleme Sisteminin Yatırım Getirisi
Uydu izlemenin Yatırım Getirisinin sayısallaştırılması, bağcılık şirketlerine yapılan teknolojik yatırımı haklı çıkarmak. Manduria'nın bağı için, 2024 kampanyasının maliyet-fayda analizi şunu gösteriyor:
ROI Analizi - Uydu İzleme Sistemi Vigna Manduria (35 ha, 2024 kampanya)
| Ses | Değer | Notlar |
|---|---|---|
| MALİYETLER | ||
| Sentinel-2 Veri Erişimi (CDSE) | 0 Avro | Ücretsiz açık veriler |
| Açık Hava Hava Durumu API'sı | 0 Avro | Ticari olmayan ücretsiz |
| Python boru hattı geliştirme | 2.500 Avro | 30 geliştirici saati x 83 EUR/saat (tek seferlik, 3 yılda amorti edilir) |
| Bulut barındırma (VM + depolama) | 600 Avro/yıl | VM 4 vCPU + 50 GB S3 depolama |
| Entegrasyon tarımsal danışmanlık | 1.000 Avro | Tek seferlik, mahsule özel eşik kalibrasyonu |
| 1. yılın toplam maliyeti | 4.100 Avro | Sonraki yıllar: ~1.400 EUR/yıl |
| TAHMİNİ FAYDALAR (35 hektar, 100.000 şişe/yıl) | ||
| Sulamanın azaltılması (zamanlama optimizasyonu) | 2.800 Avro | %15 su tasarrufu, 8.500 m3 x 0,33 EUR/m3 |
| Bitki sağlığı tedavilerinin azaltılması | 1.750 Avro | Yalnızca riskli bölgelerde hedefe yönelik müdahale (-%20 tedavi) |
| Üzüm kalitesinin iyileştirilmesi | 8.500 Avro | 12 ha'da +0,5 puan ortalama Brix derecesi, +0,20 EUR/kg kalite primi |
| Su stresinden kaynaklanan kayıpların azaltılması | 3.200 Avro | Beklenen şiddetli streste 2 parselde %8 verim iyileşmesi |
| Toplam faydalar yıl 1 | 16.250 Avro | |
| 1. Yıl Yatırım Getirisi | %296 | Yaklaşık 3 ayda geri ödeme |
Google Earth Engine: Büyük Ölçekli Analiz için Bulut İşleme
Bölgesel veya ulusal ölçekte analiz için (örn. DOC ilçesi, ilkbahar donlarından kaynaklanan zararın il ölçeğinde değerlendirilmesi, izlenmesi AGEA gibi ödeme kuruluşları için CAP'nin bir parçası olan Google Earth Engine (GEE), bilgi işlem kapasitesi sunar aksi takdirde özel HPC altyapıları gerektirecek bulutta yerel.
import ee
# Autenticazione Google Earth Engine (richiede account GEE)
ee.Authenticate()
ee.Initialize(project="your-gee-project-id")
def calculate_ndvi_gee(
aoi_geojson: dict,
start_date: str,
end_date: str,
cloud_threshold: int = 20,
) -> ee.ImageCollection:
"""
Calcola NDVI su larga scala usando Google Earth Engine.
Adatto per analisi su comprensori DOC o scala regionale.
Args:
aoi_geojson: GeoJSON dell'area di interesse
start_date: "YYYY-MM-DD"
end_date: "YYYY-MM-DD"
cloud_threshold: Percentuale max copertura nuvolosa (0-100)
Returns:
ImageCollection con NDVI calcolato per ogni scena
"""
aoi = ee.Geometry(aoi_geojson)
def mask_s2_clouds(image: ee.Image) -> ee.Image:
"""Maschera nuvole usando QA60 band di Sentinel-2."""
qa = image.select("QA60")
cloud_bit_mask = 1 << 10 # bit 10: nuvole opache
cirrus_bit_mask = 1 << 11 # bit 11: nuvole cirro
mask = qa.bitwiseAnd(cloud_bit_mask).eq(0).And(
qa.bitwiseAnd(cirrus_bit_mask).eq(0))
return image.updateMask(mask).divide(10000) # scala a [0,1]
def add_ndvi(image: ee.Image) -> ee.Image:
"""Aggiunge banda NDVI all'immagine Sentinel-2."""
ndvi = image.normalizedDifference(["B8", "B4"]).rename("NDVI")
return image.addBands(ndvi)
collection = (
ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
.filterBounds(aoi)
.filterDate(start_date, end_date)
.filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", cloud_threshold))
.map(mask_s2_clouds)
.map(add_ndvi)
)
return collection
# Calcola NDVI mediano per stagione (mosaico cloud-free)
def get_seasonal_ndvi_mosaic(
collection: ee.ImageCollection,
aoi: ee.Geometry,
) -> ee.Image:
"""Crea mosaico NDVI stagionale (mediano = robusto a outlier residui)."""
ndvi_mosaic = collection.select("NDVI").median().clip(aoi)
return ndvi_mosaic
# Esempio: Comprensorio DOC Primitivo di Manduria (~21.000 ha)
aoi_doc_manduria = {
"type": "Polygon",
"coordinates": [[
[17.2, 40.2], [17.8, 40.2], [17.8, 40.6],
[17.2, 40.6], [17.2, 40.2],
]],
}
collection_estate = calculate_ndvi_gee(
aoi_geojson=aoi_doc_manduria,
start_date="2024-07-01",
end_date="2024-08-31",
cloud_threshold=15,
)
print(f"Scene disponibili: {collection_estate.size().getInfo()}")
# Export NDVI mosaic su Google Drive (per analisi successive in Python locale)
ndvi_mosaic = get_seasonal_ndvi_mosaic(collection_estate, ee.Geometry(aoi_doc_manduria))
task = ee.batch.Export.image.toDrive(
image=ndvi_mosaic,
description="NDVI_DOC_Manduria_Estate2024",
folder="AgriTech_Export",
fileNamePrefix="ndvi_manduria_estate2024",
region=ee.Geometry(aoi_doc_manduria),
scale=10,
crs="EPSG:4326",
maxPixels=1e10,
)
task.start()
print(f"Export avviato - ID task: {task.id}")
İtalya'da AgriTech için Düzenlemeler ve Açık Veri
İtalya'daki tarımsal açık veri ekosistemi üç ana aktör etrafında yapılandırılmıştır: AGEA (Tarım Ödeme Ajansı), SIAN (Ulusal Tarım Bilgi Sistemi) ve bağlantılı bölgesel portallar. Şubat 2025'te, AGEA, SIAN'ın Ulusal Stratejik Kutup'a geçişini tamamlayarak güncelledi bulut altyapısı ve genişleyen coğrafi veri işleme yetenekleri.
Başlıca İtalyan Tarımsal Açık Veri Kaynakları
| Kaynak | URL'si | Mevcut Veriler | Biçim | Güncelleme |
|---|---|---|---|---|
| SIAN Açık Veri | data.sian.it | Yüksek çözünürlüklü ortofotolar, Arazi Kullanım Haritası (AI tabanlı), Kurumsal Grafik Planları | GeoTIFF, Şekil Dosyası, WFS | Yıllık |
| AGEA EO Hizmetleri | sian.it | Multispektral ortofotolar, X-bant radar görüntüleri, DEM, DSM | GeoTIFF, ECW | Süreli Yayın (kampanya) |
| ISTAT - Tarım İstatistikleri | istat.it/agricoltura | UAA, üretim, şirket yapısı, tarım sayımı | CSV, JSON, SDMX | Yıllık |
| MATTM Ulusal Jeoportal | geoportale.gov.it | Carta Natura, Corine Arazi Örtüsü, IDT, DBT | WMS, WFS, Şekil Dosyası | Değişken |
| ARPA Bölgesel (15 bölge) | arp.[bölge].it | Meteoklimatik istasyon verileri, bölgesel tahmin modelleri | CSV, JSON, NetCDF | Neredeyse gerçek zamanlı |
| Kopernik Kara Hizmeti | arazi.copernicus.eu | CORINE Arazi Örtüsü, Otlak, Su ve Sulaklık, Kent Atlası | GeoTIFF, Şekil Dosyası | Üç yıllık/yıllık |
AgriTech için PNRR Vergi Kredileri Geçişi 5.0
Uydu izleme sistemlerine ve hassas tarıma yapılacak yatırımlar, Geçiş Planı 5.0'ın (Kanun 207/2024 - Bütçe Kanunu 2025) avantajlarından yararlanmaya hak kazanın. 2025'ten itibaren kabul edilen tarım işletmeleri yatırımlar için vergi kredilerine erişebilir maddi ve maddi olmayan sermaye malları 4.0, iş yönetimi yazılımı da dahil olmak üzere yapay zeka bileşenleri ve coğrafi veri analizi.
Tarımsal İşletmeler için Geçiş 5.0 Vergi Kredi Oranları (2025)
| Yatırım aralığı | Enerji Tasarrufu %3-6 | Tasarruf %6-10 | Tasarruf >%10 |
|---|---|---|---|
| 2,5 milyon Euro'ya kadar | %35 | %40 | %45 |
| 2,5 Milyon Euro'dan 10 Milyon Euro'ya | %15 | %20 | %25 |
| 10M'den 50M EUR'ya | 5% | %10 | %15 |
NDVI ve hava durumu tahminlerine dayalı hassas sulama sistemleri uygun olabilir su tüketimindeki azalmayı belgelemeleri halinde "enerji tasarrufu >%6" bandı için (pompalama enerjisi) taban çizgisine kıyasla. Yeminli teknik uzmanlık talebi.
Üretim Mimarisi: En İyi Uygulamalar ve Anti-Kalıplar
Bileşenleri tanımladıktan sonra, en iyi uygulamaları özetlemek önemlidir. üretimde sağlam bir uygulama ve ortak anti-kalıpları tanımlama Kırılgan veya hatalı sistemlere yol açar.
En İyi Uygulamalar - AgriTech Uydu İzleme Sistemi
- Aşamalı bulut filtreleme: Yalnızca ürünün bulut örtüsü yüzdesini (Düzey-1 meta verileri) kullanmayın. Maskelemeyi her zaman QA60 (Sentinel-2) veya BQA (Landsat) ile piksel düzeyinde uygulayın. %5'lik bir bulut ürününün belirli bir planı tamamen gizlenmiş olabilir.
- Bulut İçin Optimize Edilmiş GeoTIFF (COG): S3/MinIO'dan verimli aralık isteği erişimi için NDVI rasterlerini COG formatında saklayın. Bulut depolama için optimize edilmemiş klasik GeoTIFF'lerden kaçının.
- Tutarlı projeksiyon: HER ŞEYİ nihai veriler için EPSG:4326'ya (WGS84) veya doğru metrik ölçümleri sürdürmek amacıyla İtalya için UTM bölgesi 32N'ye (EPSG:32632) göre standartlaştırın. CRS'yi açıkça yeniden yansıtmadan asla karıştırmayın.
- IoT temel gerçeğiyle veri doğrulama: NDVI eşiklerini saha ölçümleriyle (toprak nem sensörleri, taşınabilir LAI ölçer, toplanan tarımsal veriler) kalibre edin. Yerel kalibrasyon olmadan tek başına NDVI %15-30 oranında hatalı pozitif/negatif uyarılar verebilir.
- Zaman arşivi yönetimi: Her grafik için en az 3-5 yıllık NDVI zaman serisini koruyun. NDVI anomalisi, geçmiş ortalamayla (aynı ay, aynı mahsul) karşılaştırıldığında mutlak değerden çok daha bilgilendiricidir.
- Hız sınırlama ve önbelleğe alma API'si: CDSE ve Open-Meteo'nun hız sınırları vardır. Gereksiz yeniden indirme işlemlerini önlemek için her zaman yerel önbellek (dosya tabanlı veya Redis) uygulayın. Sentinel-2'nin indirilmesi 30-120 saniye sürer: aynı verileri iki kez talep etmeyin.
- Günlüğe kaydetme ve denetim izi: Her NDVI hesaplaması, kaynak uydu sahnesine, edinme tarihine, bulut örtüsü yüzdesine, algoritma sürümüne göre izlenebilir olmalıdır. PAC/AGEA denetimleri ve anormalliklerin ayıklanması için temeldir.
Yaygın Anti-Desenler - AgriTech Uydu Sistemleri
- Atmosfer düzeltmesinin göz ardı edilmesi: Seviye-2A (atmosferin altı) yerine Seviye-1C'nin (atmosferin üstü) kullanılması, yüksek atmosferik nem koşullarında (Po Vadisi sisi, Adriyatik Denizi) NDVI'da %10-20 düzeyinde sistematik hatalara neden olur. Kantitatif uygulamalar için HER ZAMAN L2A'yı kullanın.
- Tek gösterge olarak NDVI: EVI (yoğun bitki örtüsü için), NDWI (su stresi), Red Edge NDVI (NDVI'da görünmeden önceki erken stres) göz ardı edilirken yalnızca NDVI'ya güvenin. Profesyonel bir sistem en az 3-4 endeksi bir arada kullanır.
- Piksel boyutu ve parsel: NDVI'nın 0,3 ha'dan küçük ve pikselleri 10 m'de olan alanlara uygulanması istatistiksel olarak güvenilmez ortalamalar üretir (30 pikselden az). Küçük araziler için Planet Labs'ı (3,7m) veya drone sensörlerini düşünün.
- Zamansal birleştirme eksikliği: Tek bir alımın belirli bir tarih için mevcut olması, %19 bulut örtüsünde bile, kısmi bulut alanlarında NDVI kusurlarına yol açar. Her zaman 10-15 günlük bir zaman aralığında medoid birleştirmeyi kullanın.
- Veri sürümlendirmesi olmayan işlem hattı: NDVI'nın farklı Python/rasterio/sentinelhub sürümleriyle yeniden hesaplanması, aynı kaynak veriler için biraz farklı değerler üretir. Bilgi işlem ardışık düzenlerinizi DVC veya eşdeğeri ile sürümlendirin.
Komple Boru Hattı: Prefect ile Orkestrasyon
Açıklanan tüm bileşenlerin entegre edilmesiyle eksiksiz uydu izleme hattı Prefect ile düzenlenebilir (seriye bakın Boru Hattı Düzenlemesi) her 5 günde bir otomatik olarak çalışacak (Sentinel-2 tekrar ziyaret oranında).
from prefect import flow, task
from prefect.schedules import CronSchedule
from datetime import datetime, timedelta
import logging
logger = logging.getLogger(__name__)
@task(retries=3, retry_delay_seconds=60, name="search-sentinel2-products")
def search_products_task(bbox: tuple, lookback_days: int = 7) -> list[dict]:
"""Task Prefect: ricerca prodotti Sentinel-2 recenti."""
end_date = datetime.utcnow()
start_date = end_date - timedelta(days=lookback_days)
products = search_sentinel2_products(
bbox=bbox,
start_date=start_date.strftime("%Y-%m-%dT00:00:00.000Z"),
end_date=end_date.strftime("%Y-%m-%dT23:59:59.000Z"),
cloud_cover_max=20.0,
)
logger.info(f"Trovati {len(products)} prodotti Sentinel-2")
return products
@task(retries=2, retry_delay_seconds=120, name="download-compute-ndvi")
def compute_ndvi_task(
product: dict,
aoi_bbox: tuple,
output_dir: str = "/data/ndvi",
) -> str:
"""Task Prefect: scarica bande e calcola NDVI per un prodotto."""
# Implementazione basata su funzioni definite nelle sezioni precedenti
acquisition_date = product["ContentDate"]["Start"][:10]
output_path = f"{output_dir}/ndvi_{acquisition_date}.tif"
# Download + calcolo (codice omesso per brevita, usa funzioni precedenti)
logger.info(f"NDVI calcolato per {acquisition_date}: {output_path}")
return output_path
@task(name="zonal-stats-postgis")
def zonal_stats_task(ndvi_path: str, parcelle_shapefile: str) -> dict:
"""Task Prefect: calcola statistiche zonali e salva in PostGIS."""
gdf_parcelle = gpd.read_file(parcelle_shapefile).to_crs("EPSG:4326")
acquisition_date = ndvi_path.split("ndvi_")[1].replace(".tif", "")
gdf_ndvi = compute_zonal_ndvi(ndvi_path, gdf_parcelle, acquisition_date)
engine = create_engine(os.getenv("POSTGIS_URL"))
gdf_ndvi.to_postgis("ndvi_storico", engine, if_exists="append", index=False)
alerts = check_ndvi_alerts(gdf_ndvi)
return {"parcelle_processate": len(gdf_ndvi), "alerts": len(alerts), "alert_details": alerts}
@task(name="send-alerts")
def alerts_task(stats: dict) -> None:
"""Task Prefect: invia alert email se presenti."""
if stats["alerts"] > 0:
send_alert_email(
alerts=stats["alert_details"],
recipient=os.getenv("AGRONOMO_EMAIL"),
sender=os.getenv("ALERT_EMAIL"),
)
logger.info(f"Alert inviati per {stats['alerts']} parcelle")
@flow(
name="satellite-monitoring-pipeline",
schedule=CronSchedule(cron="0 8 */5 * *"), # ogni 5 giorni alle 8:00
)
def satellite_monitoring_flow(
bbox: tuple = (17.35, 40.28, 17.60, 40.50),
parcelle_shapefile: str = "/data/parcelle_vigna.shp",
) -> None:
"""
Pipeline completa di monitoraggio satellite per vigna.
Si esegue ogni 5 giorni, allineata alla frequenza Sentinel-2.
"""
products = search_products_task(bbox=bbox)
if not products:
logger.warning("Nessun prodotto trovato nell'ultimo periodo")
return
# Processo il prodotto più recente con minima copertura nuvolosa
best_product = sorted(
products,
key=lambda p: next(
(a["Value"] for a in p.get("Attributes", []) if a["Name"] == "cloudCover"), 100
),
)[0]
ndvi_path = compute_ndvi_task(best_product, bbox)
stats = zonal_stats_task(ndvi_path, parcelle_shapefile)
alerts_task(stats)
logger.info(
f"Pipeline completata: {stats['parcelle_processate']} parcelle, {stats['alerts']} alert"
)
if __name__ == "__main__":
satellite_monitoring_flow()
Sonuçlar ve Sonraki Adımlar
CDSE aracılığıyla ücretsiz olarak erişilebilen Sentinel-2 uydu verileri şunları temsil eder: İtalyan AgriTech'te muhtemelen en az kullanılan veri kaynağı. Python'la, rasterio, sentinelhub ve bir CDSE hesabı oluşturmak mümkündür. Kalite ve ayrıntı düzeyi açısından birçok çözümü geride bırakan NDVI izleme sistemi bir ücret karşılığında ticari.
Başarının anahtarı teknolojik gelişmişlik değil, kalibrasyon yerel tarımsal: NDVI eşikleri, uyarı süresi pencereleri, ağırlıklar Tahmin modelindeki özelliklerin tamamı belirli ürüne göre kalibre edilmelidir, yerel iklim ve şirketin tarımsal uygulamaları hakkında. Kalibre edilmiş bir sistem Puglia'daki asmalarda o olmadan Po Vadisi'ndeki buğdayda aynı derecede iyi çalışmıyor insan kalibrasyon müdahalesi.
Benzer bir sistemi uygulamak isteyenlerin hatırlaması gereken önemli noktalar:
- Her zaman kullan Sentinel-2 Seviye-2A Farklı tarihler arasındaki niceliksel karşılaştırmalar için (atmosferik düzeltme uygulandı).
- NDVI'yı en az bir tamamlayıcı indeksle birleştirin (yoğun üzüm bağları için EVI, erken su stresi için NDWI, klorofil izleme için Red Edge NDVI).
- Entegrasyon Açık Hava ERA5-Land su dengesi için: modelden ET0 + yağış + toprak nemi ve sahada IoT sensörleriniz olmadığında mükemmel bir temsil.
- Birini sakla En az 3 yıllık tarihi dizi her parsel için: tarihsel ortalamayla karşılaştırıldığında anomali, mutlak NDVI değerinden çok daha faydalıdır.
- Şununla ölçeklendir: Google Earth Motoru yalnızca bölgesel veya ulusal ölçekleri işlemeniz gerektiğinde: bireysel şirketler için CDSE'deki yerel Python işlem hattı daha kontrol edilebilir ve bağımlılığa gerek yoktur.
FoodTech Serisi devam ediyor
Uydu boru hattını inşa ettiniz. Şimdi serideki diğer makaleleri inceleyin AgriTech mimarisini tamamlayın:
- Madde 1: Python ve MQTT ile Hassas Tarım için IoT Pipeline - Yer sensörlerinin uydu boru hattına nasıl entegre edileceği.
- Madde 2: Mahsul İzleme için ML Edge: Tarlalarda Bilgisayarlı Görme - Bitki hastalıklarının erken tespiti için bilgisayarlı görme modelleri.
- Madde 4: Gıdada Blockchain izlenebilirliği: Tarladan süpermarkete - NDVI verileri zincir üstü kalite sertifikalarına nasıl güç veriyor?
Bu makalede açıklanan MLOps ve tahmine dayalı modellerin dağıtımı hakkında daha fazla bilgi edinmek için, diziyi gör MLflow ile İşletmeler için MLOps ve seri İşletme Yüksek Lisansı: RAG Enterprise.







