From 8399896861406c79daef69202a82d03d8e80db39 Mon Sep 17 00:00:00 2001 From: Val Redchenko Date: Wed, 8 Jul 2026 10:38:45 +0100 Subject: [PATCH] feat: serve motion-corrected micrograph preview image (#308) Add GET /micrographs/{uuid}/micrograph_image, the leaf of the spatial hierarchy that previously had no image endpoint (ADR 0021). Resolves in order: the pre-downscaled JPEG snapshot served as-is (zero render), then the motion-corrected MRC rendered to PNG with an optional x,y,w,h crop for zoom parity with atlas_image, else 404. Never reads high_res_path (the raw multi-GB movie stack). Returns 404 against today's data since the ingestion paths (#309) are still null; behaviour is covered by unit tests over the real routing and render path. --- src/smartem_backend/api_server.py | 35 +++++++++++ .../smartem_backend/test_micrograph_image.py | 58 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 tests/smartem_backend/test_micrograph_image.py diff --git a/src/smartem_backend/api_server.py b/src/smartem_backend/api_server.py index 0a00aeb8..c1800d65 100644 --- a/src/smartem_backend/api_server.py +++ b/src/smartem_backend/api_server.py @@ -2789,6 +2789,41 @@ async def get_gridsquare_image( return await _cached_image_response(Path(gridsquare.image_path), None) +@app.get( + "/micrographs/{micrograph_uuid}/micrograph_image", + responses={200: {"content": {"image/jpeg": {}, "image/png": {}}}}, +) +async def get_micrograph_image( + micrograph_uuid: str, + x: int | None = None, + y: int | None = None, + w: int | None = None, + h: int | None = None, + db: AsyncSession = DB_DEPENDENCY, +): + """Serve the motion-corrected micrograph preview image (ADR 0021). + + Prefers the pre-downscaled JPEG snapshot (served as-is, no render), falls back + to rendering the motion-corrected MRC to PNG with an optional x,y,w,h crop, and + otherwise 404s. Never serves high_res_path, which is the raw movie stack. + """ + micrograph = (await db.execute(select(Micrograph).where(Micrograph.uuid == micrograph_uuid))).scalars().first() + if not micrograph: + raise HTTPException(status_code=404, detail="Micrograph not found") + snapshot_path = micrograph.motion_corrected_snapshot_path + if snapshot_path and Path(snapshot_path).exists(): + return FileResponse( + snapshot_path, + media_type="image/jpeg", + headers={"Cache-Control": "private, max-age=3600"}, + ) + image_path = micrograph.motion_corrected_image_path + if image_path and Path(image_path).exists(): + crop = (x, y, w, h) if x is not None and y is not None and w is not None and h is not None else None + return await _cached_image_response(Path(image_path), crop) + raise HTTPException(status_code=404, detail="Micrograph image unavailable") + + _frontend_sse_connections = 0 _frontend_sse_max_connections = int(os.getenv("FRONTEND_SSE_MAX_CONNECTIONS", "50")) diff --git a/tests/smartem_backend/test_micrograph_image.py b/tests/smartem_backend/test_micrograph_image.py new file mode 100644 index 00000000..156f70ac --- /dev/null +++ b/tests/smartem_backend/test_micrograph_image.py @@ -0,0 +1,58 @@ +"""TestClient coverage for GET /micrographs/{uuid}/micrograph_image (ADR 0021, #308). + +Serves the motion-corrected preview: JPEG snapshot first, MRC render fallback, +else 404. Never touches high_res_path. +""" + +from .conftest import set_db_row + + +class TestMicrographImage: + def test_404_when_micrograph_missing(self, client): + set_db_row(client, None) + resp = client.get("/micrographs/missing/micrograph_image") + assert resp.status_code == 404 + assert resp.json()["detail"] == "Micrograph not found" + + def test_404_when_no_image_paths(self, client): + from smartem_backend.model.database import Micrograph + + set_db_row(client, Micrograph(uuid="mic-1")) + resp = client.get("/micrographs/mic-1/micrograph_image") + assert resp.status_code == 404 + assert resp.json()["detail"] == "Micrograph image unavailable" + + def test_serves_snapshot_and_prefers_it_over_mrc(self, client, tmp_path): + from smartem_backend.model.database import Micrograph + + snapshot = tmp_path / "snap.jpeg" + snapshot.write_bytes(b"\xff\xd8\xff\xe0jpeg-bytes") + set_db_row( + client, + Micrograph( + uuid="mic-1", + motion_corrected_snapshot_path=str(snapshot), + motion_corrected_image_path="/does/not/exist/mc.mrc", + ), + ) + resp = client.get("/micrographs/mic-1/micrograph_image") + assert resp.status_code == 200 + assert resp.headers["content-type"] == "image/jpeg" + assert resp.content == b"\xff\xd8\xff\xe0jpeg-bytes" + + def test_renders_mrc_when_snapshot_absent(self, client, tmp_path, monkeypatch): + import mrcfile + import numpy as np + + from smartem_backend import api_server + from smartem_backend.model.database import Micrograph + + monkeypatch.setattr(api_server, "IMAGE_CACHE_DIR", tmp_path / "cache") + source = tmp_path / "mc.mrc" + with mrcfile.new(str(source)) as mrc: + mrc.set_data(np.arange(16, dtype=np.float32).reshape(4, 4)) + set_db_row(client, Micrograph(uuid="mic-1", motion_corrected_image_path=str(source))) + resp = client.get("/micrographs/mic-1/micrograph_image") + assert resp.status_code == 200 + assert resp.headers["content-type"] == "image/png" + assert resp.content[:8] == b"\x89PNG\r\n\x1a\n"