Skip to content

Instantly share code, notes, and snippets.

@mithro
Created August 28, 2026 05:58
Show Gist options
  • Select an option

  • Save mithro/7a4520b69d42311e525a5d1e6e24af29 to your computer and use it in GitHub Desktop.

Select an option

Save mithro/7a4520b69d42311e525a5d1e6e24af29 to your computer and use it in GitHub Desktop.
pyuscope scan viewers: per-directory HTML grid, overlap view from uscan.json, and mosaic images
#!/usr/bin/env python3
"""Write tiles.html into every directory that holds cNNN_rNNN.jpg tiles, laying them out
in a table (rows = r, cols = c). Also writes ROOT/tiles.html linking to each page.
The page is deliberately NOT called index.html. These trees are published by
nginx with `autoindex on` (data.wafer.space), where a directory request goes
`try_files $uri/` -> index module -> index.html if it exists, and only falls
through to the generated file listing if it does not. An index.html here would
therefore replace the browsable listing of the scan tiles. Keep any name
outside the `index` set. overlap_html.py writes overlap.html for the same
reason.
Usage: grid_html.py [ROOT] [--width PX]
"""
import argparse, os, re
from html import escape
NAME_RE = re.compile(r"c(\d+)_r(\d+)\.jpe?g$", re.I)
def write_page(dirpath, files, width):
tiles = {}
for f in files:
m = NAME_RE.search(f)
if m:
tiles[(int(m.group(2)), int(m.group(1)))] = f # (row, col) -> filename
if not tiles:
return False
rows = sorted({r for r, _ in tiles}, reverse=True)
cols = sorted({c for _, c in tiles}, reverse=True)
html = [f"<!doctype html><title>{escape(os.path.basename(dirpath))}</title>",
f"<style>img{{width:{width}px;display:block}} td{{padding:0}} table{{border-spacing:4px}}</style>",
f"<h1>{escape(dirpath)}</h1><p>{len(tiles)} tiles, {len(rows)} rows x {len(cols)} cols</p>",
"<table><tr><th></th>" + "".join(f"<th>c{c}</th>" for c in cols) + "</tr>"]
for r in rows:
cells = []
for c in cols:
f = tiles.get((r, c))
cells.append(f'<td><a href="{f}"><img loading="lazy" src="{f}" title="c{c} r{r}"></a></td>' if f else "<td></td>")
html.append(f"<tr><th>r{r}</th>{''.join(cells)}</tr>")
html.append("</table>")
with open(os.path.join(dirpath, "tiles.html"), "w") as fh:
fh.write("\n".join(html))
return True
def main():
ap = argparse.ArgumentParser()
ap.add_argument("root", nargs="?", default=".")
ap.add_argument("--width", type=int, default=200, help="thumbnail width in px")
a = ap.parse_args()
pages = []
for dirpath, _, files in os.walk(a.root):
if write_page(dirpath, files, a.width):
pages.append(os.path.relpath(os.path.join(dirpath, "tiles.html"), a.root))
print("wrote", os.path.join(dirpath, "tiles.html"))
with open(os.path.join(a.root, "tiles.html"), "w") as fh:
fh.write("<!doctype html><title>scans</title><ul>" +
"".join(f'<li><a href="{p}">{escape(os.path.dirname(p))}</a></li>' for p in sorted(pages)) + "</ul>")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Compose one mosaic image per directory from cNNN_rNNN.jpg tiles (same layout as grid_html.py).
Usage: grid_png.py ROOT --out OUTDIR [--tile PX]
"""
import argparse, os, re
from multiprocessing import Pool
from PIL import Image
NAME_RE = re.compile(r"c(\d+)_r(\d+)\.jpe?g$", re.I)
def load(args):
path, tile = args
im = Image.open(path)
im.draft("RGB", (tile, tile)) # fast JPEG decode at reduced scale
return im.convert("RGB").resize((tile, tile), Image.BILINEAR)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("root")
ap.add_argument("--out", required=True)
ap.add_argument("--tile", type=int, default=64, help="pixels per tile in the mosaic")
a = ap.parse_args()
os.makedirs(a.out, exist_ok=True)
with Pool() as pool:
for dirpath, _, files in os.walk(a.root):
tiles = {}
for f in files:
m = NAME_RE.search(f)
if m:
tiles[(int(m.group(2)), int(m.group(1)))] = os.path.join(dirpath, f)
if not tiles:
continue
rows = sorted({r for r, _ in tiles}, reverse=True); cols = sorted({c for _, c in tiles}, reverse=True)
keys = sorted(tiles)
mosaic = Image.new("RGB", (len(cols) * a.tile, len(rows) * a.tile), "black")
for (r, c), im in zip(keys, pool.imap(load, [(tiles[k], a.tile) for k in keys], chunksize=32)):
mosaic.paste(im, (cols.index(c) * a.tile, rows.index(r) * a.tile))
out = os.path.join(a.out, os.path.basename(dirpath) + ".jpg")
mosaic.save(out, quality=90)
print(f"wrote {out} {mosaic.size[0]}x{mosaic.size[1]} ({len(tiles)} tiles)", flush=True)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Write overlap.html into every directory with a uscan.json, placing each tile at its
stage position so neighbouring images overlap as they were captured.
Usage: overlap_html.py [ROOT] [--scale F]
"""
import argparse, json, os
from html import escape
def write_page(dirpath, scale):
try:
with open(os.path.join(dirpath, "uscan.json")) as fh:
js = json.load(fh)
except FileNotFoundError:
return False
ax = js["points-xy3p"]["axes"]["x"]
px_per_mm = ax["pixels_per_mm"] * scale
size = ax["view_pixels"] * scale
files = {f: v["position"] for f, v in js["files"].items() if os.path.exists(os.path.join(dirpath, f))}
xs = [p["x"] for p in files.values()]; ys = [p["y"] for p in files.values()]
# same orientation as the table view: highest col at left, highest row at top
w = (max(xs) - min(xs)) * px_per_mm + size
h = (max(ys) - min(ys)) * px_per_mm + size
html = [f"<!doctype html><title>{escape(os.path.basename(dirpath))} (overlap)</title>",
"<style>#c{position:relative;background:#222} #c img{position:absolute}</style>",
f"<h1>{escape(dirpath)}</h1><p>{len(files)} tiles at scale {scale}, {round(w)} x {round(h)} px</p>",
f'<div id="c" style="width:{w:.0f}px;height:{h:.0f}px">']
for f, p in sorted(files.items()):
left = (max(xs) - p["x"]) * px_per_mm
top = (p["y"] - min(ys)) * px_per_mm
html.append(f'<img loading="lazy" src="{f}" title="{f} x={p["x"]:.3f} y={p["y"]:.3f}" '
f'style="left:{left:.1f}px;top:{top:.1f}px;width:{size:.1f}px;height:{size:.1f}px">')
html.append("</div>")
with open(os.path.join(dirpath, "overlap.html"), "w") as fh:
fh.write("\n".join(html))
return True
def main():
ap = argparse.ArgumentParser()
ap.add_argument("root", nargs="?", default=".")
ap.add_argument("--scale", type=float, default=0.25, help="tile scale factor (1.0 = full size)")
a = ap.parse_args()
pages = []
for dirpath, _, files in os.walk(a.root):
if any(f.lower().endswith(".jpg") for f in files):
if write_page(dirpath, a.scale):
pages.append(os.path.relpath(os.path.join(dirpath, "overlap.html"), a.root))
print("wrote", os.path.join(dirpath, "overlap.html"))
else:
print("skipped (no uscan.json)", dirpath)
with open(os.path.join(a.root, "overlap.html"), "w") as fh:
fh.write("<!doctype html><title>scans (overlap)</title><ul>" +
"".join(f'<li><a href="{p}">{escape(os.path.dirname(p))}</a></li>' for p in sorted(pages)) + "</ul>")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment