import json
|
import tempfile
|
import unittest
|
from pathlib import Path
|
import numpy as np
|
import rasterio
|
from rasterio.transform import from_origin
|
|
import importlib.util
|
|
|
def load_module():
|
path = Path(__file__).parents[1] / "run_change_detection.py"
|
spec = importlib.util.spec_from_file_location("change_detection_demo", path)
|
module = importlib.util.module_from_spec(spec)
|
assert spec and spec.loader
|
spec.loader.exec_module(module)
|
return module
|
|
|
class ChangeDetectionTests(unittest.TestCase):
|
def test_resize_pair_preserves_extent(self):
|
module = load_module()
|
before = np.zeros((100, 200, 3), dtype=np.uint8)
|
after = before.copy()
|
valid = np.ones((100, 200), dtype=bool)
|
_, _, resized_valid, details = module._resize_pair(before, after, valid, 64)
|
self.assertEqual((resized_valid.shape[0] % 32, resized_valid.shape[1] % 32), (0, 0))
|
self.assertEqual(details["original_width"], 200)
|
self.assertEqual(details["original_height"], 100)
|
|
def test_geotiff_mode_reads_crs_and_native_resolution(self):
|
module = load_module()
|
with tempfile.TemporaryDirectory() as directory:
|
root = Path(directory)
|
transform = from_origin(100, 200, 0.5, 0.5)
|
for name in ("before.tif", "after.tif"):
|
with rasterio.open(root / name, "w", driver="GTiff", width=96, height=64, count=3, dtype="uint8", crs="EPSG:3857", transform=transform) as dataset:
|
dataset.write(np.zeros((3, 64, 96), dtype=np.uint8))
|
image, info = module._read_rgb(root / "before.tif", "geotiff")
|
self.assertEqual(image.shape, (64, 96, 3))
|
self.assertEqual(info["crs"], "EPSG:3857")
|
before = np.zeros((64, 96, 3), dtype=np.uint8)
|
valid = np.ones((64, 96), dtype=bool)
|
_, _, _, details = module._resize_pair(before, before, valid, 0, native_resolution=True)
|
self.assertEqual((details["processed_width"], details["processed_height"]), (96, 64))
|
|
def test_geotiff_mode_rejects_missing_spatial_reference(self):
|
module = load_module()
|
with tempfile.TemporaryDirectory() as directory:
|
root = Path(directory)
|
with rasterio.open(root / "plain.tif", "w", driver="GTiff", width=32, height=32, count=3, dtype="uint8") as dataset:
|
dataset.write(np.zeros((3, 32, 32), dtype=np.uint8))
|
with self.assertRaisesRegex(ValueError, "no valid CRS"):
|
module._read_rgb(root / "plain.tif", "geotiff")
|
|
def test_geotiff_grid_alignment_resamples_only_processing_copy(self):
|
module = load_module()
|
with tempfile.TemporaryDirectory() as directory:
|
root = Path(directory)
|
before_transform = from_origin(100, 200, 1, 1)
|
after_transform = from_origin(100.25, 200.25, 1.01, 1.01)
|
for name, transform, width, height in (("before.tif", before_transform, 8, 6), ("after.tif", after_transform, 9, 7)):
|
with rasterio.open(root / name, "w", driver="GTiff", width=width, height=height, count=3, dtype="uint8", crs="EPSG:3857", transform=transform) as dataset:
|
dataset.write(np.full((3, height, width), 128, dtype=np.uint8))
|
before, before_info = module._read_rgb(root / "before.tif", "geotiff")
|
aligned = module._reproject_rgb_to_grid(root / "after.tif", before.shape[:2], before_info["transform"], before_info["crs"])
|
self.assertEqual(aligned.shape, before.shape)
|
with rasterio.open(root / "after.tif") as source:
|
self.assertEqual(source.width, 9)
|
self.assertEqual(source.height, 7)
|
|
def test_mismatched_inputs_fail_before_creating_outputs(self):
|
module = load_module()
|
with tempfile.TemporaryDirectory() as directory:
|
root = Path(directory)
|
from PIL import Image
|
Image.new("RGB", (32, 32), "black").save(root / "before.jpg")
|
Image.new("RGB", (16, 32), "black").save(root / "after.jpg")
|
with self.assertRaises(ValueError):
|
module.run_change_detection(root / "before.jpg", root / "after.jpg", root / "out")
|
self.assertFalse((root / "out").exists())
|
|
def test_threshold_is_bounded_before_creating_outputs(self):
|
module = load_module()
|
with tempfile.TemporaryDirectory() as directory:
|
root = Path(directory)
|
from PIL import Image
|
Image.new("RGB", (32, 32), "black").save(root / "before.jpg")
|
Image.new("RGB", (32, 32), "black").save(root / "after.jpg")
|
with self.assertRaises(ValueError):
|
module.run_change_detection(root / "before.jpg", root / "after.jpg", root / "out", threshold=1.0)
|
self.assertFalse((root / "out").exists())
|
|
def test_max_dimension_is_bounded_before_creating_outputs(self):
|
module = load_module()
|
with tempfile.TemporaryDirectory() as directory:
|
root = Path(directory)
|
from PIL import Image
|
Image.new("RGB", (32, 32), "black").save(root / "before.jpg")
|
Image.new("RGB", (32, 32), "black").save(root / "after.jpg")
|
with self.assertRaises(ValueError):
|
module.run_change_detection(root / "before.jpg", root / "after.jpg", root / "out", max_dimension=256)
|
self.assertFalse((root / "out").exists())
|
|
def test_empty_vector_is_written_as_feature_collection(self):
|
module = load_module()
|
with tempfile.TemporaryDirectory() as directory:
|
target = Path(directory) / "changes.geojson"
|
module._enrich_vector(target, [])
|
payload = json.loads(target.read_text(encoding="utf-8"))
|
self.assertEqual(payload, {"type": "FeatureCollection", "features": []})
|
|
def test_enriched_vector_does_not_claim_wgs84(self):
|
module = load_module()
|
with tempfile.TemporaryDirectory() as directory:
|
target = Path(directory) / "changes.geojson"
|
target.write_text(json.dumps({
|
"type": "FeatureCollection",
|
"features": [{
|
"type": "Feature",
|
"properties": {"confidence": 0.5, "class": 1},
|
"geometry": {"type": "Polygon", "coordinates": [[[0, 0], [2, 0], [2, 2], [0, 0]]]},
|
}],
|
}), encoding="utf-8")
|
details = [{"feature_id": 1, "area_pixels": 2.0, "mean_probability": 0.6, "max_probability": 0.8, "bounds_pixel": [0, 0, 2, 2]}]
|
module._enrich_vector(target, details)
|
payload = json.loads(target.read_text(encoding="utf-8"))
|
self.assertNotIn("crs", payload)
|
self.assertEqual(payload["features"][0]["properties"]["feature_id"], 1)
|
|
if __name__ == "__main__":
|
unittest.main()
|