Spaces:
Running
Running
File size: 10,047 Bytes
d3405a2 f521c52 d3405a2 f521c52 d3405a2 | 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 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | """3D Scroll: the fit's mesh as a compact point cloud, so the page can roll and unroll the scroll in the browser.
GET /api/mesh3d.bin?stride=N the binary below; N is 4, 8 or 16 (default 8) on the in-memory grid
GET /api/mesh3d.json?stride=N the same header as JSON, with the byte layout, for tools
The in-memory mesh keeps every XYZ_STRIDE-th node (2) of the 956 by 56,913 tifxyz grid; stride N keeps every N-th of
those, so a node is every 2N-th grid step (0.384 mm times N along the sheet). Only nodes whose x, y and z are all
valid are sent, in row-major order over the sampled grid. Built once per stride with numpy and kept in memory.
Layout of mesh3d.bin, little-endian throughout (HEADER_BYTES = 72, then four arrays, 72 + 13 n bytes in all):
offset type field
0 char[4] magic "P4S3"
4 uint16 version (1)
6 uint16 stride N
8 uint32 n, the number of nodes
12 uint32 rows of the sampled grid (before invalid nodes are dropped)
16 uint32 cols of the sampled grid
20 uint32 map width in px (142,283)
24 uint32 map height in px (2,390)
28 float32[3] bbox min x, y, z of the nodes, in 9.6 um scan voxels
40 float32[3] bbox max x, y, z
52 float32 map px per flat unit (2.5: one full mesh grid step)
56 float32 mm per map px (0.0768)
60 float32 mm per voxel (0.0096)
64 uint16 ink pyramid level k
66 uint16 ink window side in map px (2 ** (k + 1))
68 uint32 pad, 0
72 uint16[n][3] position x, y, z: voxel = min + q * (max - min) / 65535, per axis
72+6n uint16[n][2] flat position: full mesh grid column and row; map x = 2.5 col, map y = 2.5 row
72+10n int16[n] winding at the node (-1 no winding, -2 ambiguous)
72+12n uint8[n] ink: mean of the map pixels in a 2 ** (k + 1) px square centered on the node, from
pyramid level k, the level whose 2x2 window is at most the node spacing; 0 where the map
has no ink data (exactly 0 there, which the page draws as fine dots, not as low ink)
Scan axes: x and y across the scroll, z along it (up is high z, the top of the map). Map x runs from the outer end
(x = 0) to the inner end. An ink map is model output, not a reading.
"""
from __future__ import annotations
import math
import struct
import threading
import time
import numpy as np
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import JSONResponse, Response
class _Core:
"""app.py, looked up on use: app imports this module at its end, so a top-level import would be circular."""
def __getattr__(self, name):
import app
return getattr(app, name)
core = _Core()
router = APIRouter()
MAGIC = b"P4S3"
VERSION = 1
STRIDES = (4, 8, 16)
VOXEL_MM = 9.6e-3
HEADER = struct.Struct("<4sHHIIIII3f3ffffHHI")
HEADER_BYTES = HEADER.size # 72
HEADER_FIELDS = [ # name, struct code, offset: the same layout as the docstring, for tools that read the JSON
("magic", "4s", 0), ("version", "uint16", 4), ("stride", "uint16", 6), ("n", "uint32", 8), ("rows", "uint32", 12),
("cols", "uint32", 16), ("map_width", "uint32", 20), ("map_height", "uint32", 24),
("bbox_min_voxels", "float32[3]", 28), ("bbox_max_voxels", "float32[3]", 40), ("map_px_per_flat_unit", "float32", 52),
("mm_per_px", "float32", 56), ("mm_per_voxel", "float32", 60), ("ink_level", "uint16", 64),
("ink_window_px", "uint16", 66), ("pad", "uint32", 68)]
CACHE = {"Cache-Control": "public, max-age=86400"}
_cache: dict = {} # stride -> (source key, bytes, header dict)
_build_lock = threading.Lock()
def _source_key():
D = core.D
return id(D["X"]), D["X"].shape, id(D["ink"])
def build(stride: int = 8):
"""(bytes, header) for one stride; numpy only, a fraction of a second for the full Paris 4 grid."""
t0 = time.perf_counter()
D = core.D
xyz_stride, per_grid = core.XYZ_STRIDE, core.INK_PER_GRID
X, Y, Z = (D[k][::stride, ::stride] for k in "XYZ")
rows, cols = X.shape
ok = np.isfinite(X) & np.isfinite(Y) & np.isfinite(Z)
ii, jj = np.nonzero(ok) # row-major order
n = int(ii.size)
P = np.stack([X[ok], Y[ok], Z[ok]], 1).astype(np.float32)
if n:
lo, hi = P.min(0), P.max(0)
else:
lo = hi = np.zeros(3, np.float32)
rng = (hi - lo).astype(np.float32)
q = np.zeros((n, 3), np.uint16)
for a in range(3):
if rng[a] > 0:
q[:, a] = np.round((P[:, a] - lo[a]) / rng[a] * 65535.0).clip(0, 65535).astype(np.uint16)
# full mesh grid position of each node, which is also its flat position
gr = ii.astype(np.int64) * stride * xyz_stride
gc = jj.astype(np.int64) * stride * xyz_stride
if n and (gr.max() > 65535 or gc.max() > 65535):
raise HTTPException(500, "the mesh grid is too large for uint16 flat positions")
flat = np.stack([gc, gr], 1).astype(np.uint16)
g = D["wind"]
wind = g[np.minimum(gr, g.shape[0] - 1), np.minimum(gc, g.shape[1] - 1)].astype(np.int16)
# ink: a 2x2 window at pyramid level k centered on the node, no wider than the node spacing
spacing = stride * xyz_stride * per_grid
pyr = D["pyr"]
k = int(min(max(0, math.floor(math.log2(spacing)) - 1), len(pyr) - 1))
a = pyr[k]
h, w = a.shape
s = float(2 ** k)
mx, my = gc * per_grid, gr * per_grid
x0 = np.clip(np.floor(mx / s - 0.5).astype(np.int64), 0, max(w - 2, 0))
y0 = np.clip(np.floor(my / s - 0.5).astype(np.int64), 0, max(h - 2, 0))
x1, y1 = np.minimum(x0 + 1, w - 1), np.minimum(y0 + 1, h - 1)
tot = (a[y0, x0].astype(np.uint16) + a[y0, x1] + a[y1, x0] + a[y1, x1])
ink = ((tot + 2) // 4).astype(np.uint8)
W, H = int(D["W"]), int(D["H"])
head = HEADER.pack(MAGIC, VERSION, stride, n, rows, cols, W, H, *map(float, lo), *map(float, hi),
float(per_grid), float(core.MM_PER_PX), VOXEL_MM, k, 2 ** (k + 1), 0)
body = b"".join([head, q.astype("<u2").tobytes(), flat.astype("<u2").tobytes(), wind.astype("<i2").tobytes(),
ink.tobytes()])
lo32, hi32 = lo.astype(np.float32), hi.astype(np.float32)
off = HEADER_BYTES
layout = [{"name": "header", "dtype": "struct", "shape": [HEADER_BYTES], "offset": 0, "bytes": HEADER_BYTES}]
for name, dt, shape, size in (("position", "<u2", [n, 3], 6), ("flat", "<u2", [n, 2], 4),
("winding", "<i2", [n], 2), ("ink", "u1", [n], 1)):
layout.append({"name": name, "dtype": dt, "shape": shape, "offset": off, "bytes": size * n})
off += size * n
info = {
"format": MAGIC.decode(), "version": VERSION, "stride": stride, "grid_step": stride * xyz_stride,
"nodes": n, "rows": int(rows), "cols": int(cols), "bytes": len(body), "header_bytes": HEADER_BYTES,
"map": {"width": W, "height": H, "mm_per_px": core.MM_PER_PX},
"mm_per_voxel": VOXEL_MM, "node_spacing_mm": round(stride * xyz_stride * core.MM_PER_GRID, 4),
"bbox_voxels": {"min": [round(float(v), 3) for v in lo32], "max": [round(float(v), 3) for v in hi32]},
"bbox_mm": {"min": [round(float(v) * VOXEL_MM, 4) for v in lo32],
"max": [round(float(v) * VOXEL_MM, 4) for v in hi32]},
"position": {"dtype": "uint16", "axes": ["x", "y", "z"], "decode": "voxel = min + q * (max - min) / 65535",
"quantum_mm": [round(float(r) / 65535 * VOXEL_MM, 6) for r in (hi32 - lo32)]},
"flat": {"axes": ["grid column", "grid row"], "map_px_per_unit": per_grid,
"decode": "map x = 2.5 * column, map y = 2.5 * row"},
"winding": {"dtype": "int16", "no_winding": -1, "ambiguous": -2},
"ink": {"dtype": "uint8", "pyramid_level": k, "window_px": 2 ** (k + 1), "no_data": 0,
"no_data_nodes": int((ink == 0).sum())},
"header_fields": [{"name": a_, "type": t_, "offset": o_} for a_, t_, o_ in HEADER_FIELDS],
"layout": layout,
"axes": "scan x, y across the scroll, z along it; map x from the outer end (0) to the inner end",
"build_seconds": round(time.perf_counter() - t0, 3),
"note": "The fit's mesh, sampled; an ink map is model output, not a reading.",
}
return body, info
def get(stride: int = 8):
"""Cached (bytes, header); builds on first use under one of the app's heavy-work slots."""
if stride not in STRIDES:
raise HTTPException(400, f"stride must be one of {', '.join(map(str, STRIDES))}")
core.need()
key = _source_key()
hit = _cache.get(stride)
if hit and hit[0] == key:
return hit[1], hit[2]
with _build_lock:
hit = _cache.get(stride)
if hit and hit[0] == key:
return hit[1], hit[2]
with core.busy():
body, info = build(stride)
info["url"] = f"{core.SPACE_URL}/api/mesh3d.bin?stride={stride}"
_cache[stride] = (key, body, info)
return body, info
@router.get("/api/mesh3d.bin", tags=["map"])
def api_mesh3d_bin(stride: int = Query(8, description="4, 8 or 16 on the in-memory grid")):
"""The fit's mesh as a point cloud: quantized scan position, flat map position, winding and ink per node.
Layout: see /api/mesh3d.json or the module docstring of scroll3d.py."""
body, info = get(stride)
return Response(body, media_type="application/octet-stream",
headers=CACHE | {"X-Mesh3d-Nodes": str(info["nodes"]),
"Content-Disposition": f'inline; filename="paris4-mesh3d-s{stride}.bin"'})
@router.get("/api/mesh3d.json", tags=["map"])
def api_mesh3d_json(stride: int = Query(8, description="4, 8 or 16 on the in-memory grid")):
"""The header of /api/mesh3d.bin as JSON: counts, bbox in voxels and mm, scales and the byte layout."""
_, info = get(stride)
return JSONResponse(info, headers=CACHE)
|