diff --git a/preselection.py b/preselection.py index 6aced47..5872c2e 100644 --- a/preselection.py +++ b/preselection.py @@ -10,7 +10,8 @@ from datetime import datetime WINDOW_DAYS = 14 MIN_WINDOW_SIZE = 3 THRESHOLDS = {"aggressive": {"threshold": 0.3, "delta": 0.15}, "nonaggressive": {"threshold": 0.2, "delta": 0.25}} -BLUE_MIN = 0.01 +# S2 uses reflectance * 10000, S3 uses 0-1 +BLUE_MIN = {"s2": 100, "s3": 0.01} GREEN_BAND = 2 RED_BAND = 3 @@ -57,12 +58,13 @@ def _extract_date(filename): return None, None -def _is_excluded(entry, entries, strategy): +def _is_excluded(entry, entries, strategy, source="s2"): """True if entry is excluded by strategy (NDVI threshold/delta or dark blue).""" th = THRESHOLDS[strategy] if entry.get("ndvi") is None: return True - if entry.get("b02") is not None and entry["b02"] < BLUE_MIN: + blue_min = BLUE_MIN.get(source, BLUE_MIN["s2"]) + if entry.get("b02") is not None and entry["b02"] < blue_min: return True entry_date = datetime.fromisoformat(entry["date"].replace("Z", "+00:00")) window_ndvi = [] @@ -110,8 +112,8 @@ def create_timeseries(season, site_position, site_name): timeseries.sort(key=lambda e: e["date"]) for e in timeseries: - e["excluded_aggressive"] = _is_excluded(e, timeseries, "aggressive") - e["excluded_nonaggressive"] = _is_excluded(e, timeseries, "nonaggressive") + e["excluded_aggressive"] = _is_excluded(e, timeseries, "aggressive", source) + e["excluded_nonaggressive"] = _is_excluded(e, timeseries, "nonaggressive", source) with open(output_file, "w") as out: json.dump(timeseries, out, indent=2) diff --git a/run.py b/run.py index bf54b6f..217aa6d 100644 --- a/run.py +++ b/run.py @@ -15,6 +15,7 @@ from preselection import create_timeseries def run_pipeline(season, site_position, site_name): """Run pipeline (downloads + preselection).""" try: + print(f"Downloading S2, S3, and PhenoCam: {site_name}, {season}") download_s2(season, site_position, site_name) download_s3(season, site_position, site_name) download_phenocam(season, site_position, site_name) @@ -41,11 +42,11 @@ def run_pipeline(season, site_position, site_name): if __name__ == "__main__": - #run_pipeline(2024, (35.3045, 25.0743), "forthgr") - run_pipeline(2024, (47.116171, 11.320308), "innsbruck") - #run_pipeline(2020, (47.116171, 11.320308), "innsbruck") - #run_pipeline(2024, (58.5633, 24.3688), "pitsalu") - #run_pipeline(2023, (64.2437, 19.7673), "vindeln2") - #run_pipeline(2024, (36.7455, -6.0033), "sunflowerjerez1") - #run_pipeline(2024, (42.6558, 26.9837), "institutekarnobat") + run_pipeline(2024, (35.3045, 25.0743), "forthgr") + #run_pipeline(2024, (47.116171, 11.320308), "innsbruck") + run_pipeline(2020, (47.116171, 11.320308), "innsbruck") + run_pipeline(2024, (58.5633, 24.3688), "pitsalu") + run_pipeline(2023, (64.2437, 19.7673), "vindeln2") + run_pipeline(2024, (36.7455, -6.0033), "sunflowerjerez1") + run_pipeline(2024, (42.6558, 26.9837), "institutekarnobat") diff --git a/webapp/cloudy.html b/webapp/cloudy.html deleted file mode 100644 index b979797..0000000 --- a/webapp/cloudy.html +++ /dev/null @@ -1,354 +0,0 @@ - - - - NDVI Viewer - - - - - - - -
-
- -
2024-01-01
- -
-
-
-

S2

-
NDVI Timeseries
- -
RGB Imagery
-
-
-
NDVI Imagery
-
-
-
-
-

S3

