earthdiff 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- earthdiff/__init__.py +28 -0
- earthdiff/__main__.py +3 -0
- earthdiff/aoi.py +161 -0
- earthdiff/cause.py +144 -0
- earthdiff/change.py +337 -0
- earthdiff/cli.py +202 -0
- earthdiff/composite.py +169 -0
- earthdiff/geocode.py +186 -0
- earthdiff/history.py +86 -0
- earthdiff/i18n.py +201 -0
- earthdiff/indices.py +52 -0
- earthdiff/mcp_prompts.py +54 -0
- earthdiff/mcp_server.py +338 -0
- earthdiff/mcp_service.py +887 -0
- earthdiff/periods.py +87 -0
- earthdiff/pipeline.py +476 -0
- earthdiff/reader.py +59 -0
- earthdiff/render.py +146 -0
- earthdiff/report.py +113 -0
- earthdiff/safety.py +93 -0
- earthdiff/seasonal.py +155 -0
- earthdiff/stac.py +163 -0
- earthdiff/static/ui.html +239 -0
- earthdiff/templates/report.html +850 -0
- earthdiff/terrain.py +120 -0
- earthdiff/ui.py +227 -0
- earthdiff/vendor/LEAFLET-LICENSE.txt +26 -0
- earthdiff/vendor/leaflet.css +661 -0
- earthdiff/vendor/leaflet.js +6 -0
- earthdiff/watch.py +138 -0
- earthdiff/xlsx.py +183 -0
- earthdiff/yearly.py +109 -0
- earthdiff-0.2.0.dist-info/METADATA +368 -0
- earthdiff-0.2.0.dist-info/RECORD +37 -0
- earthdiff-0.2.0.dist-info/WHEEL +4 -0
- earthdiff-0.2.0.dist-info/entry_points.txt +3 -0
- earthdiff-0.2.0.dist-info/licenses/LICENSE +21 -0
earthdiff/i18n.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Turkish texts for the HTML report (the report switches between English and Turkish in the browser).
|
|
2
|
+
|
|
3
|
+
Warnings and cause hints are produced in English by the pipeline; this module maps each known sentence to Turkish.
|
|
4
|
+
An unknown sentence falls back to English, and ``tests/test_i18n.py`` checks that every warning the pipeline can
|
|
5
|
+
produce has a translation."""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import html
|
|
9
|
+
import re
|
|
10
|
+
from typing import Callable
|
|
11
|
+
|
|
12
|
+
CLASS_LABELS_TR = {
|
|
13
|
+
"vegetation_loss": "Bitki kaybı",
|
|
14
|
+
"vegetation_gain": "Bitki artışı",
|
|
15
|
+
"new_water": "Yeni su",
|
|
16
|
+
"water_loss": "Su kaybı",
|
|
17
|
+
"new_bare_or_built": "Yeni çıplak toprak / yapılaşma",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
PERIOD_TR = {"before": "önce", "after": "sonra"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def tr_num(s: str) -> str:
|
|
24
|
+
"""1,234.5 -> 1.234,5 (Turkish digit grouping and decimal comma)."""
|
|
25
|
+
return s.replace(",", "\0").replace(".", ",").replace("\0", ".")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
Sub = Callable[[re.Match], str]
|
|
29
|
+
|
|
30
|
+
_WARNINGS: list[tuple[re.Pattern, Sub]] = [
|
|
31
|
+
(re.compile(r"The two windows are (\d+) days apart in the seasonal cycle\. Vegetation and water change naturally with the season: "
|
|
32
|
+
r"compare the same time of year in different years to avoid false alarms\."),
|
|
33
|
+
lambda m: f"İki dönem mevsim döngüsünde {m[1]} gün arayla. Bitki örtüsü ve su mevsimle doğal olarak değişir: "
|
|
34
|
+
"yanlış alarmdan kaçınmak için farklı yılların aynı dönemini karşılaştırın."),
|
|
35
|
+
(re.compile(r"A window shorter than two weeks usually contains very few cloud-free scenes; consider a longer window\."),
|
|
36
|
+
lambda m: "İki haftadan kısa bir dönemde genellikle çok az bulutsuz sahne olur; daha uzun bir dönem seçin."),
|
|
37
|
+
(re.compile(r"Only one usable scene in the (before|after) period: the composite is a single acquisition, so residual cloud/haze or one-off conditions are not averaged out\."),
|
|
38
|
+
lambda m: f"{PERIOD_TR[m[1]].capitalize()} döneminde yalnız bir kullanılabilir sahne var: bileşim tek bir çekimden oluşuyor, "
|
|
39
|
+
"bu yüzden kalan bulut, pus ya da tek seferlik koşullar ortalamayla giderilemiyor."),
|
|
40
|
+
(re.compile(r"No usable scene in the (before|after) period\."),
|
|
41
|
+
lambda m: f"{PERIOD_TR[m[1]].capitalize()} döneminde kullanılabilir sahne yok."),
|
|
42
|
+
(re.compile(r"Only (\d+)% of the AOI is cloud-free in both periods; the remainder is excluded and not reported as 'no change'\."),
|
|
43
|
+
lambda m: f"Alanın yalnız %{m[1]} kadarı iki dönemde de bulutsuz; geri kalanı hesaba katılmadı ve \"değişim yok\" diye raporlanmadı."),
|
|
44
|
+
(re.compile(r"Changes cover (\d+)% of the AOI, and (\d+)% of the changed area is fire-like vegetation loss \(dNBR, an independent burn signal\): "
|
|
45
|
+
r"this is consistent with one large burn rather than a seasonal or atmospheric mismatch\. Inspect the before/after images to confirm\."),
|
|
46
|
+
lambda m: f"Alanın %{m[1]} kadarı değişmiş ve değişen alanın %{m[2]} kadarı yangın izine benzeyen bitki kaybı (dNBR, bağımsız bir yanık sinyali): "
|
|
47
|
+
"bu, mevsim ya da atmosfer farkından çok tek büyük bir yangınla uyumlu. Doğrulamak için önce/sonra görüntülerine bakın."),
|
|
48
|
+
(re.compile(r"Changes cover (\d+)% of the AOI: such a large share often means a seasonal, atmospheric or image-registration mismatch "
|
|
49
|
+
r"rather than real land-cover change\. Inspect the before/after images\."),
|
|
50
|
+
lambda m: f"Alanın %{m[1]} kadarı değişmiş görünüyor: bu kadar büyük bir oran çoğu zaman gerçek arazi değişiminden çok "
|
|
51
|
+
"mevsim, atmosfer ya da görüntü çakıştırma farkı demektir. Önce/sonra görüntülerine bakın."),
|
|
52
|
+
(re.compile(r"(\d+)% of the comparable area is very bright, non-vegetated surface \(salt, sand, snow or similar; (\d+)% in both periods\)\. "
|
|
53
|
+
r"Change detection is NOT reliable on such surfaces: do not read the absence of change there as 'no change'\. "
|
|
54
|
+
r"Confidence of polygons on bright pixels is lowered\."),
|
|
55
|
+
lambda m: f"Karşılaştırılabilir alanın %{m[1]} kadarı çok parlak, bitkisiz yüzey (tuz, kum, kar ya da benzeri; iki dönemde de %{m[2]}). "
|
|
56
|
+
"Bu yüzeylerde değişim tespiti güvenilir DEĞİL: orada değişim görünmemesini \"değişim yok\" diye okumayın. "
|
|
57
|
+
"Parlak piksellerdeki poligonların güven puanı düşürüldü."),
|
|
58
|
+
(re.compile(r"Exploratory signal on the bright surface: the share of water-like pixels \(NDWI > 0\) changed from (\d+)% to (\d+)%, "
|
|
59
|
+
r"which suggests a bright surface \(a salt flat or shallow lake\) became (drier|wetter)\. The default MNDWI rule cannot see this "
|
|
60
|
+
r"because dry salt also scores as water in MNDWI\. Not a mapped change: verify with other data\."),
|
|
61
|
+
lambda m: f"Parlak yüzeyde deneysel sinyal: suya benzeyen piksellerin oranı (NDWI > 0) %{m[1]} → %{m[2]} oldu; bu, parlak bir yüzeyin "
|
|
62
|
+
f"(tuz düzlüğü ya da sığ göl) {'kuruduğunu' if m[3] == 'drier' else 'ıslandığını'} düşündürüyor. Varsayılan MNDWI kuralı bunu göremez, "
|
|
63
|
+
"çünkü kuru tuz da MNDWI'da su gibi görünür. Haritalanmış bir değişim değildir: başka verilerle doğrulayın."),
|
|
64
|
+
(re.compile(r"likely_cause is unavailable: Sentinel-2 band B12 \(SWIR2\) could not be read for these scenes, so vegetation loss is not labelled fire-like / clearing-like\."),
|
|
65
|
+
lambda m: "Neden ipucu yok: bu sahnelerde Sentinel-2 B12 (SWIR2) bandı okunamadı, bu yüzden bitki kaybı yangın ya da açma olarak etiketlenmedi."),
|
|
66
|
+
(re.compile(r"The seasonal check found no usable scenes in earlier years \(windows before 2017 have no data\), so no polygon was checked\."),
|
|
67
|
+
lambda m: "Mevsim kontrolü önceki yıllarda kullanılabilir sahne bulamadı (2017 öncesinde veri yok), bu yüzden hiçbir poligon kontrol edilmedi."),
|
|
68
|
+
(re.compile(r"([\d,.]+) ha \((\d+)% of the vegetation loss, (\d+) polygons\) repeats at the same time of year in earlier years: "
|
|
69
|
+
r"likely_seasonal \(harvest or a seasonal cycle rather than a one-off loss\)\. The polygons are kept and flagged\."),
|
|
70
|
+
lambda m: f"{tr_num(m[1])} ha (bitki kaybının %{m[2]} kadarı, {m[3]} poligon) önceki yıllarda da yılın aynı zamanında tekrarlıyor: "
|
|
71
|
+
"muhtemelen mevsimsel (tek seferlik kayıp değil, hasat ya da mevsim döngüsü). Poligonlar silinmedi, işaretlendi."),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
_BASIS: list[tuple[re.Pattern, Sub]] = [
|
|
75
|
+
(re.compile(r"largest one-step drop between (\S+) and (\S+)"), lambda m: f"en büyük tek adımlık düşüş {m[1]} ile {m[2]} arasında"),
|
|
76
|
+
(re.compile(r"no SWIR2 \(B12\) measurement, so no burn signal could be computed"), lambda m: "SWIR2 (B12) ölçümü yok, yanık sinyali hesaplanamadı"),
|
|
77
|
+
(re.compile(r"dNBR (-?[\d.]+) \(at least moderate-high burn severity by Key & Benson thresholds\)"),
|
|
78
|
+
lambda m: f"dNBR {tr_num(m[1])} (Key & Benson eşiklerine göre en az orta-yüksek yanık şiddeti)"),
|
|
79
|
+
(re.compile(r"dNBR (-?[\d.]+) \(at least moderate-low burn severity\)"), lambda m: f"dNBR {tr_num(m[1])} (en az orta-düşük yanık şiddeti)"),
|
|
80
|
+
(re.compile(r"dNBR (-?[\d.]+) \(below 0\.10: no burn signal\)"), lambda m: f"dNBR {tr_num(m[1])} (0,10'un altında: yanık sinyali yok)"),
|
|
81
|
+
(re.compile(r"dNBR (-?[\d.]+) \(low, 0\.10 to 0\.27: weak burn signal\)"), lambda m: f"dNBR {tr_num(m[1])} (düşük, 0,10-0,27: zayıf yanık sinyali)"),
|
|
82
|
+
(re.compile(r"dark after \(visible reflectance (-?[\d.]+)\), like char or ash"),
|
|
83
|
+
lambda m: f"sonrasında koyu yüzey (görünür yansıma {tr_num(m[1])}), kömür ya da kül gibi"),
|
|
84
|
+
(re.compile(r"bright after \(visible reflectance (-?[\d.]+)\), like exposed soil or stubble"),
|
|
85
|
+
lambda m: f"sonrasında parlak yüzey (görünür yansıma {tr_num(m[1])}), açılmış toprak ya da anız gibi"),
|
|
86
|
+
(re.compile(r"straight-edged outline \(rectangularity (-?[\d.]+)\)"), lambda m: f"düz kenarlı sınır (dikdörtgenlik {tr_num(m[1])})"),
|
|
87
|
+
(re.compile(r"irregular outline \(rectangularity (-?[\d.]+)\)"), lambda m: f"düzensiz sınır (dikdörtgenlik {tr_num(m[1])})"),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _translate(text: str, table: list[tuple[re.Pattern, Sub]]) -> str | None:
|
|
92
|
+
for pat, sub in table:
|
|
93
|
+
m = pat.fullmatch(text)
|
|
94
|
+
if m:
|
|
95
|
+
return sub(m)
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def warning_tr(text: str) -> str:
|
|
100
|
+
"""Turkish version of a pipeline warning; the English text if it is not known."""
|
|
101
|
+
return _translate(text, _WARNINGS) or text
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def has_warning_tr(text: str) -> bool:
|
|
105
|
+
return _translate(text, _WARNINGS) is not None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def basis_tr(text: str) -> str:
|
|
109
|
+
"""Turkish version of a ``likely_cause_basis`` ('; '-joined sentences); unknown parts stay in English."""
|
|
110
|
+
return "; ".join(_translate(part, _BASIS) or part for part in text.split("; ")) if text else text
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def method_html_tr(classes, mmu_ha: float, resolution: float) -> str:
|
|
114
|
+
rows = "".join(
|
|
115
|
+
f"<tr><td><span class='chip' style='background:{c.color}'></span>{html.escape(CLASS_LABELS_TR.get(c.key, c.label))}</td><td><code>{html.escape(c.rule)}</code></td></tr>"
|
|
116
|
+
for c in classes
|
|
117
|
+
)
|
|
118
|
+
mmu = tr_num(f"{mmu_ha:g}")
|
|
119
|
+
res = tr_num(f"{resolution:g}")
|
|
120
|
+
return f"""
|
|
121
|
+
<p>earthdiff, Sentinel-2 L2A yüzey yansımasının iki <b>medyan bileşimini</b> karşılaştırır. Bulut, bulut gölgesi, sirrus, kar ve doymuş pikseller
|
|
122
|
+
sahne sınıflandırma katmanıyla (SCL; bulut pikselleri 30 m genişletilerek) ayıklanır, sonra piksel başına medyan alınır. Böylece her piksel,
|
|
123
|
+
o dönemdeki bulutsuz gözlemlerinin medyanıdır. Aşağıdakilerin hepsi açık ve incelenebilir kurallardır – makine öğrenmesi modeli yoktur.</p>
|
|
124
|
+
<table><thead><tr><th>Değişim sınıfı</th><th>Kural (d = sonra − önce)</th></tr></thead><tbody>{rows}</tbody></table>
|
|
125
|
+
<pre>NDVI = (NIR - Red) / (NIR + Red) B08, B04
|
|
126
|
+
MNDWI = (Green - SWIR1) / (Green + SWIR1) B03, B11
|
|
127
|
+
BSI = ((SWIR1 + Red) - (NIR + Blue)) / ((SWIR1 + Red) + (NIR + Blue)) B11, B04, B08, B02</pre>
|
|
128
|
+
<p>Piksel kurallarının sonucu temizlenir: 3 pikselden dar çıkıntılar morfolojik açma (3x3) ile silinir (20 m ve daha küçük piksellerde),
|
|
129
|
+
<b>en küçük haritalama biriminden ({mmu} ha)</b> küçük lekeler atılır, kalanlar poligona çevrilir (8 komşuluk). Alanlar, UTM ızgarasında
|
|
130
|
+
piksel sayısı × piksel alanıdır ({res} m piksel).</p>
|
|
131
|
+
<p><b>Güç (güven puanı)</b> 0 ile 1 arasında sezgisel bir puandır, doğru olma olasılığı <i>değildir</i>: 0,5×büyüklük (ortalama indis değişiminin eşiği ne kadar aştığı, 2 katında doyar)
|
|
132
|
+
+ 0,3×geçerli gözlem (önce/sonra en küçüğü, 3'te doyar) + 0,2×boyut (alan / birim oranının log10'u, 10 katında doyar). ≥0,70 “güçlü”, ≥0,45 “orta”, altı “zayıf”.</p>
|
|
133
|
+
<h3>Olası neden, mevsim kontrolü, parlak yüzeyler</h3>
|
|
134
|
+
<p><b>Olası neden</b> (yalnız bitki kaybı poligonlarında) bir ipucudur, hüküm değildir: <code>fire-like</code> (yangına benzer) için dNBR en az 0,27 olmalı (Key & Benson 2006'nın orta-düşük yanık şiddeti; Copernicus EFFIS de aynı eşikleri kullanır),
|
|
135
|
+
sonrasında yüzey koyu olmalı ve yangın lehine kanıt (düzensiz sınır gibi) açma lehine kanıttan fazla olmalı. <code>clearing-like</code> (açmaya benzer) zayıf yanık sinyali ya da düz kenarlı, parlak bir sınır demektir (ağaç kesimi, hasat, sürüm, inşaat); kalan her şey <code>unclear</code> (belirsiz).
|
|
136
|
+
Parlaklık ve şekil eşikleri bu projenin kendi sezgisidir. İki dönem yıllarca ayrıysa her bitki kaybı poligonu ara yılların birer geçişiyle de izlenir; bir yıldan ötekine NBR bütün dönemdekinden çok daha fazla düşmüşse (sonradan kısmen toparlanmış bir olay, örneğin eski bir yangın) ipucu o adıma göre verilir ve rapor olayın ne zaman olduğunu yazar. <b>Mevsimsel</b> işareti (isteğe bağlı kontrol), önceki yılların aynı dönemlerinde NDVI'ı benzer ölçüde düşen poligonlara konur; poligonlar hiçbir zaman silinmez.
|
|
137
|
+
<b>Parlak yüzeyler</b> (tuz, kum, kar: ortalama görünür yansıma ≥ 0,25 ve NDVI < 0,15) görüntüyü doyurur; alanın büyük bölümünü kaplıyorsa rapor orada değişimin güvenilir olmadığını söyler ve üstlerindeki poligonların güven puanını düşürür.</p>
|
|
138
|
+
<h3>Sınırlar — lütfen okuyun</h3>
|
|
139
|
+
<ul>
|
|
140
|
+
<li><b>Mevsim ve tarım.</b> Ekin, çayır ve yaprak döken orman mevsime ve tarım işlerine göre renk değiştirir. Farklı yılların aynı mevsimini karşılaştırın; hasat, sürüm, sulama ve otlatma bitki kaybı ya da artışı gibi görünür.</li>
|
|
141
|
+
<li><b>Yanık şiddeti ya da orman hukuku ürünü değildir.</b> “Bitki kaybı” yeşilliğin azaldığını söyler; nedenini söylemez (yangın, kesim, kuraklık, hasat, inşaat). “Yeni çıplak toprak / yapılaşma” toprak nemi, kurumuş ekin ya da açığa çıkmış göl tabanı da olabilir.</li>
|
|
142
|
+
<li><b>Su.</b> Sığ, bulanık ya da yosunlu su ve ıslak toprak MNDWI kuralının iki tarafına da düşebilir; bulut gölgesi ve yamaç gölgesi suyu taklit edebilir.</li>
|
|
143
|
+
<li><b>Çözünürlük.</b> Burada Sentinel-2 pikselleri {res} m (SWIR bantları aslında 20 m). Birkaç pikselden dar şeyler (yollar, küçük binalar, dar dereler) güvenilir biçimde bulunamaz. {mmu} ha'dan küçük her şey gizlenir.</li>
|
|
144
|
+
<li><b>Atmosfer ve geometri.</b> Kalan pus, tarihler arasındaki güneş ve bakış açısı farkı ve sahnelerin çakıştırması (~10 m) kenarlarda yanlış alarm üretir.</li>
|
|
145
|
+
<li><b>Arazi doğrulaması yok.</b> Sonuçlar tarama ipuçlarıdır. Önemli bir bulguyu daha yüksek çözünürlüklü görüntü, arazi verisi ya da resmî kaynakla doğrulayın.</li>
|
|
146
|
+
</ul>"""
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------- plain-language reading of a change (same texts as the report page)
|
|
150
|
+
CLASS_LABELS_EN = {
|
|
151
|
+
"vegetation_loss": "Vegetation loss",
|
|
152
|
+
"vegetation_gain": "Vegetation gain",
|
|
153
|
+
"new_water": "New water",
|
|
154
|
+
"water_loss": "Water loss",
|
|
155
|
+
"new_bare_or_built": "New bare soil / built-up",
|
|
156
|
+
}
|
|
157
|
+
WHAT = {
|
|
158
|
+
"en": {"vegetation_loss": "Green vegetation was lost here",
|
|
159
|
+
"vegetation_gain": "Vegetation grew or became greener here (regrowth, new crops, planting)",
|
|
160
|
+
"new_water": "Water appeared here (flooding, a filling reservoir or a new pond)",
|
|
161
|
+
"water_loss": "Water disappeared here (a drying lake or reservoir, a retreating shore)",
|
|
162
|
+
"new_bare_or_built": "The ground became bare or built-up here (quarry, construction, cleared or ploughed land)"},
|
|
163
|
+
"tr": {"vegetation_loss": "Burada yeşil bitki örtüsü kaybolmuş",
|
|
164
|
+
"vegetation_gain": "Burada bitki örtüsü artmış ya da yeşermiş (yeniden büyüme, yeni ekin, dikim)",
|
|
165
|
+
"new_water": "Burada su ortaya çıkmış (taşkın, dolan baraj gölü ya da yeni gölet)",
|
|
166
|
+
"water_loss": "Burada su çekilmiş (kuruyan göl ya da baraj, geri çekilen kıyı)",
|
|
167
|
+
"new_bare_or_built": "Burada zemin çıplak toprağa ya da yapılaşmaya dönmüş (taş ocağı, inşaat, açılmış ya da sürülmüş arazi)"},
|
|
168
|
+
}
|
|
169
|
+
CAUSE = {
|
|
170
|
+
"en": {"fire-like": "it looks like a burn: the surface is darker and burnt afterwards",
|
|
171
|
+
"clearing-like": "it looks like clearing: felling, harvest, ploughing or construction", "unclear": "the cause is unclear"},
|
|
172
|
+
"tr": {"fire-like": "yangın izine benziyor: sonrasında yüzey koyu ve yanık",
|
|
173
|
+
"clearing-like": "açmaya benziyor: ağaç kesimi, hasat, sürüm ya da inşaat", "unclear": "nedeni belirsiz"},
|
|
174
|
+
}
|
|
175
|
+
CAUSE_SHORT = {"en": {"fire-like": "fire-like", "clearing-like": "clearing-like", "unclear": "unclear"},
|
|
176
|
+
"tr": {"fire-like": "yangına benzer", "clearing-like": "açmaya benzer", "unclear": "belirsiz"}}
|
|
177
|
+
STRENGTH = {"en": {"high": "strong", "medium": "moderate", "low": "weak"}, "tr": {"high": "güçlü", "medium": "orta", "low": "zayıf"}}
|
|
178
|
+
SEASONAL_TXT = {"en": " The same drop happened at this time of year before, so it is probably harvest or a seasonal cycle.",
|
|
179
|
+
"tr": " Aynı düşüş önceki yıllarda da yılın bu zamanında olmuş; muhtemelen hasat ya da mevsim döngüsü."}
|
|
180
|
+
EVENT_TXT = {"en": " The largest drop happened between {a} and {b}.", "tr": " En büyük düşüş {a} ile {b} arasında olmuş."}
|
|
181
|
+
BRIGHT_TXT = {"en": " Very bright surface: change here is unreliable.", "tr": " Çok parlak yüzey: buradaki değişim güvenilir değil."}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def class_label(key: str, lang: str) -> str:
|
|
185
|
+
return (CLASS_LABELS_TR if lang == "tr" else CLASS_LABELS_EN).get(key, key)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def describe(p: dict, lang: str = "en") -> str:
|
|
189
|
+
"""One plain sentence on what changed at a feature (``p`` = its properties)."""
|
|
190
|
+
lang = "tr" if lang == "tr" else "en"
|
|
191
|
+
s = WHAT[lang].get(p.get("class"), class_label(p.get("class", ""), lang))
|
|
192
|
+
if p.get("class") == "vegetation_loss" and p.get("likely_cause") in CAUSE[lang]:
|
|
193
|
+
s += "; " + CAUSE[lang][p["likely_cause"]]
|
|
194
|
+
s += "."
|
|
195
|
+
if p.get("likely_seasonal") is True:
|
|
196
|
+
s += SEASONAL_TXT[lang]
|
|
197
|
+
if p.get("event_before") and p.get("event_after"):
|
|
198
|
+
s += EVENT_TXT[lang].format(a=p["event_before"], b=p["event_after"])
|
|
199
|
+
if (p.get("bright_share") or 0) > 0.2:
|
|
200
|
+
s += BRIGHT_TXT[lang]
|
|
201
|
+
return s
|
earthdiff/indices.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Spectral indices (all inputs are surface reflectance, 0..1)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def normalized_difference(a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
|
8
|
+
"""(a - b) / (a + b), NaN where the sum is not positive or an input is NaN."""
|
|
9
|
+
a = a.astype(np.float32, copy=False)
|
|
10
|
+
b = b.astype(np.float32, copy=False)
|
|
11
|
+
s = a + b
|
|
12
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
13
|
+
out = (a - b) / s
|
|
14
|
+
out[~(s > 1e-6)] = np.nan
|
|
15
|
+
return out.astype(np.float32, copy=False)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def ndvi(nir, red):
|
|
19
|
+
"""Normalized Difference Vegetation Index: (NIR - Red) / (NIR + Red)."""
|
|
20
|
+
return normalized_difference(nir, red)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def mndwi(green, swir1):
|
|
24
|
+
"""Modified NDWI (Xu 2006): (Green - SWIR1) / (Green + SWIR1). > 0 mostly open water."""
|
|
25
|
+
return normalized_difference(green, swir1)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def ndbi(swir1, nir):
|
|
29
|
+
"""Normalized Difference Built-up Index: (SWIR1 - NIR) / (SWIR1 + NIR)."""
|
|
30
|
+
return normalized_difference(swir1, nir)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def bsi(blue, red, nir, swir1):
|
|
34
|
+
"""Bare Soil Index: ((SWIR1 + Red) - (NIR + Blue)) / ((SWIR1 + Red) + (NIR + Blue))."""
|
|
35
|
+
return normalized_difference(swir1 + red, nir + blue)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def nbr(nir, swir2):
|
|
39
|
+
"""Normalized Burn Ratio (Key & Benson 2006): (NIR - SWIR2) / (NIR + SWIR2), Sentinel-2 B08 and B12."""
|
|
40
|
+
return normalized_difference(nir, swir2)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def compute_indices(bands: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
|
44
|
+
out = {
|
|
45
|
+
"ndvi": ndvi(bands["nir"], bands["red"]),
|
|
46
|
+
"mndwi": mndwi(bands["green"], bands["swir16"]),
|
|
47
|
+
"bsi": bsi(bands["blue"], bands["red"], bands["nir"], bands["swir16"]),
|
|
48
|
+
"ndbi": ndbi(bands["swir16"], bands["nir"]),
|
|
49
|
+
}
|
|
50
|
+
if "swir22" in bands: # optional band: only present when B12 could be read
|
|
51
|
+
out["nbr"] = nbr(bands["nir"], bands["swir22"])
|
|
52
|
+
return out
|
earthdiff/mcp_prompts.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Texts shown to the model: server instructions and the guidance prompt/resource."""
|
|
2
|
+
|
|
3
|
+
INSTRUCTIONS = """earthdiff gives you "satellite eyes": it compares free Sentinel-2 imagery of two time windows and reports what changed
|
|
4
|
+
(vegetation loss or gain, new water, water loss, new bare soil / built-up) with hectares, confidence, warnings and maps.
|
|
5
|
+
No account or API key is needed. It is read-only with respect to your data; results are written to a local output folder.
|
|
6
|
+
|
|
7
|
+
Workflow
|
|
8
|
+
1. Place: if the user names a place, call find_place. If it returns status "ambiguous", ask the user which candidate they mean
|
|
9
|
+
(do not guess). If a bounding box is larger than the analysis limit, offer the narrowing_options; never shrink it silently.
|
|
10
|
+
2. Time: pick two windows. "after" is the recent period (YYYY-MM, YYYY-MM-DD or START/END). If "before" is left empty it is the
|
|
11
|
+
same window one year earlier (same season = fewer false alarms). For "what changed in the last year?" compare the most recent
|
|
12
|
+
one to three complete months with the same months a year earlier, in a season with good light (avoid snow and deep winter).
|
|
13
|
+
Windows are at most 366 days; prefer 1 to 3 months.
|
|
14
|
+
3. plan_change (optional, cheap): scene counts and warnings without downloading. Use it for large areas or unusual windows.
|
|
15
|
+
4. detect_change: runs the analysis (usually 1 to 4 minutes; progress is reported). It returns before, after and change-map images
|
|
16
|
+
plus numbers. Only one analysis runs at a time.
|
|
17
|
+
5. explain_method: when the user asks how it works or how far to trust it.
|
|
18
|
+
|
|
19
|
+
Answer honestly
|
|
20
|
+
- Always report: hectares and polygon counts per class, the comparable share of the area, the confidence and data_quality, and EVERY
|
|
21
|
+
entry of "warnings" (season mismatch, cloud, single scene, implausibly large change). Do not drop warnings to sound confident.
|
|
22
|
+
- Describe WHAT changed (spectral change), not WHY. Fire, felling, harvest, drought and construction can look the same. vegetation_loss_likely_cause
|
|
23
|
+
(fire-like / clearing-like / unclear) is a hint from dNBR, brightness and shape: say "fire-like", never "it was a fire".
|
|
24
|
+
- seasonal_check (optional, off by default, plan_change tells the extra cost) flags loss that repeats every year at the same season (likely harvest): report those hectares separately.
|
|
25
|
+
- If bright_surface.reliable is false (salt, sand, snow cover a large share), say that change there is not reliable and that no detected change must not be read as no change.
|
|
26
|
+
- This is a screening tool, not an official damage assessment. Say so, and suggest verification before any consequential decision.
|
|
27
|
+
- Credit the data: "Contains modified Copernicus Sentinel data" (the exact line is in the result); place search is (c) OpenStreetMap contributors.
|
|
28
|
+
- Tell the user where report.html (interactive swipe map) and changes.geojson were saved.
|
|
29
|
+
- bbox is [west, south, east, north] = [lon_min, lat_min, lon_max, lat_max] in degrees. Longitude first. Check center_lon_lat in the result.
|
|
30
|
+
- Unknown parameters are rejected; if a call fails, read the error: it says what to change.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
GUIDE = """# Using earthdiff
|
|
34
|
+
|
|
35
|
+
You are a careful remote-sensing analyst. The tools compare two Sentinel-2 time windows with transparent spectral rules.
|
|
36
|
+
|
|
37
|
+
1. **Resolve the place.** `find_place` (OpenStreetMap Nominatim). Ambiguous or oversized -> ask the user, offer the narrowing options.
|
|
38
|
+
2. **Choose windows.** Same season, one to three months each. Leave `before` empty for "same period last year".
|
|
39
|
+
3. **Plan, then run.** `plan_change` shows scene counts and warnings; `detect_change` does the work (progress is reported).
|
|
40
|
+
4. **Report.** Per class: hectares, polygons. Then comparable share, confidence, data quality, all warnings, the three images,
|
|
41
|
+
file locations, the Copernicus attribution. Say what the numbers are not: cause, official damage, verified.
|
|
42
|
+
5. **Method questions** -> `explain_method`.
|
|
43
|
+
|
|
44
|
+
Never invent a bbox from memory when a place search is possible. Never claim a cause. Never omit a warning.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def what_changed(place_or_area: str, period: str = "") -> str:
|
|
49
|
+
return (
|
|
50
|
+
f"{GUIDE}\n\nQuestion: what changed around {place_or_area}"
|
|
51
|
+
+ (f" ({period})" if period else "")
|
|
52
|
+
+ "?\n\nFirst resolve the place with find_place (ask if ambiguous or too large). Decide the two windows and say which you chose and why. "
|
|
53
|
+
"Optionally call plan_change, then detect_change. Answer with the numbers, every warning, the limits and the attribution."
|
|
54
|
+
)
|