Spaces:
Running on Zero
Running on Zero
Commit Β·
7b5645e
1
Parent(s): 73d5259
support more formats
Browse files- _utils/image_io.py +93 -46
- app.py +58 -34
_utils/image_io.py
CHANGED
|
@@ -32,6 +32,31 @@ _CHANNEL = ("C", "S") # C = separate channel planes, S = interleaved RGB sa
|
|
| 32 |
_UNKNOWN = ("Q", "I") # file named no axis; only these may be *guessed* as channels
|
| 33 |
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
def _read_array(path):
|
| 36 |
"""Load an image file into (array, axes) where axes names each dimension.
|
| 37 |
|
|
@@ -40,23 +65,12 @@ def _read_array(path):
|
|
| 40 |
"8-bit Color", palette PNG/GIF) are expanded through their color lookup
|
| 41 |
table so we return true RGB, not bare indices.
|
| 42 |
"""
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
axes = str(series.axes)
|
| 50 |
-
is_palette = getattr(page, "photometric", None) == tifffile.PHOTOMETRIC.PALETTE
|
| 51 |
-
if is_palette and page.colormap is not None:
|
| 52 |
-
colormap = np.asarray(page.colormap) # (3, 2**bits), uint16
|
| 53 |
-
rgb = np.moveaxis(colormap[:, arr], 0, -1) # (..., H, W, 3)
|
| 54 |
-
# TIFF colormaps are 16-bit; scale down to 8-bit (65535/255=257).
|
| 55 |
-
arr = np.round(rgb / 257.0).astype(np.uint8)
|
| 56 |
-
axes = axes + "S" # the LUT added an RGB sample axis
|
| 57 |
-
return arr, axes
|
| 58 |
-
# Everything else (and if tifffile is unavailable): PIL. Expand palette
|
| 59 |
-
# images to RGB so the LUT is applied instead of returning bare indices.
|
| 60 |
img = Image.open(path)
|
| 61 |
if img.mode in ("P", "PA"):
|
| 62 |
img = img.convert("RGB")
|
|
@@ -66,8 +80,7 @@ def _read_array(path):
|
|
| 66 |
|
| 67 |
def _shape_axes(path):
|
| 68 |
"""Return (shape, axes) from metadata only - no pixel decode."""
|
| 69 |
-
|
| 70 |
-
if ext in (".tif", ".tiff") and tifffile is not None:
|
| 71 |
try:
|
| 72 |
with tifffile.TiffFile(path) as tif:
|
| 73 |
series = tif.series[0]
|
|
@@ -114,12 +127,17 @@ def _plan_axes(shape, axes):
|
|
| 114 |
return shape, axes, guessed
|
| 115 |
|
| 116 |
|
| 117 |
-
def _reduce_to_hwc(arr, axes, frame=0):
|
| 118 |
"""Collapse a labelled array to 2D (H, W) or 3D (H, W, C).
|
| 119 |
|
| 120 |
The channel axis (named by the file, or inferred by ``_plan_axes`` only when
|
| 121 |
-
the file names none) is moved last and kept whole.
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
"""
|
| 124 |
axes = list(axes)
|
| 125 |
|
|
@@ -141,27 +159,45 @@ def _reduce_to_hwc(arr, axes, frame=0):
|
|
| 141 |
axes.append(axes.pop(ci))
|
| 142 |
|
| 143 |
target = 3 if ci is not None else 2
|
| 144 |
-
|
| 145 |
while len(axes) > target:
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
axes.pop(0)
|
| 150 |
-
|
| 151 |
|
| 152 |
return arr
|
| 153 |
|
| 154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
class ImageInfo(NamedTuple):
|
| 156 |
"""How a file's dimensions were interpreted (read from metadata only).
|
| 157 |
|
| 158 |
-
frames
|
| 159 |
-
channels
|
| 160 |
-
axes
|
| 161 |
-
guessed
|
| 162 |
-
shape
|
| 163 |
-
width
|
| 164 |
-
height
|
|
|
|
|
|
|
|
|
|
| 165 |
"""
|
| 166 |
frames: int
|
| 167 |
channels: int
|
|
@@ -170,6 +206,9 @@ class ImageInfo(NamedTuple):
|
|
| 170 |
shape: tuple
|
| 171 |
width: int
|
| 172 |
height: int
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
|
| 175 |
def inspect_image(path):
|
|
@@ -179,8 +218,13 @@ def inspect_image(path):
|
|
| 179 |
planned_shape, planned_axes, guessed = _plan_axes(shape, axes)
|
| 180 |
has_c = bool(planned_axes) and planned_axes[-1] in _CHANNEL
|
| 181 |
channels = int(planned_shape[-1]) if has_c else 1
|
| 182 |
-
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
# Spatial size comes from the named Y/X axes, so it stays correct
|
| 186 |
# whatever order the other axes are in.
|
|
@@ -193,7 +237,8 @@ def inspect_image(path):
|
|
| 193 |
if not (width and height): # no Y/X named; fall back to the header probe
|
| 194 |
width, height = image_size(path)
|
| 195 |
|
| 196 |
-
return ImageInfo(frames, channels, str(axes), guessed, tuple(shape),
|
|
|
|
| 197 |
except Exception: # noqa: BLE001
|
| 198 |
return ImageInfo(1, 1, "", False, (), 0, 0)
|
| 199 |
|
|
@@ -209,8 +254,7 @@ def array_nbytes(path):
|
|
| 209 |
Computed from shape + dtype in the header - no pixel decode - so it is safe
|
| 210 |
to call on a file that is too large to open. Returns 0 if unknown.
|
| 211 |
"""
|
| 212 |
-
|
| 213 |
-
if ext in (".tif", ".tiff") and tifffile is not None:
|
| 214 |
try:
|
| 215 |
with tifffile.TiffFile(path) as tif:
|
| 216 |
series = tif.series[0]
|
|
@@ -232,8 +276,7 @@ def image_size(path):
|
|
| 232 |
that would be too large to load. Returns (0, 0) if the size cannot be
|
| 233 |
determined, so callers treat it as "unknown" and proceed.
|
| 234 |
"""
|
| 235 |
-
|
| 236 |
-
if ext in (".tif", ".tiff") and tifffile is not None:
|
| 237 |
try:
|
| 238 |
with tifffile.TiffFile(path) as tif:
|
| 239 |
page = tif.pages[0]
|
|
@@ -261,10 +304,10 @@ def _to_rgb(arr):
|
|
| 261 |
return arr[:, :, :3]
|
| 262 |
|
| 263 |
|
| 264 |
-
def _load_rgb(path, frame=0):
|
| 265 |
"""Read a file and reduce it to an (H, W, 3) array plus its source dtype."""
|
| 266 |
raw, axes = _read_array(path)
|
| 267 |
-
arr = _reduce_to_hwc(raw, axes, frame=frame)
|
| 268 |
arr = _to_rgb(arr)
|
| 269 |
return arr, raw.dtype
|
| 270 |
|
|
@@ -306,7 +349,7 @@ def _save_png(arr, out_path=None, out_dir=None, base=None, suffix="_std.png"):
|
|
| 306 |
return out_path
|
| 307 |
|
| 308 |
|
| 309 |
-
def standardize_image(path, frame=0, out_dir=None, out_path=None):
|
| 310 |
"""Standardize any image/TIFF to a canonical 8-bit RGB PNG.
|
| 311 |
|
| 312 |
Used for both the preview and the model: one frame, <=3 channels, with a
|
|
@@ -315,7 +358,11 @@ def standardize_image(path, frame=0, out_dir=None, out_path=None):
|
|
| 315 |
|
| 316 |
Args:
|
| 317 |
path: path to the input image (TIFF, PNG, JPG, ...).
|
| 318 |
-
frame: which frame of
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
out_dir: optional directory for the output PNG.
|
| 320 |
out_path: optional explicit output file path (overrides out_dir).
|
| 321 |
|
|
@@ -324,7 +371,7 @@ def standardize_image(path, frame=0, out_dir=None, out_path=None):
|
|
| 324 |
returned unchanged so callers degrade gracefully.
|
| 325 |
"""
|
| 326 |
try:
|
| 327 |
-
arr, dtype = _load_rgb(path, frame=frame)
|
| 328 |
arr = _to_uint8(arr, stretch=(dtype != np.uint8))
|
| 329 |
base = os.path.splitext(os.path.basename(path))[0]
|
| 330 |
return _save_png(arr, out_path=out_path, out_dir=out_dir, base=base, suffix="_std.png")
|
|
|
|
| 32 |
_UNKNOWN = ("Q", "I") # file named no axis; only these may be *guessed* as channels
|
| 33 |
|
| 34 |
|
| 35 |
+
TIFF_EXTENSIONS = (
|
| 36 |
+
sorted("." + e for e in tifffile.TIFF.FILE_EXTENSIONS if "." not in e)
|
| 37 |
+
if tifffile is not None else [".tif", ".tiff"]
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _read_tiff(path):
|
| 42 |
+
"""Read a TIFF-family file into (array, axes). Raises if not readable."""
|
| 43 |
+
with tifffile.TiffFile(path) as tif:
|
| 44 |
+
series = tif.series[0]
|
| 45 |
+
page = tif.pages[0]
|
| 46 |
+
arr = np.asarray(series.asarray())
|
| 47 |
+
axes = str(series.axes)
|
| 48 |
+
|
| 49 |
+
is_palette = (getattr(page, "photometric", None) == tifffile.PHOTOMETRIC.PALETTE
|
| 50 |
+
and not any(a in _CHANNEL for a in axes))
|
| 51 |
+
if is_palette and page.colormap is not None:
|
| 52 |
+
colormap = np.asarray(page.colormap) # (3, 2**bits), uint16
|
| 53 |
+
rgb = np.moveaxis(colormap[:, arr], 0, -1) # (..., H, W, 3)
|
| 54 |
+
# TIFF colormaps are 16-bit; scale down to 8-bit (65535/255=257).
|
| 55 |
+
arr = np.round(rgb / 257.0).astype(np.uint8)
|
| 56 |
+
axes = axes + "S" # the LUT added an RGB sample axis
|
| 57 |
+
return arr, axes
|
| 58 |
+
|
| 59 |
+
|
| 60 |
def _read_array(path):
|
| 61 |
"""Load an image file into (array, axes) where axes names each dimension.
|
| 62 |
|
|
|
|
| 65 |
"8-bit Color", palette PNG/GIF) are expanded through their color lookup
|
| 66 |
table so we return true RGB, not bare indices.
|
| 67 |
"""
|
| 68 |
+
if tifffile is not None:
|
| 69 |
+
try:
|
| 70 |
+
return _read_tiff(path)
|
| 71 |
+
except Exception: # noqa: BLE001 - not a TIFF, or tifffile cannot parse it
|
| 72 |
+
pass
|
| 73 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
img = Image.open(path)
|
| 75 |
if img.mode in ("P", "PA"):
|
| 76 |
img = img.convert("RGB")
|
|
|
|
| 80 |
|
| 81 |
def _shape_axes(path):
|
| 82 |
"""Return (shape, axes) from metadata only - no pixel decode."""
|
| 83 |
+
if tifffile is not None:
|
|
|
|
| 84 |
try:
|
| 85 |
with tifffile.TiffFile(path) as tif:
|
| 86 |
series = tif.series[0]
|
|
|
|
| 127 |
return shape, axes, guessed
|
| 128 |
|
| 129 |
|
| 130 |
+
def _reduce_to_hwc(arr, axes, frame=0, sub_frame=None):
|
| 131 |
"""Collapse a labelled array to 2D (H, W) or 3D (H, W, C).
|
| 132 |
|
| 133 |
The channel axis (named by the file, or inferred by ``_plan_axes`` only when
|
| 134 |
+
the file names none) is moved last and kept whole. Stack axes (Z / time /
|
| 135 |
+
page) are handled in order:
|
| 136 |
+
* the first is indexed by ``frame`` (the frame slider);
|
| 137 |
+
* the second is indexed by ``sub_frame`` if given, else collapsed by a
|
| 138 |
+
maximum-intensity projection (the natural default for a focal stack -
|
| 139 |
+
keeps the brightest signal across planes rather than an arbitrary one);
|
| 140 |
+
* any deeper ones are projected too.
|
| 141 |
"""
|
| 142 |
axes = list(axes)
|
| 143 |
|
|
|
|
| 159 |
axes.append(axes.pop(ci))
|
| 160 |
|
| 161 |
target = 3 if ci is not None else 2
|
| 162 |
+
stack = 0
|
| 163 |
while len(axes) > target:
|
| 164 |
+
if stack == 0:
|
| 165 |
+
idx = max(0, min(int(frame), arr.shape[0] - 1))
|
| 166 |
+
arr = arr[idx]
|
| 167 |
+
elif stack == 1 and sub_frame is not None:
|
| 168 |
+
idx = max(0, min(int(sub_frame), arr.shape[0] - 1))
|
| 169 |
+
arr = arr[idx]
|
| 170 |
+
else:
|
| 171 |
+
arr = arr.max(axis=0) # maximum-intensity projection over this axis
|
| 172 |
axes.pop(0)
|
| 173 |
+
stack += 1
|
| 174 |
|
| 175 |
return arr
|
| 176 |
|
| 177 |
|
| 178 |
+
# tifffile axis letters -> words a microscopist uses, for messages and slider
|
| 179 |
+
# labels. Anything unmapped (an unnamed 'Q'/'I' axis, a raw page axis) is just a
|
| 180 |
+
# "frame".
|
| 181 |
+
_AXIS_LABELS = {"T": "timepoint", "Z": "z-plane"}
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def _axis_label(a):
|
| 185 |
+
return _AXIS_LABELS.get(a, "frame")
|
| 186 |
+
|
| 187 |
+
|
| 188 |
class ImageInfo(NamedTuple):
|
| 189 |
"""How a file's dimensions were interpreted (read from metadata only).
|
| 190 |
|
| 191 |
+
frames - length of the first stack (Z/T) axis, 1 if not a stack
|
| 192 |
+
channels - length of the channel axis, 1 if single-channel
|
| 193 |
+
axes - the file's own axes string, e.g. 'CZYX' ('Q' = unnamed)
|
| 194 |
+
guessed - True if the channel axis was inferred rather than read
|
| 195 |
+
shape - the file's raw shape as stored, e.g. (1, 4, 1, 1024, 1024)
|
| 196 |
+
width - pixels along the X axis (0 if unknown)
|
| 197 |
+
height - pixels along the Y axis (0 if unknown)
|
| 198 |
+
sub_frames- length of a *second* stack axis (e.g. Z in a time+Z file), 1 if
|
| 199 |
+
there is only one stack axis
|
| 200 |
+
frame_label / sub_label - the words for those two axes ('timepoint', ...)
|
| 201 |
"""
|
| 202 |
frames: int
|
| 203 |
channels: int
|
|
|
|
| 206 |
shape: tuple
|
| 207 |
width: int
|
| 208 |
height: int
|
| 209 |
+
sub_frames: int = 1
|
| 210 |
+
frame_label: str = "frame"
|
| 211 |
+
sub_label: str = "frame"
|
| 212 |
|
| 213 |
|
| 214 |
def inspect_image(path):
|
|
|
|
| 218 |
planned_shape, planned_axes, guessed = _plan_axes(shape, axes)
|
| 219 |
has_c = bool(planned_axes) and planned_axes[-1] in _CHANNEL
|
| 220 |
channels = int(planned_shape[-1]) if has_c else 1
|
| 221 |
+
|
| 222 |
+
stack = [(a, d) for a, d in zip(planned_axes, planned_shape)
|
| 223 |
+
if a not in ("Y", "X") and a not in _CHANNEL]
|
| 224 |
+
frames = int(stack[0][1]) if stack else 1
|
| 225 |
+
frame_label = _axis_label(stack[0][0]) if stack else "frame"
|
| 226 |
+
sub_frames = int(stack[1][1]) if len(stack) > 1 else 1
|
| 227 |
+
sub_label = _axis_label(stack[1][0]) if len(stack) > 1 else "frame"
|
| 228 |
|
| 229 |
# Spatial size comes from the named Y/X axes, so it stays correct
|
| 230 |
# whatever order the other axes are in.
|
|
|
|
| 237 |
if not (width and height): # no Y/X named; fall back to the header probe
|
| 238 |
width, height = image_size(path)
|
| 239 |
|
| 240 |
+
return ImageInfo(frames, channels, str(axes), guessed, tuple(shape),
|
| 241 |
+
width, height, sub_frames, frame_label, sub_label)
|
| 242 |
except Exception: # noqa: BLE001
|
| 243 |
return ImageInfo(1, 1, "", False, (), 0, 0)
|
| 244 |
|
|
|
|
| 254 |
Computed from shape + dtype in the header - no pixel decode - so it is safe
|
| 255 |
to call on a file that is too large to open. Returns 0 if unknown.
|
| 256 |
"""
|
| 257 |
+
if tifffile is not None:
|
|
|
|
| 258 |
try:
|
| 259 |
with tifffile.TiffFile(path) as tif:
|
| 260 |
series = tif.series[0]
|
|
|
|
| 276 |
that would be too large to load. Returns (0, 0) if the size cannot be
|
| 277 |
determined, so callers treat it as "unknown" and proceed.
|
| 278 |
"""
|
| 279 |
+
if tifffile is not None:
|
|
|
|
| 280 |
try:
|
| 281 |
with tifffile.TiffFile(path) as tif:
|
| 282 |
page = tif.pages[0]
|
|
|
|
| 304 |
return arr[:, :, :3]
|
| 305 |
|
| 306 |
|
| 307 |
+
def _load_rgb(path, frame=0, sub_frame=None):
|
| 308 |
"""Read a file and reduce it to an (H, W, 3) array plus its source dtype."""
|
| 309 |
raw, axes = _read_array(path)
|
| 310 |
+
arr = _reduce_to_hwc(raw, axes, frame=frame, sub_frame=sub_frame)
|
| 311 |
arr = _to_rgb(arr)
|
| 312 |
return arr, raw.dtype
|
| 313 |
|
|
|
|
| 349 |
return out_path
|
| 350 |
|
| 351 |
|
| 352 |
+
def standardize_image(path, frame=0, sub_frame=None, out_dir=None, out_path=None):
|
| 353 |
"""Standardize any image/TIFF to a canonical 8-bit RGB PNG.
|
| 354 |
|
| 355 |
Used for both the preview and the model: one frame, <=3 channels, with a
|
|
|
|
| 358 |
|
| 359 |
Args:
|
| 360 |
path: path to the input image (TIFF, PNG, JPG, ...).
|
| 361 |
+
frame: which frame of the first stack axis to use (0-based; ignored for
|
| 362 |
+
non-stacks).
|
| 363 |
+
sub_frame: which plane of a second stack axis (e.g. Z in a time+Z file)
|
| 364 |
+
to use (0-based). None means combine those planes by a maximum-
|
| 365 |
+
intensity projection.
|
| 366 |
out_dir: optional directory for the output PNG.
|
| 367 |
out_path: optional explicit output file path (overrides out_dir).
|
| 368 |
|
|
|
|
| 371 |
returned unchanged so callers degrade gracefully.
|
| 372 |
"""
|
| 373 |
try:
|
| 374 |
+
arr, dtype = _load_rgb(path, frame=frame, sub_frame=sub_frame)
|
| 375 |
arr = _to_uint8(arr, stretch=(dtype != np.uint8))
|
| 376 |
base = os.path.splitext(os.path.basename(path))[0]
|
| 377 |
return _save_png(arr, out_path=out_path, out_dir=out_dir, base=base, suffix="_std.png")
|
app.py
CHANGED
|
@@ -23,7 +23,7 @@ from inference_count import load_model as load_count_model, run as run_count
|
|
| 23 |
from inference_track import load_model as load_track_model, run as run_track
|
| 24 |
from _utils.image_io import (
|
| 25 |
standardize_image, inspect_image, image_size, array_nbytes,
|
| 26 |
-
RECOMMENDED_SIZE, WARN_SIZE, MAX_SIZE, MAX_READ_BYTES,
|
| 27 |
)
|
| 28 |
|
| 29 |
HF_TOKEN = os.getenv("HF_TOKEN")
|
|
@@ -408,6 +408,8 @@ def _describe_read(info):
|
|
| 408 |
|
| 409 |
if info.frames > 1:
|
| 410 |
lines.append(f"Showing first frame by default (you can use the stack frame slider to pick another).")
|
|
|
|
|
|
|
| 411 |
|
| 412 |
lines.append(
|
| 413 |
"If wrong, please convert your image to a 8-bit RGB/Grayscale image file before uploading (there are available tools such as ImageJ/Fiji)."
|
|
@@ -417,26 +419,30 @@ def _describe_read(info):
|
|
| 417 |
|
| 418 |
def prepare_uploaded_image(annot_value):
|
| 419 |
"""On annotator upload: standardize frame 0 for preview and configure the
|
| 420 |
-
stack
|
| 421 |
|
| 422 |
Browsers cannot render 16-bit / float / multi-page TIFFs, so we standardize
|
| 423 |
to an 8-bit RGB PNG. If the file is a stack, notify the user and reveal a
|
| 424 |
-
frame slider (default frame 1);
|
|
|
|
|
|
|
| 425 |
|
| 426 |
-
Returns (annotator_value, raw_path, frame_slider_update).
|
| 427 |
"""
|
|
|
|
| 428 |
img_path = _annot_path(annot_value)
|
| 429 |
if not img_path:
|
| 430 |
-
return annot_value, None,
|
| 431 |
|
| 432 |
# Clear the annotator on refusal, so a rejected file cannot reach inference.
|
| 433 |
rejected = check_image(img_path)
|
| 434 |
if rejected:
|
| 435 |
gr.Warning(rejected, duration=None, title="β Cannot use this file")
|
| 436 |
-
return None, None,
|
| 437 |
|
| 438 |
info = inspect_image(img_path)
|
| 439 |
-
display = standardize_image(img_path, frame=0
|
|
|
|
| 440 |
|
| 441 |
# Stay quiet unless something non-obvious happened; a plain RGB or grayscale
|
| 442 |
# image needs no explanation.
|
|
@@ -444,15 +450,19 @@ def prepare_uploaded_image(annot_value):
|
|
| 444 |
gr.Info(_describe_read(info), duration=None, title="π Image Loading Info")
|
| 445 |
|
| 446 |
slider = (gr.update(visible=True, maximum=info.frames, value=1) if info.frames > 1
|
| 447 |
-
else
|
| 448 |
-
|
|
|
|
|
|
|
|
|
|
| 449 |
|
| 450 |
|
| 451 |
-
def select_frame(raw_path, frame_num):
|
| 452 |
-
"""Re-render the annotator preview for the chosen
|
|
|
|
| 453 |
if not raw_path:
|
| 454 |
return gr.update()
|
| 455 |
-
return standardize_image(raw_path, frame=int(frame_num) - 1)
|
| 456 |
|
| 457 |
|
| 458 |
@spaces.GPU
|
|
@@ -1470,6 +1480,11 @@ with gr.Blocks(
|
|
| 1470 |
label="π Stack frame (choose frame to use)",
|
| 1471 |
visible=False,
|
| 1472 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1473 |
|
| 1474 |
# Example Images Gallery
|
| 1475 |
gr.Markdown("π Example Image Gallery", elem_classes="frame-heading")
|
|
@@ -1497,7 +1512,7 @@ with gr.Blocks(
|
|
| 1497 |
gr.Markdown("β Upload New Example Image to Gallery", elem_classes="frame-heading")
|
| 1498 |
image_uploader = gr.File(
|
| 1499 |
show_label=False,
|
| 1500 |
-
file_types=["image"
|
| 1501 |
type="filepath"
|
| 1502 |
)
|
| 1503 |
add_to_gallery_btn = gr.Button("β Add to Gallery", variant="secondary", size="sm")
|
|
@@ -1555,13 +1570,14 @@ with gr.Blocks(
|
|
| 1555 |
annotator.upload(
|
| 1556 |
fn=prepare_uploaded_image,
|
| 1557 |
inputs=annotator,
|
| 1558 |
-
outputs=[annotator, seg_raw_state, seg_frame_slider]
|
| 1559 |
-
)
|
| 1560 |
-
seg_frame_slider.release(
|
| 1561 |
-
fn=select_frame,
|
| 1562 |
-
inputs=[seg_raw_state, seg_frame_slider],
|
| 1563 |
-
outputs=annotator
|
| 1564 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1565 |
|
| 1566 |
# click event for segmentation
|
| 1567 |
run_seg_btn.click(
|
|
@@ -1577,9 +1593,10 @@ with gr.Blocks(
|
|
| 1577 |
|
| 1578 |
# click event for clear button
|
| 1579 |
clear_btn.click(
|
| 1580 |
-
fn=lambda: (None, {}, None, gr.update(visible=False, value=1)
|
|
|
|
| 1581 |
inputs=None,
|
| 1582 |
-
outputs=[annotator, seg_vis_state, seg_raw_state, seg_frame_slider]
|
| 1583 |
)
|
| 1584 |
|
| 1585 |
# init Gallery with example images
|
|
@@ -1621,12 +1638,12 @@ with gr.Blocks(
|
|
| 1621 |
if evt.index is not None and evt.index < len(all_imgs):
|
| 1622 |
item = all_imgs[evt.index]
|
| 1623 |
return prepare_uploaded_image(_GALLERY_RAW_SOURCE.get(item, item))
|
| 1624 |
-
return None, None, gr.update(visible=False, value=1)
|
| 1625 |
|
| 1626 |
example_gallery.select(
|
| 1627 |
fn=load_from_gallery,
|
| 1628 |
inputs=user_uploaded_examples,
|
| 1629 |
-
outputs=[annotator, seg_raw_state, seg_frame_slider]
|
| 1630 |
)
|
| 1631 |
|
| 1632 |
# click event for submitting feedback
|
|
@@ -1704,6 +1721,11 @@ with gr.Blocks(
|
|
| 1704 |
label="π Stack frame (choose frame to use)",
|
| 1705 |
visible=False,
|
| 1706 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1707 |
|
| 1708 |
# Example gallery with "add" functionality
|
| 1709 |
gr.Markdown("π Example Image Gallery", elem_classes="frame-heading")
|
|
@@ -1734,7 +1756,7 @@ with gr.Blocks(
|
|
| 1734 |
gr.Markdown("β Add Example Image to Gallery", elem_classes="frame-heading")
|
| 1735 |
count_image_uploader = gr.File(
|
| 1736 |
show_label=False,
|
| 1737 |
-
file_types=["image"],
|
| 1738 |
type="filepath"
|
| 1739 |
)
|
| 1740 |
add_to_count_gallery_btn = gr.Button("β Add to Gallery", variant="secondary", size="sm")
|
|
@@ -1828,13 +1850,14 @@ with gr.Blocks(
|
|
| 1828 |
count_annotator.upload(
|
| 1829 |
fn=prepare_uploaded_image,
|
| 1830 |
inputs=count_annotator,
|
| 1831 |
-
outputs=[count_annotator, count_raw_state, count_frame_slider]
|
| 1832 |
-
)
|
| 1833 |
-
count_frame_slider.release(
|
| 1834 |
-
fn=select_frame,
|
| 1835 |
-
inputs=[count_raw_state, count_frame_slider],
|
| 1836 |
-
outputs=count_annotator
|
| 1837 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1838 |
|
| 1839 |
# When user selects from gallery, load into annotator
|
| 1840 |
def load_from_count_gallery(evt: gr.SelectData, all_imgs):
|
|
@@ -1843,12 +1866,12 @@ with gr.Blocks(
|
|
| 1843 |
selected_img = all_imgs[evt.index]
|
| 1844 |
print(f"πΈ Loading image from gallery: {selected_img}")
|
| 1845 |
return prepare_uploaded_image(_GALLERY_RAW_SOURCE.get(selected_img, selected_img))
|
| 1846 |
-
return None, None, gr.update(visible=False, value=1)
|
| 1847 |
|
| 1848 |
count_example_gallery.select(
|
| 1849 |
fn=load_from_count_gallery,
|
| 1850 |
inputs=count_user_examples,
|
| 1851 |
-
outputs=[count_annotator, count_raw_state, count_frame_slider]
|
| 1852 |
)
|
| 1853 |
|
| 1854 |
# Run counting
|
|
@@ -1865,9 +1888,10 @@ with gr.Blocks(
|
|
| 1865 |
|
| 1866 |
# Clear selection
|
| 1867 |
clear_btn.click(
|
| 1868 |
-
fn=lambda: (None, {}, None, gr.update(visible=False, value=1)
|
|
|
|
| 1869 |
inputs=None,
|
| 1870 |
-
outputs=[count_annotator, count_vis_state, count_raw_state, count_frame_slider]
|
| 1871 |
)
|
| 1872 |
|
| 1873 |
# Submit feedback
|
|
|
|
| 23 |
from inference_track import load_model as load_track_model, run as run_track
|
| 24 |
from _utils.image_io import (
|
| 25 |
standardize_image, inspect_image, image_size, array_nbytes,
|
| 26 |
+
RECOMMENDED_SIZE, WARN_SIZE, MAX_SIZE, MAX_READ_BYTES, TIFF_EXTENSIONS,
|
| 27 |
)
|
| 28 |
|
| 29 |
HF_TOKEN = os.getenv("HF_TOKEN")
|
|
|
|
| 408 |
|
| 409 |
if info.frames > 1:
|
| 410 |
lines.append(f"Showing first frame by default (you can use the stack frame slider to pick another).")
|
| 411 |
+
if info.sub_frames > 1:
|
| 412 |
+
lines.append(f"Showing the first of {info.sub_frames} {info.sub_label}s by default (use the {info.sub_label} slider to pick another).")
|
| 413 |
|
| 414 |
lines.append(
|
| 415 |
"If wrong, please convert your image to a 8-bit RGB/Grayscale image file before uploading (there are available tools such as ImageJ/Fiji)."
|
|
|
|
| 419 |
|
| 420 |
def prepare_uploaded_image(annot_value):
|
| 421 |
"""On annotator upload: standardize frame 0 for preview and configure the
|
| 422 |
+
stack sliders.
|
| 423 |
|
| 424 |
Browsers cannot render 16-bit / float / multi-page TIFFs, so we standardize
|
| 425 |
to an 8-bit RGB PNG. If the file is a stack, notify the user and reveal a
|
| 426 |
+
frame slider (default frame 1); a file with a second stack axis (e.g. Z in a
|
| 427 |
+
time+Z series) also gets a z-plane slider (0 = max-intensity projection).
|
| 428 |
+
Sliders that don't apply stay hidden.
|
| 429 |
|
| 430 |
+
Returns (annotator_value, raw_path, frame_slider_update, zplane_slider_update).
|
| 431 |
"""
|
| 432 |
+
hidden = gr.update(visible=False, value=1)
|
| 433 |
img_path = _annot_path(annot_value)
|
| 434 |
if not img_path:
|
| 435 |
+
return annot_value, None, hidden, hidden
|
| 436 |
|
| 437 |
# Clear the annotator on refusal, so a rejected file cannot reach inference.
|
| 438 |
rejected = check_image(img_path)
|
| 439 |
if rejected:
|
| 440 |
gr.Warning(rejected, duration=None, title="β Cannot use this file")
|
| 441 |
+
return None, None, hidden, hidden
|
| 442 |
|
| 443 |
info = inspect_image(img_path)
|
| 444 |
+
display = standardize_image(img_path, frame=0,
|
| 445 |
+
sub_frame=0 if info.sub_frames > 1 else None)
|
| 446 |
|
| 447 |
# Stay quiet unless something non-obvious happened; a plain RGB or grayscale
|
| 448 |
# image needs no explanation.
|
|
|
|
| 450 |
gr.Info(_describe_read(info), duration=None, title="π Image Loading Info")
|
| 451 |
|
| 452 |
slider = (gr.update(visible=True, maximum=info.frames, value=1) if info.frames > 1
|
| 453 |
+
else hidden)
|
| 454 |
+
zslider = (gr.update(visible=True, maximum=info.sub_frames, value=1,
|
| 455 |
+
label=f"π¬ {info.sub_label.capitalize()} (choose plane to use)")
|
| 456 |
+
if info.sub_frames > 1 else hidden)
|
| 457 |
+
return display, img_path, slider, zslider
|
| 458 |
|
| 459 |
|
| 460 |
+
def select_frame(raw_path, frame_num, zplane=1):
|
| 461 |
+
"""Re-render the annotator preview for the chosen frame and z-plane (both
|
| 462 |
+
1-based; z-plane is ignored for files with only one stack axis)."""
|
| 463 |
if not raw_path:
|
| 464 |
return gr.update()
|
| 465 |
+
return standardize_image(raw_path, frame=int(frame_num) - 1, sub_frame=int(zplane) - 1)
|
| 466 |
|
| 467 |
|
| 468 |
@spaces.GPU
|
|
|
|
| 1480 |
label="π Stack frame (choose frame to use)",
|
| 1481 |
visible=False,
|
| 1482 |
)
|
| 1483 |
+
seg_zplane_slider = gr.Slider(
|
| 1484 |
+
minimum=1, maximum=1, step=1, value=1,
|
| 1485 |
+
label="π¬ Z-plane (choose plane to use)",
|
| 1486 |
+
visible=False,
|
| 1487 |
+
)
|
| 1488 |
|
| 1489 |
# Example Images Gallery
|
| 1490 |
gr.Markdown("π Example Image Gallery", elem_classes="frame-heading")
|
|
|
|
| 1512 |
gr.Markdown("β Upload New Example Image to Gallery", elem_classes="frame-heading")
|
| 1513 |
image_uploader = gr.File(
|
| 1514 |
show_label=False,
|
| 1515 |
+
file_types=["image"] + list(TIFF_EXTENSIONS),
|
| 1516 |
type="filepath"
|
| 1517 |
)
|
| 1518 |
add_to_gallery_btn = gr.Button("β Add to Gallery", variant="secondary", size="sm")
|
|
|
|
| 1570 |
annotator.upload(
|
| 1571 |
fn=prepare_uploaded_image,
|
| 1572 |
inputs=annotator,
|
| 1573 |
+
outputs=[annotator, seg_raw_state, seg_frame_slider, seg_zplane_slider]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1574 |
)
|
| 1575 |
+
for _slider in (seg_frame_slider, seg_zplane_slider):
|
| 1576 |
+
_slider.release(
|
| 1577 |
+
fn=select_frame,
|
| 1578 |
+
inputs=[seg_raw_state, seg_frame_slider, seg_zplane_slider],
|
| 1579 |
+
outputs=annotator
|
| 1580 |
+
)
|
| 1581 |
|
| 1582 |
# click event for segmentation
|
| 1583 |
run_seg_btn.click(
|
|
|
|
| 1593 |
|
| 1594 |
# click event for clear button
|
| 1595 |
clear_btn.click(
|
| 1596 |
+
fn=lambda: (None, {}, None, gr.update(visible=False, value=1),
|
| 1597 |
+
gr.update(visible=False, value=1)),
|
| 1598 |
inputs=None,
|
| 1599 |
+
outputs=[annotator, seg_vis_state, seg_raw_state, seg_frame_slider, seg_zplane_slider]
|
| 1600 |
)
|
| 1601 |
|
| 1602 |
# init Gallery with example images
|
|
|
|
| 1638 |
if evt.index is not None and evt.index < len(all_imgs):
|
| 1639 |
item = all_imgs[evt.index]
|
| 1640 |
return prepare_uploaded_image(_GALLERY_RAW_SOURCE.get(item, item))
|
| 1641 |
+
return None, None, gr.update(visible=False, value=1), gr.update(visible=False, value=1)
|
| 1642 |
|
| 1643 |
example_gallery.select(
|
| 1644 |
fn=load_from_gallery,
|
| 1645 |
inputs=user_uploaded_examples,
|
| 1646 |
+
outputs=[annotator, seg_raw_state, seg_frame_slider, seg_zplane_slider]
|
| 1647 |
)
|
| 1648 |
|
| 1649 |
# click event for submitting feedback
|
|
|
|
| 1721 |
label="π Stack frame (choose frame to use)",
|
| 1722 |
visible=False,
|
| 1723 |
)
|
| 1724 |
+
count_zplane_slider = gr.Slider(
|
| 1725 |
+
minimum=1, maximum=1, step=1, value=1,
|
| 1726 |
+
label="π¬ Z-plane (choose plane to use)",
|
| 1727 |
+
visible=False,
|
| 1728 |
+
)
|
| 1729 |
|
| 1730 |
# Example gallery with "add" functionality
|
| 1731 |
gr.Markdown("π Example Image Gallery", elem_classes="frame-heading")
|
|
|
|
| 1756 |
gr.Markdown("β Add Example Image to Gallery", elem_classes="frame-heading")
|
| 1757 |
count_image_uploader = gr.File(
|
| 1758 |
show_label=False,
|
| 1759 |
+
file_types=["image"] + list(TIFF_EXTENSIONS),
|
| 1760 |
type="filepath"
|
| 1761 |
)
|
| 1762 |
add_to_count_gallery_btn = gr.Button("β Add to Gallery", variant="secondary", size="sm")
|
|
|
|
| 1850 |
count_annotator.upload(
|
| 1851 |
fn=prepare_uploaded_image,
|
| 1852 |
inputs=count_annotator,
|
| 1853 |
+
outputs=[count_annotator, count_raw_state, count_frame_slider, count_zplane_slider]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1854 |
)
|
| 1855 |
+
for _slider in (count_frame_slider, count_zplane_slider):
|
| 1856 |
+
_slider.release(
|
| 1857 |
+
fn=select_frame,
|
| 1858 |
+
inputs=[count_raw_state, count_frame_slider, count_zplane_slider],
|
| 1859 |
+
outputs=count_annotator
|
| 1860 |
+
)
|
| 1861 |
|
| 1862 |
# When user selects from gallery, load into annotator
|
| 1863 |
def load_from_count_gallery(evt: gr.SelectData, all_imgs):
|
|
|
|
| 1866 |
selected_img = all_imgs[evt.index]
|
| 1867 |
print(f"πΈ Loading image from gallery: {selected_img}")
|
| 1868 |
return prepare_uploaded_image(_GALLERY_RAW_SOURCE.get(selected_img, selected_img))
|
| 1869 |
+
return None, None, gr.update(visible=False, value=1), gr.update(visible=False, value=1)
|
| 1870 |
|
| 1871 |
count_example_gallery.select(
|
| 1872 |
fn=load_from_count_gallery,
|
| 1873 |
inputs=count_user_examples,
|
| 1874 |
+
outputs=[count_annotator, count_raw_state, count_frame_slider, count_zplane_slider]
|
| 1875 |
)
|
| 1876 |
|
| 1877 |
# Run counting
|
|
|
|
| 1888 |
|
| 1889 |
# Clear selection
|
| 1890 |
clear_btn.click(
|
| 1891 |
+
fn=lambda: (None, {}, None, gr.update(visible=False, value=1),
|
| 1892 |
+
gr.update(visible=False, value=1)),
|
| 1893 |
inputs=None,
|
| 1894 |
+
outputs=[count_annotator, count_vis_state, count_raw_state, count_frame_slider, count_zplane_slider]
|
| 1895 |
)
|
| 1896 |
|
| 1897 |
# Submit feedback
|