-
NDVI Timeseries
- -
RGB Imagery
-
-
-
NDVI Imagery
-
-
-
-
-
- - - diff --git a/webapp/s2-timeseries.html b/webapp/preselection.html similarity index 83% rename from webapp/s2-timeseries.html rename to webapp/preselection.html index 9a21243..9476fcc 100644 --- a/webapp/s2-timeseries.html +++ b/webapp/preselection.html @@ -12,7 +12,11 @@ .selectors { margin-bottom: 20px; } .selectors select { padding: 5px 10px; font-size: 14px; margin-right: 15px; } h1 { margin: 0 0 5px 0; font-size: 22px; } - h2 { margin: 0 0 15px 0; font-size: 16px; color: #666; } + .season-row { padding-bottom: 15px; } + h2 { margin: 0; font-size: 16px; color: #666; display: inline; } + .download-links { margin-left: 10px; font-size: 14px; } + .download-links a { margin-right: 8px; color: #0066cc; text-decoration: none; } + .download-links a:hover { text-decoration: underline; } .plot { width: 100%; height: 100px; border: 1px solid #ccc; margin-bottom: 15px; } .plot-label { font-size: 12px; margin-bottom: 3px; color: #666; } #dateSlider { width: 100%; margin: 15px 0; } @@ -27,23 +31,27 @@

Innsbruck

-

2024

+

2024

- - + + + + +
2024-01-01
-
S2 RGB (closest available)
+
S2 RGB (closest available)
@@ -59,6 +67,8 @@ { key: "b8a", label: "B8A (NIR)", color: "#9900cc" } ]; let siteName = "innsbruck", season = "2024"; + let source = "s2"; + let exclusion = "none"; let sitePosition = [47.116171, 11.320308]; let start = new Date(2024, 0, 1); let timeseries = []; @@ -68,10 +78,11 @@ let s2Map = null, s2Overlay = null, s2Marker = null; const urlParams = new URLSearchParams(location.search); - const [strategy, sigma] = (urlParams.get("scenario") || "aggressive_20").split("_"); - function getBasePath() { - return `processed_${strategy}_sigma${sigma || "20"}`; + function filteredTimeseries(arr) { + if (exclusion === "none") return arr; + const key = exclusion === "aggressive" ? "excluded_aggressive" : "excluded_nonaggressive"; + return arr.filter(t => !t[key]); } function fmtDate(d) { @@ -92,7 +103,7 @@ const w = canvas.width, h = canvas.height, pad = 30; const plotW = w - pad * 2, plotH = h - pad * 2; - const data = timeseries.filter(t => t[bandKey] != null); + const data = filteredTimeseries(timeseries).filter(t => t[bandKey] != null); if (!data.length) return; const dates = data.map(t => new Date(t.date)); @@ -157,7 +168,7 @@ canvas.height = 100; const w = canvas.width, h = canvas.height, pad = 30; const plotW = w - pad * 2, plotH = h - pad * 2; - const data = ndviTimeseries.filter(t => t.ndvi != null); + const data = filteredTimeseries(ndviTimeseries).filter(t => t.ndvi != null); if (!data.length) return; const dates = data.map(t => new Date(t.date)); @@ -221,7 +232,7 @@ canvas.height = 100; const w = canvas.width, h = canvas.height, pad = 30; const plotW = w - pad * 2, plotH = h - pad * 2; - const data = gccTimeseries.filter(t => t.greenness_index != null); + const data = filteredTimeseries(gccTimeseries).filter(t => t.greenness_index != null); if (!data.length) return; const dates = data.map(t => new Date(t.date)); @@ -283,25 +294,36 @@ BANDS.forEach(b => drawBandPlot(`plot_${b.key}`, b.key, b.label, b.color)); } + function computeGcc(entry) { + const b = entry.b02 + entry.b03 + entry.b04; + return b > 0 ? entry.b03 / b : null; + } + async function loadTimeseries() { - const base = `../data/${siteName}/${season}/${getBasePath()}`; + const rawBase = `../data/${siteName}/${season}/raw`; + const src = document.getElementById("sourceSelect")?.value || "s2"; + source = src; try { - const [bandsRes, gccRes, ndviRes] = await Promise.all([ - fetch(`${base}/s2_bands/timeseries.json`), - fetch(`${base}/gcc/s2/timeseries.json`), - fetch(`${base}/ndvi/s2/timeseries.json`) - ]); - timeseries = bandsRes.ok ? await bandsRes.json() : []; - gccTimeseries = gccRes.ok ? await gccRes.json() : []; - ndviTimeseries = ndviRes.ok ? await ndviRes.json() : []; + const preselectionRes = await fetch(`${rawBase}/preselection/${source}_preselection.json`); + const preselection = preselectionRes.ok ? await preselectionRes.json() : []; + timeseries = preselection; + ndviTimeseries = preselection; + gccTimeseries = preselection.map(t => ({ ...t, greenness_index: computeGcc(t) })).filter(t => t.greenness_index != null); } catch { timeseries = []; - gccTimeseries = []; ndviTimeseries = []; + gccTimeseries = []; } + const srcLabel = source.toUpperCase(); + document.getElementById("mapLabel").textContent = `${srcLabel} RGB (closest available)`; + const jsonUrl = `${rawBase}/preselection/${source}_preselection.json`; + const csvUrl = `${rawBase}/preselection/${source}_preselection.csv`; + document.getElementById("downloadLinks").innerHTML = + `[JSON]` + + `[CSV]`; document.getElementById("bandPlots").innerHTML = - `
S2 NDVI
` + - `
S2 GCC (Greenness Index)
` + + `
${srcLabel} NDVI
` + + `
${srcLabel} GCC (Greenness Index)
` + BANDS.map(b => `
${b.label}
`).join(""); const yearEnd = new Date(parseInt(season), 11, 31); document.getElementById("dateSlider").max = Math.ceil((yearEnd - start) / 86400000); @@ -312,7 +334,7 @@ async function probeDataExists(sitename, s) { try { - const res = await fetch(`../data/${sitename}/${s}/processed_aggressive_sigma20/s2_bands/timeseries.json`, { method: "HEAD" }); + const res = await fetch(`../data/${sitename}/${s}/raw/preselection/s2_preselection.json`, { method: "HEAD" }); return res.ok; } catch { return false; } } @@ -381,7 +403,9 @@ `` ).join(""); document.getElementById("seasonSelect").value = initialSeason; - document.getElementById("scenarioSelect").value = `${strategy}_${sigma || "20"}`; + document.getElementById("sourceSelect").value = urlParams.get("source") || "s2"; + exclusion = urlParams.get("exclusion") || "none"; + document.getElementById("exclusionSelect").value = exclusion; const initSite = getSiteBySitename(initialSite); if (initSite?.geometry?.coordinates) { @@ -403,10 +427,18 @@ document.getElementById("seasonSelect").addEventListener("change", function() { setSiteSeason(siteSelect.value, this.value); }); - document.getElementById("scenarioSelect").addEventListener("change", function() { - const p = new URLSearchParams(location.search); - p.set("scenario", this.value); - window.location.search = p.toString(); + document.getElementById("sourceSelect").addEventListener("change", async function() { + source = this.value; + urlParams.set("source", source); + history.replaceState({}, "", `?${urlParams}`); + await loadTimeseries(); + }); + document.getElementById("exclusionSelect").addEventListener("change", function() { + exclusion = this.value; + urlParams.set("exclusion", exclusion); + history.replaceState({}, "", `?${urlParams}`); + drawAllPlots(); + updateS2Imagery(); }); await setSiteSeason(initialSite, initialSeason); @@ -420,7 +452,7 @@ function closestFilename(dateStr) { const target = new Date(dateStr); - const withData = timeseries.filter(t => t.filename); + const withData = filteredTimeseries(timeseries).filter(t => t.filename); if (!withData.length) return null; const closest = withData.reduce((c, t) => Math.abs(new Date(t.date) - target) < Math.abs(new Date(c.date) - target) ? t : c @@ -435,7 +467,7 @@ } async function loadS2Geotiff(filename) { - const path = `../data/${siteName}/${season}/${getBasePath()}/s2/${filename}`; + const path = `../data/${siteName}/${season}/raw/${source}/${filename}`; const tiff = await GeoTIFF.fromArrayBuffer(await (await fetch(path)).arrayBuffer()); const image = await tiff.getImage(); const rasters = await image.readRasters();