Sunday, October 18, 2026

 """Self-check for tests/photogrammetry_fixtures.py 

Run: pytest tests/test_photogrammetry_fixtures.py --no-cov

"""


import json

import urllib.request


import cv2

import numpy as np


from core.azure.survey_corners import track_from_srt

from tests.photogrammetry_fixtures import ( # noqa: F401 (fixtures are imported for pytest)

    encode_multipart,

    fake_nodeodm,

    frame_overlap_fraction,

    ground,

    ground_truth_orthomosaic,

    no_gps_tour,

    two_tours,

    union_bbox_px,

)



def _post(url, fields, files=()):

    body, content_type = encode_multipart(fields, list(files))

    request = urllib.request.Request(url, data=body, headers={"Content-Type": content_type}, method="POST")

    return json.load(urllib.request.urlopen(request))



def _get_json(url):

    return json.load(urllib.request.urlopen(url))



def test_tours_have_the_configured_overlap_and_share_area(ground, two_tours):

    a, b = two_tours

    assert len(a.frames) > 10 and len(b.frames) > 10

    # consecutive frames along a flight line overlap ~75 % (the default forward overlap)

    assert abs(frame_overlap_fraction(a.frames[0], a.frames[1]) - 0.75) < 0.05

    # the two tours overlap each other (shared flight lines), so one mosaic can be built from both

    assert any(frame_overlap_fraction(fa, fb) > 0.3 for fa in a.frames for fb in b.frames)

    x0, y0, w, h = union_bbox_px([a, b])

    assert ground_truth_orthomosaic(ground, [a, b]).shape[:2] == (h, w)



def test_tour_writes_frames_srt_and_manifest_that_survey_corners_can_parse(two_tours, no_gps_tour, tmp_path):

    a, _ = two_tours

    paths = a.write(tmp_path / "a")

    assert len(paths) == len(a.frames) and (tmp_path / "a" / "frame1.jpg").exists()

    manifest = json.loads((tmp_path / "a" / "manifest.json").read_text())

    assert [m["frame_id"] for m in manifest] == [f.frame_id for f in a.frames]


    track = track_from_srt(str(tmp_path / "a" / "tourA.SRT")) # the real parser used by /corners

    assert track is not None and len(track) == len(a.frames)

    assert np.ptp(track[:, 0]) > 50 # ~112 m east span


    no_gps_tour.write(tmp_path / "n")

    assert not list((tmp_path / "n").glob("*.SRT")) # no telemetry for the no-GPS tour

    assert all(m["lat"] is None for m in json.loads((tmp_path / "n" / "manifest.json").read_text()))



def test_frame_lat_lon_matches_its_pixel_position(ground, two_tours):

    f = two_tours[0].frames[0]

    gt = ground.geotransform()

    cx, cy = f.bbox_px[0] + f.bbox_px[2] / 2, f.bbox_px[1] + f.bbox_px[3] / 2

    assert abs(f.lat - (gt["origin_lat"] + cy * gt["lat_per_px"])) < 1e-8

    assert abs(f.lon - (gt["origin_lon"] + cx * gt["lon_per_px"])) < 1e-8



def test_fake_nodeodm_task_lifecycle(ground, two_tours, fake_nodeodm):

    a, b = two_tours

    x0, y0, w, h = union_bbox_px([a, b])

    options = json.dumps([{"name": "fast-orthophoto", "value": True}])

    task = _post(fake_nodeodm.url + "/task/new", {"name": "t", "options": options},

                 [("images", fr.filename, b"x") for fr in a.frames[:3]])["uuid"]

    assert fake_nodeodm.tasks[task].images == [fr.filename for fr in a.frames[:3]]


    codes = [_get_json(f"{fake_nodeodm.url}/task/{task}/info")["status"]["code"] for _ in range(4)]

    assert codes == [10, 20, 20, 40] # queued, running, running, completed


    png = urllib.request.urlopen(f"{fake_nodeodm.url}/task/{task}/download/orthophoto.tif").read()

    mosaic = cv2.imdecode(np.frombuffer(png, np.uint8), cv2.IMREAD_COLOR)

    assert mosaic.shape[:2] == (h, w) # the ground-truth orthomosaic

    shots = _get_json(f"{fake_nodeodm.url}/task/{task}/download/shots.geojson")

    assert len(shots["features"]) == len(a.frames) + len(b.frames)


    _post(fake_nodeodm.url + "/task/cancel", {"uuid": task})

    assert _get_json(f"{fake_nodeodm.url}/task/{task}/info")["status"]["code"] == 50 # cancelled



No comments:

Post a Comment