Spaces:
Runtime error
Runtime error
Download grounding/diff.py from KrishBh/satquery-backend: direct link, hf CLI and curl.
- Browser
- Download file 4.81 kB
-
https://huggingface.co/spaces/KrishBh/satquery-backend/resolve/main/grounding/diff.py
- Command line
-
hf download hf://spaces/KrishBh/satquery-backend/grounding/diff.py
-
curl -L -o diff.py https://huggingface.co/spaces/KrishBh/satquery-backend/resolve/main/grounding/diff.py
4.81 kB
| # -*- coding: utf-8 -*- | |
| """ | |
| change_detection/diff.py | |
| Compares two grounding JSON files (output of grounding_pipeline.py) for | |
| the same location's before/after images, and produces change.json | |
| matching the Day 5 contract: | |
| { | |
| "change_type": "flood" | "land_cover_change" | "no_change", | |
| "changed_tiles": [tile_id, tile_id, ...], | |
| "summary": { | |
| "changed_area_percent": float, | |
| "total_tiles": int, | |
| "changed_tile_count": int | |
| } | |
| } | |
| Usage: | |
| python diff.py before.json after.json --out change.json | |
| """ | |
| import argparse | |
| import json | |
| from flood_detector import classify_change_type | |
| def load_grounding_json(path: str) -> dict: | |
| with open(path, "r") as f: | |
| return json.load(f) | |
| def check_alignment(before: dict, after: dict): | |
| """ | |
| Sanity check: before/after must be the same grid over the same | |
| dimensions for a tile-by-tile diff to be valid. Tanaya's Day 5 job | |
| is to guarantee this — this check catches it if something slipped. | |
| """ | |
| if before["image_width"] != after["image_width"] or before["image_height"] != after["image_height"]: | |
| raise ValueError( | |
| f"Image dimensions don't match: before={before['image_width']}x{before['image_height']}, " | |
| f"after={after['image_width']}x{after['image_height']}. " | |
| "Before/after pair must be aligned to the same dimensions." | |
| ) | |
| if before["grid"] != after["grid"]: | |
| raise ValueError( | |
| f"Grid sizes don't match: before={before['grid']}, after={after['grid']}. " | |
| "Both images must be run through the pipeline with the same GRID_SIZE." | |
| ) | |
| if len(before["tiles"]) != len(after["tiles"]): | |
| raise ValueError( | |
| f"Tile counts don't match: before has {len(before['tiles'])}, " | |
| f"after has {len(after['tiles'])}." | |
| ) | |
| def diff_tiles(before: dict, after: dict) -> list: | |
| """ | |
| Compare tiles by tile_id and return details for every tile whose | |
| class changed between before and after. | |
| """ | |
| after_by_id = {t["tile_id"]: t for t in after["tiles"]} | |
| changed = [] | |
| for before_tile in before["tiles"]: | |
| tile_id = before_tile["tile_id"] | |
| after_tile = after_by_id.get(tile_id) | |
| if after_tile is None: | |
| # Shouldn't happen if check_alignment passed, but guard anyway. | |
| continue | |
| if before_tile["class"] != after_tile["class"]: | |
| changed.append({ | |
| "tile_id": tile_id, | |
| "before_class": before_tile["class"], | |
| "after_class": after_tile["class"], | |
| }) | |
| return changed | |
| def build_change_json(before: dict, after: dict) -> dict: | |
| check_alignment(before, after) | |
| changed_tile_details = diff_tiles(before, after) | |
| change_type = classify_change_type(changed_tile_details) | |
| total_tiles = len(before["tiles"]) | |
| changed_count = len(changed_tile_details) | |
| changed_area_percent = round((changed_count / total_tiles) * 100, 1) if total_tiles else 0.0 | |
| # Per Arka's confirmed format: changed_tiles is a plain array of tile_ids, | |
| # not full objects. Frontend looks up bbox from its own afterGroundingData. | |
| changed_tiles = [t["tile_id"] for t in changed_tile_details] | |
| return { | |
| "change_type": change_type, | |
| "changed_tiles": changed_tiles, | |
| "summary": { | |
| "changed_area_percent": changed_area_percent, | |
| "total_tiles": total_tiles, | |
| "changed_tile_count": changed_count, | |
| }, | |
| } | |
| def validate_output(result: dict): | |
| for key in ["change_type", "changed_tiles", "summary"]: | |
| assert key in result, f"Missing key: {key}" | |
| assert isinstance(result["changed_tiles"], list), "changed_tiles must be a list" | |
| for key in ["changed_area_percent", "total_tiles", "changed_tile_count"]: | |
| assert key in result["summary"], f"Missing summary key: {key}" | |
| print("Validation passed: change.json structure is correct.") | |
| def main(): | |
| parser = argparse.ArgumentParser(description="Diff two grounding JSON files and produce change.json") | |
| parser.add_argument("before_json", help="Path to the before image's grounding JSON") | |
| parser.add_argument("after_json", help="Path to the after image's grounding JSON") | |
| parser.add_argument("--out", default="change.json", help="Path to write the output change.json") | |
| args = parser.parse_args() | |
| before = load_grounding_json(args.before_json) | |
| after = load_grounding_json(args.after_json) | |
| result = build_change_json(before, after) | |
| validate_output(result) | |
| output_json = json.dumps(result, indent=2) | |
| print(output_json) | |
| with open(args.out, "w") as f: | |
| f.write(output_json) | |
| print(f"Written to {args.out}") | |
| if __name__ == "__main__": | |
| main() | |