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}