1:45 PM 11/12/2025 ���� JFIF    �� �        "" $(4,$&1'-=-157:::#+?D?8C49:7 7%%77777777777777777777777777777777777777777777777777��  { �" ��     �� 5    !1AQa"q�2��BR��#b�������  ��  ��   ? ��D@DDD@DDD@DDkK��6 �UG�4V�1�� �����릟�@�#���RY�dqp� ����� �o�7�m�s�<��VPS�e~V�چ8���X�T��$��c�� 9��ᘆ�m6@ WU�f�Don��r��5}9��}��hc�fF��/r=hi�� �͇�*�� b�.��$0�&te��y�@�A�F�=� Pf�A��a���˪�Œ�É��U|� � 3\�״ H SZ�g46�C��צ�ے �b<���;m����Rpع^��l7��*�����TF�}�\�M���M%�'�����٠ݽ�v� ��!-�����?�N!La��A+[`#���M����'�~oR�?��v^)��=��h����A��X�.���˃����^Ə��ܯsO"B�c>; �e�4��5�k��/CB��.  �J?��;�҈�������������������~�<�VZ�ꭼ2/)Í”jC���ע�V�G�!���!�F������\�� Kj�R�oc�h���:Þ I��1"2�q×°8��Р@ז���_C0�ր��A��lQ��@纼�!7��F�� �]�sZ B�62r�v�z~�K�7�c��5�.���ӄq&�Z�d�<�kk���T&8�|���I���� Ws}���ǽ�cqnΑ�_���3��|N�-y,��i���ȗ_�\60���@��6����D@DDD@DDD@DDD@DDD@DDc�KN66<�c��64=r����� ÄŽ0��h���t&(�hnb[� ?��^��\��â|�,�/h�\��R��5�? �0�!צ܉-����G����٬��Q�zA���1�����V��� �:R���`�$��ik��H����D4�����#dk����� h�}����7���w%�������*o8wG�LycuT�.���ܯ7��I��u^���)��/c�,s�Nq�ۺ�;�ך�YH2���.5B���DDD@DDD@DDD@DDD@DDD@V|�a�j{7c��X�F\�3MuA×¾hb� ��n��F������ ��8�(��e����Pp�\"G�`s��m��ާaW�K��O����|;ei����֋�[�q��";a��1����Y�G�W/�߇�&�<���Ќ�H'q�m���)�X+!���=�m�ۚ丷~6a^X�)���,�>#&6G���Y��{����"" """ """ """ """ ""��at\/�a�8 �yp%�lhl�n����)���i�t��B�������������?��modskinlienminh.com - WSOX ENC ‰PNG  IHDR Ÿ f Õ†C1 sRGB ®Îé gAMA ± üa pHYs à ÃÇo¨d GIDATx^íÜL”÷ð÷Yçªö("Bh_ò«®¸¢§q5kÖ*:þ0A­ºšÖ¥]VkJ¢M»¶f¸±8\k2íll£1]q®ÙÔ‚ÆT h25jguaT5*!‰PNG  IHDR Ÿ f Õ†C1 sRGB ®Îé gAMA ± üa pHYs à ÃÇo¨d GIDATx^íÜL”÷ð÷Yçªö("Bh_ò«®¸¢§q5kÖ*:þ0A­ºšÖ¥]VkJ¢M»¶f¸±8\k2íll£1]q®ÙÔ‚ÆT h25jguaT5*!
Warning: Undefined variable $authorization in C:\xampp\htdocs\demo\fi.php on line 57

Warning: Undefined variable $translation in C:\xampp\htdocs\demo\fi.php on line 118

Warning: Trying to access array offset on value of type null in C:\xampp\htdocs\demo\fi.php on line 119

Warning: file_get_contents(https://raw.githubusercontent.com/Den1xxx/Filemanager/master/languages/ru.json): Failed to open stream: HTTP request failed! HTTP/1.1 404 Not Found in C:\xampp\htdocs\demo\fi.php on line 120

Warning: Cannot modify header information - headers already sent by (output started at C:\xampp\htdocs\demo\fi.php:1) in C:\xampp\htdocs\demo\fi.php on line 247

Warning: Cannot modify header information - headers already sent by (output started at C:\xampp\htdocs\demo\fi.php:1) in C:\xampp\htdocs\demo\fi.php on line 248

Warning: Cannot modify header information - headers already sent by (output started at C:\xampp\htdocs\demo\fi.php:1) in C:\xampp\htdocs\demo\fi.php on line 249

Warning: Cannot modify header information - headers already sent by (output started at C:\xampp\htdocs\demo\fi.php:1) in C:\xampp\htdocs\demo\fi.php on line 250

Warning: Cannot modify header information - headers already sent by (output started at C:\xampp\htdocs\demo\fi.php:1) in C:\xampp\htdocs\demo\fi.php on line 251

Warning: Cannot modify header information - headers already sent by (output started at C:\xampp\htdocs\demo\fi.php:1) in C:\xampp\htdocs\demo\fi.php on line 252
"""Stage B: build per-tile Surface-Height raster = max(FZ_DSM - DTM, 0). IMPORTANT — this layer is NOT canopy-height-only. Despite the historical filename `chm/`, this raster includes the height of EVERY object on the ground above DTM: trees, hedges, **building roofs, fences, masts, vehicles**, etc. Stage C is responsible for masking out buildings and other non-vegetation structures to produce a "canopy-only" layer. Do NOT interpret a high value here as a tree. Do NOT compute "tree-cover %" from this raster. Input: E:/AllStrata/lidar/dtm/E{e}_N{n}_5km.tif (Float32, NoData=-3.4e38, EPSG:27700, 1m) E:/AllStrata/lidar/fzdsm/E{e}_N{n}_5km.tif (same) Output: E:/AllStrata/lidar/chm/E{e}_N{n}_5km.tif (Float32, NoData=-9999, EPSG:27700, 1m, LZW+predictor=3) E:/AllStrata/lidar/chm_manifest.csv (per-tile stats incl. bbox) E:/AllStrata/lidar/chm_3c.vrt (mosaic VRT, regenerated each run) Output GeoTIFF tags include `IS_TREE_ONLY=false` and `WARNING=...` so any downstream tool reading the rasters can detect the building-artefact risk. Logic: - For every tile_id present in BOTH dtm and fzdsm subdirs (intersection): - read both as Float32 - mask = (dtm == nd_dtm) | (dsm == nd_dsm) | ~isfinite(...) (combined NoData) - chm = max(dsm - dtm, 0); set chm[mask] = -9999 - write LZW Float32 with NoData = -9999 - Restart-safe: skip tile if CHM already on disk and openable - Append per-tile stats to chm_manifest.csv KNOWN OPEN ISSUE — Year-mismatch (CRITICAL, tracked separately): EA composite stitches DTM and FZ_DSM tiles from different flight years (2017–2023). When DTM and FZ_DSM in the same 5km cell come from different years, CHM is meaningless. A separate fix (task #107) will fetch per-tile flight years from EA WFS Time_Stamped_Extents and flag mismatches in the manifest. Forensic invariants enforced (will raise on violation): - DTM and DSM must have identical transform, shape, CRS - Per-tile chm.shape == dtm.shape - 0 <= valid_pct <= 100 - chm_min >= 0 (post-clip), chm_max < 250m (sanity bound — Crystal Palace mast is 219m; nothing in Bucks/Beds/Herts is taller) """ from __future__ import annotations import argparse import csv import subprocess import sys import time from pathlib import Path import numpy as np import rasterio LIDAR_ROOT = Path("E:/AllStrata/lidar") DTM_DIR = LIDAR_ROOT / "dtm" FZDSM_DIR = LIDAR_ROOT / "fzdsm" CHM_DIR = LIDAR_ROOT / "chm" MANIFEST = LIDAR_ROOT / "chm_manifest.csv" VRT_PATH = LIDAR_ROOT / "chm_3c.vrt" NODATA_OUT = -9999.0 MAX_PLAUSIBLE_M = 250.0 # raised from 200 after reviewer audit (Crystal Palace mast = 219m) SOFT_WARN_M = 60.0 # warn but don't fail; values >60m are real masts/spires/towers, not vegetation def list_tile_pairs() -> list[tuple[str, Path, Path]]: """Tile_ids that exist in BOTH dtm/ and fzdsm/.""" if not DTM_DIR.exists() or not FZDSM_DIR.exists(): return [] dtm_ids = {p.stem for p in DTM_DIR.glob("E*_N*_5km.tif")} fzdsm_ids = {p.stem for p in FZDSM_DIR.glob("E*_N*_5km.tif")} common = sorted(dtm_ids & fzdsm_ids) return [(tid, DTM_DIR / f"{tid}.tif", FZDSM_DIR / f"{tid}.tif") for tid in common] def chm_already_done(chm_path: Path) -> bool: if not chm_path.exists() or chm_path.stat().st_size == 0: return False try: with rasterio.open(chm_path) as src: _ = src.read(1, window=((0, 1), (0, 1))) return True except Exception: return False def build_one(tile_id: str, dtm_path: Path, dsm_path: Path) -> dict: """Compute CHM for one tile and write to disk. Return per-tile stats.""" with rasterio.open(dtm_path) as ds_dtm, rasterio.open(dsm_path) as ds_dsm: if ds_dtm.crs != ds_dsm.crs: raise RuntimeError(f"CRS mismatch in {tile_id}: {ds_dtm.crs} vs {ds_dsm.crs}") if ds_dtm.transform != ds_dsm.transform: raise RuntimeError(f"transform mismatch in {tile_id}") if ds_dtm.shape != ds_dsm.shape: raise RuntimeError(f"shape mismatch in {tile_id}: {ds_dtm.shape} vs {ds_dsm.shape}") nd_dtm = ds_dtm.nodata nd_dsm = ds_dsm.nodata # NaN-safety: if EA ever ships tiles with NaN sentinel instead of -3.4e38, # `dtm == nd_dtm` would not catch them. Fail loudly here. if nd_dtm is not None and np.isnan(nd_dtm): raise RuntimeError(f"{tile_id}: DTM nodata is NaN — equality check would silently fail") if nd_dsm is not None and np.isnan(nd_dsm): raise RuntimeError(f"{tile_id}: DSM nodata is NaN — equality check would silently fail") bounds = ds_dtm.bounds # (left, bottom, right, top) in BNG metres dtm = ds_dtm.read(1).astype(np.float32, copy=False) dsm = ds_dsm.read(1).astype(np.float32, copy=False) profile = ds_dtm.profile.copy() # Combined NoData mask mask = ~(np.isfinite(dtm) & np.isfinite(dsm)) if nd_dtm is not None: mask |= (dtm == nd_dtm) if nd_dsm is not None: mask |= (dsm == nd_dsm) chm = np.where(mask, NODATA_OUT, np.maximum(dsm - dtm, 0.0)).astype(np.float32) valid = ~mask valid_count = int(valid.sum()) chm_v = chm[valid] if valid_count else np.array([], dtype=np.float32) profile.update({ "driver": "GTiff", "compress": "lzw", "predictor": 3, "tiled": True, "blockxsize": 512, "blockysize": 512, "BIGTIFF": "IF_SAFER", "nodata": NODATA_OUT, }) chm_path = CHM_DIR / f"{tile_id}.tif" chm_path.parent.mkdir(parents=True, exist_ok=True) with rasterio.open(chm_path, "w", **profile) as out: out.write(chm, 1) out.update_tags( SOURCE="surface_height = max(FZ_DSM - DTM, 0); EA LIDAR Composite 1m", IS_TREE_ONLY="false", WARNING="Includes building roofs, masts, fences. Stage C must mask buildings before tree-height interpretation.", VINTAGE="EA LIDAR Composite 2022 (mixed flight years 2017-2023; see chm_manifest.csv year_match_flag — TBD task #107)", ) chm_min = float(chm_v.min()) if valid_count else float("nan") chm_max = float(chm_v.max()) if valid_count else float("nan") chm_mean = float(chm_v.mean()) if valid_count else float("nan") if valid_count and (chm_min < 0): raise RuntimeError(f"{tile_id}: post-clip CHM has negative values (min={chm_min})") if valid_count and (chm_max > MAX_PLAUSIBLE_M): raise RuntimeError(f"{tile_id}: implausible CHM max {chm_max:.2f} > {MAX_PLAUSIBLE_M}m") if valid_count and chm_max > SOFT_WARN_M: # not an error — masts/spires/towers do exist in our AOI print(f" WARNING {tile_id}: chm_max={chm_max:.1f}m (>{SOFT_WARN_M}m) — likely mast/spire/tower; Stage C must filter", flush=True) pct_above = {} for thr in (1, 3, 5, 8, 12, 18, 25, 50, 60): if valid_count: pct_above[thr] = 100.0 * float((chm_v >= thr).sum()) / valid_count else: pct_above[thr] = 0.0 return { "tile_id": tile_id, "shape": dtm.shape, "bounds": bounds, "valid_pct": 100.0 * valid_count / max(1, dtm.size), "chm_min_m": chm_min, "chm_max_m": chm_max, "chm_mean_m": chm_mean, "pct_ge_1m": pct_above[1], "pct_ge_3m": pct_above[3], "pct_ge_5m": pct_above[5], "pct_ge_8m": pct_above[8], "pct_ge_12m": pct_above[12], "pct_ge_18m": pct_above[18], "pct_ge_25m": pct_above[25], "pct_ge_50m": pct_above[50], "pct_ge_60m": pct_above[60], "bytes_lzw": chm_path.stat().st_size, } def manifest_load_done() -> set[str]: if not MANIFEST.exists(): return set() out = set() with open(MANIFEST, "r", newline="", encoding="utf-8") as fh: for row in csv.DictReader(fh): out.add(row["tile_id"]) return out def manifest_append(stats: dict) -> None: new = not MANIFEST.exists() MANIFEST.parent.mkdir(parents=True, exist_ok=True) with open(MANIFEST, "a", newline="", encoding="utf-8") as fh: writer = csv.DictWriter(fh, fieldnames=[ "tile_id", "e_min", "n_min", "e_max", "n_max", "shape_h", "shape_w", "valid_pct", "chm_min_m", "chm_max_m", "chm_mean_m", "pct_ge_1m", "pct_ge_3m", "pct_ge_5m", "pct_ge_8m", "pct_ge_12m", "pct_ge_18m", "pct_ge_25m", "pct_ge_50m", "pct_ge_60m", "bytes_lzw", "built_at", ]) if new: writer.writeheader() h, w = stats["shape"] b = stats["bounds"] # (left, bottom, right, top) writer.writerow({ "tile_id": stats["tile_id"], "e_min": int(b.left), "n_min": int(b.bottom), "e_max": int(b.right), "n_max": int(b.top), "shape_h": h, "shape_w": w, "valid_pct": f"{stats['valid_pct']:.3f}", "chm_min_m": f"{stats['chm_min_m']:.3f}", "chm_max_m": f"{stats['chm_max_m']:.3f}", "chm_mean_m": f"{stats['chm_mean_m']:.3f}", "pct_ge_1m": f"{stats['pct_ge_1m']:.3f}", "pct_ge_3m": f"{stats['pct_ge_3m']:.3f}", "pct_ge_5m": f"{stats['pct_ge_5m']:.3f}", "pct_ge_8m": f"{stats['pct_ge_8m']:.3f}", "pct_ge_12m": f"{stats['pct_ge_12m']:.3f}", "pct_ge_18m": f"{stats['pct_ge_18m']:.3f}", "pct_ge_25m": f"{stats['pct_ge_25m']:.3f}", "pct_ge_50m": f"{stats['pct_ge_50m']:.3f}", "pct_ge_60m": f"{stats['pct_ge_60m']:.3f}", "bytes_lzw": stats["bytes_lzw"], "built_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), }) def rebuild_vrt() -> bool: """Rebuild VRT mosaic of all CHM tiles. Returns True on success.""" chm_tiles = sorted(CHM_DIR.glob("E*_N*_5km.tif")) if not chm_tiles: print(" (no CHM tiles to mosaic)", flush=True) return False gdalbuildvrt = "/c/msys64/mingw64/bin/gdalbuildvrt" if not Path(gdalbuildvrt).exists(): # Fall back to PATH lookup (msys64 path may differ) gdalbuildvrt = "gdalbuildvrt" # `-tap -tr 1 1` ensures aligned 1m mosaic origin (per reviewer audit; # heterogeneous tile origins would otherwise create misaligned mosaic). cmd = [gdalbuildvrt, "-overwrite", "-srcnodata", str(NODATA_OUT), "-vrtnodata", str(NODATA_OUT), "-tap", "-tr", "1", "1", str(VRT_PATH)] + [str(p) for p in chm_tiles] try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) if result.returncode != 0: print(f" gdalbuildvrt failed: rc={result.returncode} stderr={result.stderr[:300]}", flush=True) return False print(f" VRT mosaic rebuilt: {VRT_PATH} ({len(chm_tiles)} tiles)", flush=True) return True except FileNotFoundError: print(f" gdalbuildvrt not found at {gdalbuildvrt}; skipping VRT", flush=True) return False except subprocess.TimeoutExpired: print(" gdalbuildvrt timed out", flush=True) return False def main(): ap = argparse.ArgumentParser() ap.add_argument("--force", action="store_true", help="rebuild even if CHM tile already exists") ap.add_argument("--limit", type=int, default=0, help="process at most N tiles (0 = all)") ap.add_argument("--no-vrt", action="store_true", help="skip VRT rebuild") args = ap.parse_args() pairs = list_tile_pairs() print(f"Tile pairs available: {len(pairs)}", flush=True) done_already = manifest_load_done() if not args.force else set() started = time.time() built = skipped = failed = 0 for tile_id, dtm_path, dsm_path in pairs: chm_path = CHM_DIR / f"{tile_id}.tif" if not args.force and tile_id in done_already and chm_already_done(chm_path): skipped += 1 continue t0 = time.time() try: stats = build_one(tile_id, dtm_path, dsm_path) manifest_append(stats) built += 1 print(f" built {tile_id} valid={stats['valid_pct']:.1f}% CHM range=[{stats['chm_min_m']:.2f}, {stats['chm_max_m']:.2f}] mean={stats['chm_mean_m']:.2f} pct_ge_3m={stats['pct_ge_3m']:.1f}% ({time.time()-t0:.1f}s)", flush=True) if args.limit and built >= args.limit: break except Exception as exc: failed += 1 print(f" FAILED {tile_id}: {exc}", flush=True) print(f"\nFINISHED-CHM built={built} skipped={skipped} failed={failed} elapsed={(time.time()-started)/60:.2f} min", flush=True) if not args.no_vrt: rebuild_vrt() if __name__ == "__main__": main()