Download code/labeling/src/step3_mature_cloud_masking.py from lsh9034/ci-net: direct link, hf CLI and curl.
- Browser
- Download file 18.1 kB
-
https://huggingface.co/lsh9034/ci-net/resolve/main/code/labeling/src/step3_mature_cloud_masking.py
- Command line
-
hf download hf://lsh9034/ci-net/code/labeling/src/step3_mature_cloud_masking.py
-
curl -L -o step3_mature_cloud_masking.py https://huggingface.co/lsh9034/ci-net/resolve/main/code/labeling/src/step3_mature_cloud_masking.py
18.1 kB
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| Mature Cloud Removal (temporal descendant masking) | |
| κ°μ κ°μ | |
| - step2μ label/link κ²°κ³Όλ₯Ό λ°λΌκ°λ©° μ΄κΈ° κ΅¬λ¦ λλ μ±μ ꡬλ¦μΌλ‘ νμ λ ν΄λ¬μ€ν°μ λ―Έλ ν΄λ¬μ€ν°λ₯Ό μ κ±°νλ€. | |
| - μ κ±° 쑰건μ BT, radar κ°, κ³Όκ±° μ°κ²° μ¬λΆ, ν΄λ¬μ€ν° ν¬κΈ°λ₯Ό ν¨κ» μ¬μ©νλ€. | |
| - μ΅μ λ²μ μ μκ²½λ/λλ’° 보쑰νμΌμ μ¬μ©νμ§ μκ³ , BT μ λ ₯μ ir105 λ° RADAR_VAR(hsr)λ§ μ¬μ©νλ€. | |
| λ§μ€νΉ 쑰건 | |
| 1. BT <= MATURE_THRESHOLD Kμ΄λ©΄ ν΄λΉ μμ μ΄ν ν΄λ¬μ€ν° μ κ±° | |
| 2. RADAR_VAR >= RADAR_THRESHOLD dBZμ΄λ©΄ ν΄λΉ μμ μ΄ν ν΄λ¬μ€ν° μ κ±° | |
| 3. radar μμ λ°μ΄κ³ κ³Όκ±° μ°κ²°μ΄ μμΌλ©΄ μ 체 μμ μ£ΌκΈ° μ κ±° | |
| 4. MIN_SIZE_THRESHOLD ν½μ μ΄μμ΄κ³ κ³Όκ±° μ°κ²°μ΄ μμΌλ©΄ μ 체 μμ μ£ΌκΈ° μ κ±° | |
| * μ λ ₯ | |
| - step2 label: YYYYMMDDHHMM_label.nc | |
| - step2 links: YYYYMMDDHHMM_links.pkl | |
| - BT/RADAR npy: concat_gk2a_radar_YYYYMMDDHHMM.npy (ir105, hsr/cappi) | |
| * κ²°κ³Όλ¬Ό | |
| - YYYYMMDDHHMM_label.nc (μ κ±° λμ ν΄λ¬μ€ν°κ° NaN μ²λ¦¬λ μ΅μ’ label) | |
| κ²½λ‘ μ€μ | |
| - CONCAT_BT_BASE/LABEL_BASE/LINKS_BASE/SAVE_DIRλ import μμ μλ λΉ λ¬Έμμ΄μ΄λ€. | |
| - mainμμ build/run/step3_mature_cloud_masking_config.jsonμ μ½μ λ€ apply_config()κ° μ€μ κ²½λ‘λ‘ μ±μ΄λ€. | |
| - bt_rootκ° data_preprocess/result/res_2kmμ²λΌ L1B/L2 ν΄λμ΄λ©΄, μ€μ labeling μ λ ₯μΈ L1B/YYYYMMDDλ₯Ό μλ μ ννλ€. | |
| """ | |
| import os | |
| import sys | |
| import argparse | |
| import numpy as np | |
| import xarray as xr | |
| from datetime import datetime, timedelta, timezone | |
| from collections import deque | |
| from tqdm import tqdm | |
| import pickle | |
| import pandas as pd | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # κ²½λ‘/νλΌλ―Έν° | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| try: | |
| sys.stdout.reconfigure(encoding="utf-8", errors="replace") | |
| sys.stderr.reconfigure(encoding="utf-8", errors="replace") | |
| except AttributeError: | |
| pass | |
| CONFIG_NAME = "step3_mature_cloud_masking_config.json" | |
| def _find_package_root(config_name: str) -> str: | |
| cur = os.path.dirname(os.path.abspath(__file__)) | |
| while True: | |
| if os.path.exists(os.path.join(cur, "build", "run", config_name)): | |
| return cur | |
| parent = os.path.dirname(cur) | |
| if parent == cur: | |
| return os.path.abspath(os.getcwd()) | |
| cur = parent | |
| ROOT = _find_package_root(CONFIG_NAME) | |
| RUN_DIR = os.path.join(ROOT, "build", "run") | |
| sys.path.insert(0, ROOT) | |
| from .config_utils import load_config | |
| def _resolve_path(path: str) -> str: | |
| if os.path.isabs(path): | |
| return path | |
| return os.path.abspath(os.path.join(RUN_DIR, path)) | |
| def _has_date_dirs(path: str) -> bool: | |
| if not os.path.isdir(path): | |
| return False | |
| return any( | |
| os.path.isdir(os.path.join(path, name)) and len(name) == 8 and name.isdigit() | |
| for name in os.listdir(path) | |
| ) | |
| def _resolve_data_root(path: str) -> str: | |
| if _has_date_dirs(path): | |
| return path | |
| for subdir in ("L1B", "l1b"): | |
| candidate = os.path.join(path, subdir) | |
| if _has_date_dirs(candidate): | |
| return candidate | |
| return path | |
| # Config μ μ© μ placeholder. μ€μ κ°μ apply_config()μμ μ±μ΄λ€. | |
| CONCAT_BT_BASE = "" | |
| LABEL_BASE = "" | |
| LINKS_BASE = "" | |
| SAVE_DIR = "" | |
| MATURE_THRESHOLD = 233.0 # IR10.5 (K) | |
| RADAR_VAR = "hsr" # μ¬μ©ν λ μ΄λ λ³μ: "hsr" λλ "cappi" | |
| RADAR_THRESHOLD = 35.0 # dBZ | |
| TIME_STEP = 10 # λΆ | |
| MAX_REMOVE_HOURS = 6 # μκ° | |
| # μ΄κΈ° κ΅¬λ¦ νν°λ§ μ€μ | |
| MIN_SIZE_THRESHOLD = 156 # ν½μ μ μκ³κ° | |
| START_TIME = "202508010000" # YYYYMMDDHHMM | |
| END_TIME = "202510312350" # YYYYMMDDHHMM | |
| def apply_config(cfg: dict) -> None: | |
| global CONCAT_BT_BASE, LABEL_BASE, LINKS_BASE, SAVE_DIR | |
| global MATURE_THRESHOLD, RADAR_VAR, RADAR_THRESHOLD, TIME_STEP | |
| global MAX_REMOVE_HOURS, MIN_SIZE_THRESHOLD, START_TIME, END_TIME | |
| CONCAT_BT_BASE = _resolve_data_root(_resolve_path(cfg["bt_root"])) | |
| LABEL_BASE = _resolve_path(cfg["label_root"]) | |
| LINKS_BASE = _resolve_path(cfg.get("links_root", cfg["label_root"])) | |
| SAVE_DIR = _resolve_path(cfg["output_dir"]) | |
| os.makedirs(SAVE_DIR, exist_ok=True) | |
| MATURE_THRESHOLD = float(cfg.get("mature_threshold", MATURE_THRESHOLD)) | |
| RADAR_VAR = cfg.get("radar_var", RADAR_VAR) | |
| RADAR_THRESHOLD = float(cfg.get("radar_threshold", RADAR_THRESHOLD)) | |
| TIME_STEP = int(cfg.get("time_step_min", TIME_STEP)) | |
| MAX_REMOVE_HOURS = int(cfg.get("max_remove_hours", MAX_REMOVE_HOURS)) | |
| MIN_SIZE_THRESHOLD = int(cfg.get("min_size_threshold", MIN_SIZE_THRESHOLD)) | |
| START_TIME = cfg.get("start_time", START_TIME) | |
| END_TIME = cfg.get("end_time", END_TIME) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # μ νΈ/μΊμ | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DATA_CACHE = {} # dt -> (label, ir105, radar) | |
| LINKS_CACHE = {} # dt -> { "YYYY..._id": ["YYYY.. ._id", ...] } | |
| def generate_timesteps(start_time, end_time, step_minutes=TIME_STEP): | |
| s = datetime.strptime(start_time, "%Y%m%d%H%M") | |
| e = datetime.strptime(end_time, "%Y%m%d%H%M") | |
| out, cur = [], s | |
| while cur <= e: | |
| out.append(cur.strftime("%Y%m%d%H%M")) | |
| cur += timedelta(minutes=step_minutes) | |
| return out | |
| def safe_unique_labels(label_arr): | |
| """λΌλ²¨μμ 0/NaN μ κ±°νκ³ μ μ λ μ΄λΈ λ°°μ΄ λ°ν""" | |
| vals = np.unique(label_arr) | |
| if vals.dtype.kind == "f": | |
| vals = vals[np.isfinite(vals)] | |
| vals = vals[vals != 0] | |
| return vals.astype(np.int64) | |
| def load_data(dt_str): | |
| day = dt_str[:8] | |
| lp = os.path.join(LABEL_BASE, day, f"{dt_str}_label.nc") | |
| bp = os.path.join(CONCAT_BT_BASE, day, f"concat_gk2a_radar_{dt_str}.npy") | |
| label, ir105, radar = None, None, None | |
| # 1. λΌλ²¨ λ°μ΄ν° λ‘λ (λ μ΄λ μ 무μ κ΄κ³μμ΄ λ‘λ μλ) | |
| if os.path.exists(lp): | |
| try: | |
| with xr.open_dataset(lp) as lds: | |
| label = lds["label"].values.copy() | |
| except Exception: | |
| pass | |
| # 2. BT/λ μ΄λ(.npy) λ°μ΄ν° λ‘λ (μκ±°λ ν€κ° λλ½λλ©΄ None μ μ§) | |
| if os.path.exists(bp): | |
| try: | |
| data = np.load(bp, allow_pickle=True).item() | |
| if isinstance(data, dict): | |
| ir105 = data.get("ir105", None) | |
| radar = data.get(RADAR_VAR, None) | |
| except Exception: | |
| pass | |
| return label, ir105, radar | |
| def get_data(dt): | |
| if dt not in DATA_CACHE: | |
| DATA_CACHE[dt] = load_data(dt) | |
| return DATA_CACHE[dt] | |
| def load_links(dt_str): | |
| """μ격 νμ: {"YYYYMMDDHHMM_id": ["YYYYMMDDHHMM_id", ...]}""" | |
| day = dt_str[:8] | |
| p = os.path.join(LINKS_BASE, day, f"{dt_str}_links.pkl") | |
| if not os.path.exists(p): | |
| return {} | |
| try: | |
| with open(p, "rb") as f: | |
| d = pickle.load(f) | |
| return d if isinstance(d, dict) else {} | |
| except Exception: | |
| return {} | |
| def get_links(dt): | |
| if dt not in LINKS_CACHE: | |
| LINKS_CACHE[dt] = load_links(dt) | |
| return LINKS_CACHE[dt] | |
| def parse_cid_str(s): | |
| """'YYYYMMDDHHMM_id' β (dt:str, id:int). νμ λΆμΌμΉ μ (None,None)""" | |
| if not isinstance(s, str) or "_" not in s: | |
| return (None, None) | |
| dt, nid = s.split("_", 1) | |
| try: | |
| return (dt, int(nid)) | |
| except ValueError: | |
| return (None, None) | |
| def make_cid(dt, cid_int): | |
| return f"{dt}_{int(cid_int)}" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # μ΄κΈ° κ΅¬λ¦ μ‘°κ±΄ μ²΄ν¬ | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def is_outside_radar_region(dt, cid_int): | |
| """ꡬλ¦μ λͺ¨λ ν½μ μ΄ λ μ΄λ μμ λ°μ μλμ§ νμΈ""" | |
| label, _, radar_data = get_data(dt) | |
| if label is None or radar_data is None: | |
| return False | |
| mask = (label == cid_int) | |
| if not np.any(mask): | |
| return False | |
| # ν΄λ¬μ€ν° μμμ λ μ΄λ κ° μΆμΆ | |
| radar_values = radar_data[mask] | |
| valid_radar = radar_values[np.isfinite(radar_values)] | |
| # μ ν¨ν λ μ΄λ κ°μ΄ νλλ μμΌλ©΄ λ μ΄λ μμ λ° | |
| return len(valid_radar) == 0 | |
| def has_no_past_connection(dt, cid_int): | |
| """ν΄λΉ ꡬλ¦μ΄ κ³Όκ±°μ μ°κ²°μ΄ μλμ§ νμΈ (μ΅μ΄ μμ± κ΅¬λ¦)""" | |
| links = get_links(dt) | |
| cid_key = make_cid(dt, cid_int) | |
| if cid_key not in links: | |
| return True | |
| past_connections = links[cid_key] | |
| return not past_connections or len(past_connections) == 0 | |
| def get_cluster_size(dt, cid_int): | |
| """ν΄λ¬μ€ν°μ ν½μ μ λ°ν""" | |
| label, _, _ = get_data(dt) | |
| if label is None: | |
| return 0 | |
| mask = (label == cid_int) | |
| return np.sum(mask) | |
| def is_initial_cloud_to_remove(dt, cid_int): | |
| """μ κ±° λμ μ΄κΈ° ꡬλ¦μΈμ§ νμΈ""" | |
| if not has_no_past_connection(dt, cid_int): | |
| return False | |
| cluster_size = get_cluster_size(dt, cid_int) | |
| if is_outside_radar_region(dt, cid_int): | |
| return True | |
| if cluster_size >= MIN_SIZE_THRESHOLD: | |
| return True | |
| return False | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # μ±μ 쑰건 μ²΄ν¬ | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def is_mature(dt, cid_int): | |
| """μ±μ κ΅¬λ¦ μ‘°κ±΄ μ²΄ν¬ (λ¨μν)""" | |
| label, ir, radar_data = get_data(dt) | |
| if label is None or ir is None or radar_data is None: | |
| return False | |
| m = (label == cid_int) | |
| if not np.any(m): | |
| return False | |
| # 쑰건 1: BT 쑰건 μ²΄ν¬ | |
| v_ir = ir[m] | |
| v_ir = v_ir[np.isfinite(v_ir)] | |
| if len(v_ir) > 0 and np.nanmin(v_ir) <= MATURE_THRESHOLD: | |
| return True | |
| # 쑰건 2: λ μ΄λ μκ³κ° μ²΄ν¬ | |
| v_radar = radar_data[m] | |
| v_radar = v_radar[np.isfinite(v_radar)] | |
| if len(v_radar) > 0 and np.nanmax(v_radar) >= RADAR_THRESHOLD: | |
| return True | |
| return False | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # νμ(λ―Έλ) κ΅¬λ¦ μ°ΎκΈ° / μμ§ | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def successors(prev_dt, prev_id, next_dt): | |
| links_next = get_links(next_dt) | |
| if not links_next: | |
| return [] | |
| out = [] | |
| for now_key, prev_list in links_next.items(): | |
| now_dt, now_id = parse_cid_str(now_key) | |
| if now_dt is None or now_id is None: | |
| continue | |
| for pv in (prev_list or []): | |
| pv_dt, pv_id = parse_cid_str(pv) | |
| if pv_dt == prev_dt and pv_id == prev_id: | |
| out.append(now_id) | |
| break | |
| return out | |
| def check_mature_cloud(dt, cid_int): | |
| return is_mature(dt, cid_int) | |
| def collect_descendants_from(dt, cid_int, timesteps, max_minutes): | |
| out = set([make_cid(dt, cid_int)]) | |
| if dt not in timesteps: | |
| return out | |
| start_idx = timesteps.index(dt) | |
| last_idx = len(timesteps)-1 | |
| q = deque() | |
| q.append((start_idx, cid_int)) | |
| visited = set([(start_idx, cid_int)]) | |
| while q: | |
| idx, cur_id = q.popleft() | |
| cur_dt = timesteps[idx] | |
| out.add(make_cid(cur_dt, cur_id)) | |
| if idx == last_idx: | |
| continue | |
| next_dt = timesteps[idx+1] | |
| for nx_id in successors(cur_dt, cur_id, next_dt): | |
| st = (idx+1, nx_id) | |
| if st in visited: | |
| continue | |
| visited.add(st) | |
| q.append(st) | |
| return out | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # λ©μΈ | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def process_mature_cloud_masking(start_time=START_TIME, end_time=END_TIME): | |
| print(f"π κ΅¬λ¦ λ§μ€νΉ (λ¨μνλ 쑰건): {start_time}~{end_time}") | |
| timesteps = generate_timesteps(start_time, end_time, TIME_STEP) | |
| print(f"\nπ νμμ€ν : {len(timesteps)}κ°, μ κ±° λ²μ: {MAX_REMOVE_HOURS}h") | |
| print(f"π λ§μ€νΉ 쑰건:") | |
| print(f" 1. BT β€ {MATURE_THRESHOLD}K (μ΅μ΄ μμ λΆν° λ―Έλ μ 체)") | |
| print(f" 2. {RADAR_VAR.upper()} β₯ {RADAR_THRESHOLD}dBZ (μ΅μ΄ μμ λΆν° λ―Έλ μ 체)") | |
| print(f" 3. {RADAR_VAR.upper()} μμ λ° + κ³Όκ±° μ°κ²° μμ (μ 체 μμ μ£ΌκΈ°)") | |
| print(f" 4. {MIN_SIZE_THRESHOLD}ν½μ μ΄μ + κ³Όκ±° μ°κ²° μμ (μ 체 μμ μ£ΌκΈ°)") | |
| remove_cids = set() | |
| initial_remove_count = 0 | |
| mature_remove_count = 0 | |
| # 1) ν΅ν© κ΅¬λ¦ νν°λ§ (μ΄κΈ° + μ±μ) | |
| for dt in tqdm(timesteps, desc="κ΅¬λ¦ νν°λ§"): | |
| label, ir, radar_data = get_data(dt) | |
| # λΌλ²¨μ΄ μμ μμΌλ©΄ 건λλ | |
| if label is None: | |
| continue | |
| # λ°μ΄ν°(IR λλ HSR)κ° μμΌλ©΄ ν΄λΉ νμμ€ν μ "μλ‘μ΄ μ±μ κ΅¬λ¦ νλ³"μ ν¨μ€ | |
| # λ¨, λ€λ₯Έ νμμ€ν μμ μμ§λ remove_cids μΆμ μλ μν₯ μμ | |
| if ir is None or radar_data is None: | |
| continue | |
| for cid in safe_unique_labels(label): | |
| start_key = make_cid(dt, cid) | |
| if start_key in remove_cids: | |
| continue | |
| # 1) μ΄κΈ° κ΅¬λ¦ μ²΄ν¬ (μ°μ μμ) | |
| if is_initial_cloud_to_remove(dt, cid): | |
| descendants = collect_descendants_from(dt, cid, timesteps, MAX_REMOVE_HOURS*60) | |
| remove_cids |= descendants | |
| initial_remove_count += len(descendants) | |
| cluster_size = get_cluster_size(dt, cid) | |
| is_outside_radar = is_outside_radar_region(dt, cid) | |
| reason = f"{RADAR_VAR.upper()}μμλ°" if is_outside_radar else f"λνꡬλ¦({cluster_size}ν½μ )" | |
| # print(f" π μ΄κΈ° κ΅¬λ¦ μ κ±°: {dt}_{cid} ({reason}) β {len(descendants)}κ° νμ") | |
| continue | |
| # 2) μ±μ κ΅¬λ¦ μ²΄ν¬ | |
| if check_mature_cloud(dt, cid): | |
| mature_descendants = collect_descendants_from(dt, cid, timesteps, MAX_REMOVE_HOURS*60) | |
| remove_cids |= mature_descendants | |
| mature_remove_count += len(mature_descendants) | |
| # print(f" β‘ μ±μ κ΅¬λ¦ μ κ±°: {dt}_{cid} (μ±μ쑰건) β {len(mature_descendants)}κ° λ―Έλ ν΄λ¬μ€ν°") | |
| print(f"\nπ νν°λ§ κ²°κ³Ό:") | |
| print(f" - μ΄κΈ° κ΅¬λ¦ μ κ±°: {initial_remove_count}κ°") | |
| print(f" - μ±μ κ΅¬λ¦ μ κ±°: {mature_remove_count}κ°") | |
| print(f" - μ΄ μ κ±° λμ: {len(remove_cids)}κ°") | |
| # 2) λΌλ²¨ νμΌ μ λ°μ΄νΈ/μ μ₯ | |
| for dt in tqdm(timesteps, desc="λΌλ²¨ μ λ°μ΄νΈ"): | |
| label, _, _ = get_data(dt) | |
| # HSR λ°μ΄ν° λλ½ μ¬λΆμ μκ΄μμ΄ λΌλ²¨ νμΌμ΄ μ‘΄μ¬νλ©΄ λ§μ€νΉ μ§ν | |
| if label is None: | |
| continue | |
| # μ΄ μκ°μ μ κ±° λμ idλ€ μΆμΆ | |
| to_remove = [] | |
| for key in remove_cids: | |
| kdt, kid = parse_cid_str(key) | |
| if kdt == dt and kid is not None: | |
| to_remove.append(kid) | |
| new_label = label.astype(float, copy=True) | |
| removed = 0 | |
| for cid in to_remove: | |
| m = (label == cid) | |
| if np.any(m): | |
| new_label[m] = np.nan | |
| removed += 1 | |
| # λͺ¨λ μ κ±°λλ©΄ μ μ₯ μ€ν΅ | |
| keep = new_label[np.isfinite(new_label)] | |
| keep = keep[keep != 0] | |
| if keep.size == 0: | |
| continue | |
| day = dt[:8] | |
| out_dir = os.path.join(SAVE_DIR, day) | |
| os.makedirs(out_dir, exist_ok=True) | |
| out_path = os.path.join(out_dir, f"{dt}_label.nc") | |
| xr.Dataset({"label": (["y","x"], new_label)}).to_netcdf(out_path) | |
| # print(f" β {dt}: {removed}κ° μ κ±° β {out_path}") | |
| print("\nπ μλ£: λ¨μνλ κ΅¬λ¦ λ§μ€νΉ (μ¦μ 쑰건 + λ―Έλ νμ μ κ±°)") | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="Remove mature-cloud objects from CI labels.") | |
| parser.add_argument("--config", required=True, help="JSON configuration path") | |
| parser.add_argument("--device", default=None, help="Accepted for a common CLI; this stage runs on CPU") | |
| parser.add_argument("--output-dir", default=None, help="Override save_dir") | |
| args = parser.parse_args() | |
| config_path = os.path.abspath(args.config) | |
| RUN_DIR = os.path.dirname(config_path) | |
| cfg = load_config(config_path) | |
| if args.output_dir: | |
| cfg["output_dir"] = os.path.abspath(args.output_dir) | |
| apply_config(cfg) | |
| process_mature_cloud_masking(START_TIME, END_TIME) | |