File size: 6,969 Bytes
c368438 | 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 | """Export a rendered surface volume (zarr) to numbered layer TIFFs: 00.tif, 01.tif, ...
villa's `vc_render_tifxyz --zarr-output` and the team's published surface volumes are
zarr stores of shape (layers, height, width), uint8, either a bare array or an OME-Zarr
group whose full-resolution level is "0". v8in (YoussefMoNader/ink-8um-v8in) reads a
directory of numbered layer images instead, and uses the central 24 when there are more.
Chunks of these stores usually hold every layer (w045: 28 x 128 x 128), so reading one
layer decompresses all depths in each touched chunk. The export therefore reads all requested
layers in row bands into memory when they fit under max_bytes (w045: about 1.4 GB),
and falls back to one layer at a time otherwise. Depth order is kept as
stored; reverse it at inference time (v8in `--reverse`), not here, so one export serves both
directions. Needs numpy, tifffile and zarr (all in villa's environment).
The depth-shuffle control (after aviad12g's vesuvius-depth-order-control) is the exception:
`shuffle` writes the layers in a fixed random order, never the stored or the reversed one. A
model that reads ink loses it there; on PHerc0841 every released reader we tried fell to
about 0.5 AUC on shuffled layers, a cleaner null than the reversed volume, which keeps a
depth structure. `kit shuffle` writes a zarr (villa's input); `kit layers --shuffle` writes
v8in's layer folder in the same order.
"""
from pathlib import Path
from .verify import VerifyError, _numpy, missing
def _zarr():
try:
import zarr
except ImportError as exc:
raise missing("zarr") from exc
return zarr
def open_volume(path, level="0"):
"""Return the (layers, height, width) array of a surface-volume zarr."""
zarr = _zarr()
try:
node = zarr.open(str(path), mode="r")
except Exception as exc: # zarr raises several types for a missing or malformed store
raise VerifyError(f"cannot open {path} as zarr: {exc}") from exc
if not hasattr(node, "shape"):
if level not in node:
raise VerifyError(f"{path}: zarr group has no level {level!r}; levels: {sorted(node.keys())}")
node = node[level]
if len(node.shape) != 3:
raise VerifyError(f"{path}: expected (layers, height, width), got shape {node.shape}")
return node
def depth_permutation(n, seed):
"""A fixed random order of n layers that is neither the stored nor the reversed order."""
np = _numpy()
if n < 3:
raise VerifyError(f"a depth shuffle needs at least 3 layers, got {n}")
rng = np.random.default_rng(seed)
ident = list(range(n))
while True:
perm = [int(i) for i in rng.permutation(n)]
if perm != ident and perm != ident[::-1]:
return perm
DEFAULT_MAX_BYTES = 4 * 1024 ** 3
BAND_ROWS = 1024
def export_layers(volume, out_dir, start=0, count=None, compression="zlib", crop=None,
max_bytes=DEFAULT_MAX_BYTES, order=None):
"""Write layers start .. start+count-1 of volume as 00.tif, 01.tif, ... in out_dir, which must
not already hold numbered layer TIFFs.
crop is (y0, y1, x0, x1) in full-resolution pixels, or None for the whole layer. order, a
permutation of range(count), writes layer start+order[i] as file i (the shuffle control)."""
np = _numpy()
import tifffile
total = volume.shape[0]
if count is None:
count = total - start
if start < 0 or count < 1 or start + count > total:
raise VerifyError(f"layers {start}..{start + count - 1} are outside the volume's {total} layers")
h, w = volume.shape[1:]
y0, y1, x0, x1 = 0, h, 0, w
if crop is not None:
y0, y1, x0, x1 = crop
if not (0 <= y0 < y1 <= h and 0 <= x0 < x1 <= w):
raise VerifyError(f"crop {list(crop)} is outside the {h} x {w} layer")
region = (slice(y0, y1), slice(x0, x1))
if volume.dtype != np.uint8:
raise VerifyError(f"the volume is {volume.dtype}; v8in expects uint8 layers")
out = Path(out_dir)
if out.is_dir() and any(p.suffix == ".tif" and p.stem.isdigit() for p in out.iterdir()):
raise VerifyError(f"{out} already holds layer TIFFs from an earlier export, which v8in would read too; "
"remove them or give an empty folder")
stack = None
if count * (y1 - y0) * (x1 - x0) <= max_bytes:
stack = np.empty((count, y1 - y0, x1 - x0), dtype=np.uint8)
for band in range(y0, y1, BAND_ROWS):
end = min(band + BAND_ROWS, y1)
stack[:, band - y0:end - y0] = volume[start:start + count, band:end, x0:x1]
if order is None:
order = list(range(count))
elif sorted(order) != list(range(count)):
raise VerifyError(f"order must be a permutation of 0..{count - 1}")
out.mkdir(parents=True, exist_ok=True)
width = max(2, len(str(count - 1)))
written = []
for i, src in enumerate(order):
layer = stack[src] if stack is not None else np.asarray(volume[(start + src, *region)])
path = out / f"{i:0{width}d}.tif"
tifffile.imwrite(path, layer, compression=compression)
written.append(path)
return written
def export_file(src, out_dir, start=0, count=None, level="0", crop=None, max_bytes=DEFAULT_MAX_BYTES,
shuffle_seed=None):
volume = open_volume(src, level)
n = (volume.shape[0] - start) if count is None else count
order = None if shuffle_seed is None else depth_permutation(n, shuffle_seed)
written = export_layers(volume, out_dir, start, count, crop=crop, max_bytes=max_bytes, order=order)
return {"source": str(src), "shape": list(volume.shape), "start": start, "crop": crop,
"layers": len(written), "out_dir": str(out_dir), "order": order}
def shuffle_file(src, dst, seed, level="0", crop=None):
"""Write src's layers in a fixed random order to a new zarr (array "0"), for villa's reader."""
np = _numpy()
zarr = _zarr()
if Path(dst).exists():
raise VerifyError(f"{dst} already exists; remove it or give a new path for the shuffled zarr")
volume = open_volume(src, level)
n, h, w = volume.shape
y0, y1, x0, x1 = crop if crop is not None else (0, h, 0, w)
if not (0 <= y0 < y1 <= h and 0 <= x0 < x1 <= w):
raise VerifyError(f"crop {list(crop)} is outside the {h} x {w} layer")
order = depth_permutation(n, seed)
data = np.empty((n, y1 - y0, x1 - x0), dtype=volume.dtype)
for band in range(y0, y1, BAND_ROWS):
end = min(band + BAND_ROWS, y1)
data[:, band - y0:end - y0] = volume[:, band:end, x0:x1][order]
group = zarr.open_group(str(dst), mode="w-")
group.create_array("0", data=data, chunks=(n, min(128, y1 - y0), min(128, x1 - x0)))
group.attrs["depth_shuffle"] = {"source": str(src), "seed": seed, "order": order, "crop": crop}
return {"source": str(src), "out": str(dst), "shape": list(data.shape), "seed": seed, "order": order}
|