File size: 5,913 Bytes
0127c21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
"""
Area estimation for a previously-grounded region — Phase 16, follow-up
chain hop 2 ("How much area?" after "Where exactly?").

Closes part of the gap flagged in PHASE15.md §2 ("What this does NOT
do... it does not chain area calculation... automatically") and
LEFT.md §3 ("Multi-step follow-up conversations... the rest of the
chain (area size, then radar cross-check) is not built yet").

HONESTY CONTRACT:
This is a geometric calculation over the PREVIOUS turn's bounding box —
not a new model call, not a segmentation of the actual object, and not a
guaranteed-accurate measurement. It only produces a real area figure
when the source image carries usable georeferencing (a CRS + bounds from
GeoTIFF metadata, in projected/metric units):

- PNG/JPEG benchmark images have no CRS at all -> area is reported as
  unavailable, never guessed.
- GeoTIFFs in a geographic (degree-based) CRS, e.g. plain WGS84
  (EPSG:4326) without a projection, would need proper reprojection to
  convert degrees to metres — this module deliberately does NOT do that
  (no pyproj dependency added for a hackathon follow-up feature) and
  instead reports area as unavailable rather than silently treating
  degrees as if they were metres, which would be badly wrong at most
  latitudes.
- Only GeoTIFFs in a recognisably projected/metric CRS (UTM zones, Web
  Mercator, a handful of common projected EPSG codes) get a real number,
  computed as: pixel-box footprint -> ground distance (using this
  image's own bounds/width/height as its ground-sample distance) ->
  rectangular area in km². This is the box's rectangular footprint, not
  a precise outline of the detected object.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Optional


@dataclass
class AreaResult:
    area_km2: Optional[float]
    area_note: str
    method: str


# Pragmatic prototype check on the CRS string only — not a full units
# lookup via a projections database. Good enough to catch the common
# "GeoTIFF in a UTM/Mercator-family projected CRS" case that matters for
# a hackathon demo; anything else is honestly reported as unavailable
# rather than guessed. State this limitation wherever this is surfaced.
_PROJECTED_CRS_HINTS = ("UTM", "3857", "MERCATOR", ":326", ":327", ":3035", ":5070", ":2163")
_GEOGRAPHIC_CRS_HINTS = ("4326", "WGS 84", "WGS84", "LONGLAT", "LONG/LAT", "GEOGCS")


def _looks_projected(crs: Optional[str]) -> Optional[bool]:
    """Returns True (projected/metric), False (geographic/degrees), or
    None (couldn't tell from the string — treated as unsupported)."""
    if not crs:
        return None
    c = crs.upper()
    if any(h in c for h in _GEOGRAPHIC_CRS_HINTS):
        return False
    if any(h in c for h in _PROJECTED_CRS_HINTS):
        return True
    return None


def estimate_box_area_km2(
    box: tuple[float, float, float, float],
    image_width: int,
    image_height: int,
    crs: Optional[str],
    bounds: Optional[tuple],
) -> AreaResult:
    """box is (x_min, y_min, x_max, y_max) in PIXEL coordinates on the
    same image the box was originally computed from — image_width/
    image_height/crs/bounds must all come from that same image's own
    upload metadata (api/upload.py's UploadResponse), never a different
    image, or the ground-sample distance will be silently wrong."""
    x_min, y_min, x_max, y_max = box

    if not crs or not bounds or image_width <= 0 or image_height <= 0:
        return AreaResult(
            area_km2=None,
            area_note=(
                "Area unavailable — the source image has no georeferencing metadata "
                "(no CRS/bounds), so pixel dimensions cannot be converted to a ground "
                "distance. This is expected for PNG/JPEG benchmark images."
            ),
            method="unavailable_no_georeferencing",
        )

    projected = _looks_projected(crs)
    if projected is not True:
        reason = (
            f"source CRS ({crs}) appears to be a geographic (degree-based) coordinate "
            "system, not a projected metric one"
            if projected is False
            else f"source CRS ({crs}) could not be confidently identified as projected/metric"
        )
        return AreaResult(
            area_km2=None,
            area_note=(
                f"Area unavailable — {reason}. Converting degrees to km\u00b2 without proper "
                "reprojection would silently misestimate area (the error varies with "
                "latitude), so this is intentionally left unreported rather than guessed. "
                "Reproject the source GeoTIFF to a projected CRS (e.g. the appropriate UTM "
                "zone) to get a real area figure here."
            ),
            method="unavailable_geographic_crs",
        )

    minx, miny, maxx, maxy = bounds
    px_width_m = abs(maxx - minx) / image_width
    px_height_m = abs(maxy - miny) / image_height

    box_width_px = abs(x_max - x_min)
    box_height_px = abs(y_max - y_min)

    box_width_m = box_width_px * px_width_m
    box_height_m = box_height_px * px_height_m
    area_km2 = (box_width_m * box_height_m) / 1_000_000.0

    return AreaResult(
        area_km2=round(area_km2, 4),
        area_note=(
            f"Approximate area from the previously grounded bounding box: "
            f"{box_width_m:.0f} m \u00d7 {box_height_m:.0f} m, using this image's own GeoTIFF "
            f"ground-sample distance ({px_width_m:.2f} m/px \u00d7 {px_height_m:.2f} m/px, derived "
            "from its bounds/width/height — not a hardcoded sensor resolution). This is the "
            "box's rectangular footprint, not a precise segmentation of the actual "
            "object/region — treat it as a rough order-of-magnitude estimate, not a "
            "validated measurement."
        ),
        method="bbox_pixel_to_ground_geotiff_gsd",
    )