SingleViewHeigthEstimation / tests /test_objects.py
akashch1512's picture
Add object detection and visualization features
b710eca
Raw History Blame Contribute Delete
5.38 kB
"""`infer/objects.py` on synthetic class + height maps whose answer is known.
No torch, no checkpoint: `pip install numpy scipy scikit-image pillow pytest`
and run `pytest -q` from the repo root.
"""
from __future__ import annotations
import json
import numpy as np
import pytest
from config import CLASS_NAMES
from infer.objects import (
BUILDING_ID, TREE_ID, WATER_ID, ObjectParams, extract_objects, summarise,
)
GSD = 0.5
GROUND_ID = CLASS_NAMES.index("ground")
def blank(h=120, w=160):
return np.full((h, w), GROUND_ID, np.uint8), np.zeros((h, w), np.float32)
def add_cone(seg, height, cy, cx, radius_px, top_m):
"""A conical crown: `top_m` at the centre, 0.5 * top at the rim."""
yy, xx = np.mgrid[:seg.shape[0], :seg.shape[1]]
d = np.hypot(yy - cy, xx - cx)
inside = d <= radius_px
seg[inside] = TREE_ID
cone = top_m * (1.0 - 0.5 * d / radius_px)
height[inside] = np.maximum(height[inside], cone[inside])
def test_three_separate_trees():
seg, h = blank()
truth = [(30, 30, 8, 12.0), (30, 110, 6, 7.0), (90, 70, 10, 18.0)]
for cy, cx, r, top in truth:
add_cone(seg, h, cy, cx, r, top)
obj = extract_objects(seg, h, GSD)
trees = obj["trees"]
assert len(trees) == 3
# sorted tallest first
assert [t["h"] for t in trees] == sorted((t["h"] for t in trees), reverse=True)
for (cy, cx, r, top), t in zip(sorted(truth, key=lambda t: -t[3]), trees):
assert t["h"] == pytest.approx(top, abs=0.6)
# corner-origin pixels: the centre of pixel (cy, cx) is (cx + 0.5, cy + 0.5)
assert t["x"] == pytest.approx(cx + 0.5, abs=1.0)
assert t["y"] == pytest.approx(cy + 0.5, abs=1.0)
assert t["r"] == pytest.approx(r * GSD, rel=0.2)
def test_touching_crowns_are_split():
seg, h = blank()
add_cone(seg, h, 60, 60, 12, 10.0)
add_cone(seg, h, 60, 80, 12, 11.0) # overlaps the first
trees = extract_objects(seg, h, GSD)["trees"]
assert len(trees) == 2
xs = sorted(t["x"] for t in trees)
assert xs[0] < 70.5 < xs[1]
def test_flat_topped_blob_is_still_one_tree():
seg, h = blank()
seg[40:60, 40:60] = TREE_ID
h[40:60, 40:60] = 6.0
trees = extract_objects(seg, h, GSD)["trees"]
assert len(trees) == 1
assert trees[0]["h"] == pytest.approx(6.0, abs=0.01)
def test_shrubs_below_min_height_are_not_trees():
seg, h = blank()
add_cone(seg, h, 60, 60, 10, 1.5)
assert extract_objects(seg, h, GSD)["trees"] == []
def test_building_footprint_and_height():
seg, h = blank()
seg[30:50, 10:40] = BUILDING_ID
h[30:50, 10:40] = 12.0
h[30, 10:40] = 3.0 # blurred roof edge, eroded away
obj = extract_objects(seg, h, GSD)
(b,) = obj["buildings"]
assert b["h"] == pytest.approx(12.0)
assert b["area_m2"] == pytest.approx(20 * 30 * GSD**2)
poly = np.array(b["poly"])
assert 4 <= len(poly) <= 8 # a rectangle, simplified
assert poly[:, 0].min() == pytest.approx(10, abs=0.6)
assert poly[:, 0].max() == pytest.approx(40, abs=0.6)
assert poly[:, 1].min() == pytest.approx(30, abs=0.6)
assert poly[:, 1].max() == pytest.approx(50, abs=0.6)
# clockwise on screen (y down) == positive shoelace
x, y = poly[:, 0], poly[:, 1]
assert np.sum(x * np.roll(y, -1) - np.roll(x, -1) * y) > 0
def test_flat_building_is_rejected():
seg, h = blank()
seg[30:50, 10:40] = BUILDING_ID
h[30:50, 10:40] = 0.8
assert extract_objects(seg, h, GSD)["buildings"] == []
def test_water_keeps_lakes_and_drops_puddles():
seg, h = blank()
seg[60:100, 60:120] = WATER_ID # 600 m^2
seg[10:14, 10:14] = WATER_ID # 4 m^2
water = extract_objects(seg, h, GSD)["water"]
assert len(water) == 1
assert water[0]["area_m2"] == pytest.approx(40 * 60 * GSD**2)
def test_caps_keep_the_tallest_and_flag_truncation():
seg, h = blank()
for cx, top in ((25, 5.0), (80, 9.0), (135, 7.0)):
add_cone(seg, h, 60, cx, 8, top)
obj = extract_objects(seg, h, GSD, ObjectParams(max_trees=2))
assert [round(t["h"]) for t in obj["trees"]] == [9, 7]
assert obj["truncated"]["trees"] is True
assert obj["counts"]["trees"] == 2
def test_contract_is_json_and_self_describing():
seg, h = blank()
add_cone(seg, h, 60, 60, 8, 9.0)
seg[10:30, 100:140] = BUILDING_ID
h[10:30, 100:140] = 7.0
obj = json.loads(json.dumps(extract_objects(seg, h, GSD)))
assert obj["version"] == 1
assert obj["grid"] == {**obj["grid"], "width": 160, "height": 120, "gsd_m": GSD}
assert obj["counts"] == {"trees": 1, "buildings": 1, "water": 0}
assert "1 trees" in summarise(obj)
def test_empty_scene_and_bad_input():
seg, h = blank()
obj = extract_objects(seg, h, GSD)
assert obj["counts"] == {"trees": 0, "buildings": 0, "water": 0}
with pytest.raises(ValueError):
extract_objects(seg, h[:10], GSD)
with pytest.raises(ValueError):
extract_objects(seg, h, 0)
def test_previews_render():
from viz.classes import class_preview, objects_overlay
seg, h = blank()
add_cone(seg, h, 60, 60, 8, 9.0)
rgb = np.full(seg.shape + (3,), 128, np.uint8)
assert class_preview(rgb, seg, CLASS_NAMES).size == (160, 120)
assert objects_overlay(rgb, extract_objects(seg, h, GSD)).size == (160, 120)