ci-deconvolve 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/PKG-INFO +3 -1
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/README.md +2 -0
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/ci_deconvolve.egg-info/PKG-INFO +3 -1
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/ci_deconvolve.egg-info/SOURCES.txt +2 -1
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/pyproject.toml +1 -1
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/src/ci_deconvolve/__init__.py +1 -1
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/src/ci_deconvolve/cli.py +235 -5
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/src/core/deconvolve.py +336 -166
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/src/core/ome_zarr_io.py +95 -16
- ci_deconvolve-0.2.1/tests/test_cli_contract.py +86 -0
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/ci_deconvolve.egg-info/dependency_links.txt +0 -0
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/ci_deconvolve.egg-info/entry_points.txt +0 -0
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/ci_deconvolve.egg-info/requires.txt +0 -0
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/ci_deconvolve.egg-info/top_level.txt +0 -0
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/setup.cfg +0 -0
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/src/core/__init__.py +0 -0
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/src/core/_meta_helpers.py +0 -0
- {ci_deconvolve-0.2.0 → ci_deconvolve-0.2.1}/src/core/deconvolve_ci.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ci-deconvolve
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Focused command-line CI-RL deconvolution for OME-TIFF and OME-Zarr images.
|
|
5
5
|
Author: NL-BioImaging contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -102,6 +102,7 @@ ci_deconvolve --input image.ome.tiff --output out ^
|
|
|
102
102
|
--iterations 40 ^
|
|
103
103
|
--device auto ^
|
|
104
104
|
--output-format ome-tiff ^
|
|
105
|
+
--projection none ^
|
|
105
106
|
--emission-wl 520 ^
|
|
106
107
|
--excitation-wl 488 ^
|
|
107
108
|
--pixel-size-xy 0.065 ^
|
|
@@ -146,6 +147,7 @@ does not process plate fields.
|
|
|
146
147
|
| Option | Default | Description |
|
|
147
148
|
| --- | --- | --- |
|
|
148
149
|
| `--output-format ome-tiff\|ome-zarr` | `ome-tiff` | Writes `<stem>_decon.ome.tiff` or `<stem>_decon.ome.zarr`. |
|
|
150
|
+
| `--projection none\|max-z` | `none` | With `max-z`, writes a maximum-Z projection when the deconvolved result is 3D. 2D inputs are unchanged. |
|
|
149
151
|
| `--iterations N[,N...]` | `40` | CI-RL iteration count. A comma- or semicolon-separated list applies per channel, with the last value reused for extra channels. |
|
|
150
152
|
| `--device auto\|cpu\|cuda` | `auto` | Compute device. `auto` lets PyTorch choose CUDA when available, otherwise CPU. |
|
|
151
153
|
| `-v`, `--verbose` | off | Enable INFO logging from the CLI and core deconvolution code. |
|
|
@@ -74,6 +74,7 @@ ci_deconvolve --input image.ome.tiff --output out ^
|
|
|
74
74
|
--iterations 40 ^
|
|
75
75
|
--device auto ^
|
|
76
76
|
--output-format ome-tiff ^
|
|
77
|
+
--projection none ^
|
|
77
78
|
--emission-wl 520 ^
|
|
78
79
|
--excitation-wl 488 ^
|
|
79
80
|
--pixel-size-xy 0.065 ^
|
|
@@ -118,6 +119,7 @@ does not process plate fields.
|
|
|
118
119
|
| Option | Default | Description |
|
|
119
120
|
| --- | --- | --- |
|
|
120
121
|
| `--output-format ome-tiff\|ome-zarr` | `ome-tiff` | Writes `<stem>_decon.ome.tiff` or `<stem>_decon.ome.zarr`. |
|
|
122
|
+
| `--projection none\|max-z` | `none` | With `max-z`, writes a maximum-Z projection when the deconvolved result is 3D. 2D inputs are unchanged. |
|
|
121
123
|
| `--iterations N[,N...]` | `40` | CI-RL iteration count. A comma- or semicolon-separated list applies per channel, with the last value reused for extra channels. |
|
|
122
124
|
| `--device auto\|cpu\|cuda` | `auto` | Compute device. `auto` lets PyTorch choose CUDA when available, otherwise CPU. |
|
|
123
125
|
| `-v`, `--verbose` | off | Enable INFO logging from the CLI and core deconvolution code. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ci-deconvolve
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Focused command-line CI-RL deconvolution for OME-TIFF and OME-Zarr images.
|
|
5
5
|
Author: NL-BioImaging contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -102,6 +102,7 @@ ci_deconvolve --input image.ome.tiff --output out ^
|
|
|
102
102
|
--iterations 40 ^
|
|
103
103
|
--device auto ^
|
|
104
104
|
--output-format ome-tiff ^
|
|
105
|
+
--projection none ^
|
|
105
106
|
--emission-wl 520 ^
|
|
106
107
|
--excitation-wl 488 ^
|
|
107
108
|
--pixel-size-xy 0.065 ^
|
|
@@ -146,6 +147,7 @@ does not process plate fields.
|
|
|
146
147
|
| Option | Default | Description |
|
|
147
148
|
| --- | --- | --- |
|
|
148
149
|
| `--output-format ome-tiff\|ome-zarr` | `ome-tiff` | Writes `<stem>_decon.ome.tiff` or `<stem>_decon.ome.zarr`. |
|
|
150
|
+
| `--projection none\|max-z` | `none` | With `max-z`, writes a maximum-Z projection when the deconvolved result is 3D. 2D inputs are unchanged. |
|
|
149
151
|
| `--iterations N[,N...]` | `40` | CI-RL iteration count. A comma- or semicolon-separated list applies per channel, with the last value reused for extra channels. |
|
|
150
152
|
| `--device auto\|cpu\|cuda` | `auto` | Compute device. `auto` lets PyTorch choose CUDA when available, otherwise CPU. |
|
|
151
153
|
| `-v`, `--verbose` | off | Enable INFO logging from the CLI and core deconvolution code. |
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import json
|
|
4
5
|
import logging
|
|
5
6
|
import shutil
|
|
7
|
+
import signal
|
|
6
8
|
import sys
|
|
7
9
|
import time
|
|
8
10
|
from pathlib import Path
|
|
9
11
|
from typing import Iterable
|
|
10
12
|
|
|
13
|
+
from ci_deconvolve import __version__
|
|
14
|
+
|
|
11
15
|
LOGGER = logging.getLogger("ci_deconvolve")
|
|
16
|
+
_CANCEL_REQUESTED = False
|
|
12
17
|
|
|
13
18
|
OME_TIFF_SUFFIXES = (".ome.tif", ".ome.tiff")
|
|
14
19
|
ZARR_SUFFIXES = (".zarr", ".ome.zarr")
|
|
@@ -105,13 +110,30 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
105
110
|
)
|
|
106
111
|
parser.add_argument("input_path", nargs="?", help="Input file/folder. Alias for --input.")
|
|
107
112
|
parser.add_argument("--input", dest="input_option", help="OME-TIFF, OME-Zarr, or folder.")
|
|
108
|
-
parser.add_argument("--output",
|
|
113
|
+
parser.add_argument("--output", help="Output folder.")
|
|
114
|
+
parser.add_argument(
|
|
115
|
+
"--validate-env",
|
|
116
|
+
action="store_true",
|
|
117
|
+
help="Report Python/PyTorch/Zarr/CIDeconvolve environment details and exit.",
|
|
118
|
+
)
|
|
109
119
|
parser.add_argument(
|
|
110
120
|
"--output-format",
|
|
111
121
|
choices=("ome-tiff", "ome-zarr"),
|
|
112
122
|
default="ome-tiff",
|
|
113
123
|
help="Output format. Default: ome-tiff.",
|
|
114
124
|
)
|
|
125
|
+
parser.add_argument(
|
|
126
|
+
"--projection",
|
|
127
|
+
choices=("none", "max-z"),
|
|
128
|
+
default="none",
|
|
129
|
+
help="Write a max-Z projection instead of the full 3D stack when input data is 3D.",
|
|
130
|
+
)
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
"--ome-zarr-pyramid",
|
|
133
|
+
choices=("auto", "on", "off"),
|
|
134
|
+
default="auto",
|
|
135
|
+
help="Write XY pyramid levels for OME-Zarr output. Default: auto.",
|
|
136
|
+
)
|
|
115
137
|
parser.add_argument("--iterations", type=_parse_int_list, default=[40])
|
|
116
138
|
parser.add_argument("--device", choices=("auto", "cpu", "cuda"), default="auto")
|
|
117
139
|
parser.add_argument("--background", default="auto")
|
|
@@ -161,6 +183,10 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
161
183
|
)
|
|
162
184
|
parser.add_argument("--two-d-wf-bg-radius-um", type=float, default=0.5)
|
|
163
185
|
parser.add_argument("--two-d-wf-bg-scale", type=float, default=1.0)
|
|
186
|
+
parser.add_argument("--save-qc-mips", dest="save_qc_mips", action="store_true", default=True)
|
|
187
|
+
parser.add_argument("--no-qc-mips", dest="save_qc_mips", action="store_false")
|
|
188
|
+
parser.add_argument("--write-manifest", dest="write_manifest", action="store_true", default=True)
|
|
189
|
+
parser.add_argument("--no-manifest", dest="write_manifest", action="store_false")
|
|
164
190
|
parser.add_argument("-v", "--verbose", action="store_true")
|
|
165
191
|
return parser
|
|
166
192
|
|
|
@@ -175,6 +201,119 @@ def _output_path(input_path: Path, output_dir: Path, output_format: str) -> Path
|
|
|
175
201
|
return output_dir / f"{_stem(input_path)}_decon.ome.tiff"
|
|
176
202
|
|
|
177
203
|
|
|
204
|
+
def _apply_projection(result: dict, projection: str) -> dict:
|
|
205
|
+
if projection != "max-z":
|
|
206
|
+
return result
|
|
207
|
+
channels = list(result.get("channels") or [])
|
|
208
|
+
if not channels:
|
|
209
|
+
return result
|
|
210
|
+
if channels[0].ndim == 4 and channels[0].shape[1] > 1:
|
|
211
|
+
import numpy as np
|
|
212
|
+
|
|
213
|
+
projected = dict(result)
|
|
214
|
+
projected["channels"] = [
|
|
215
|
+
np.max(channel, axis=1).astype(np.float32, copy=False)
|
|
216
|
+
for channel in channels
|
|
217
|
+
]
|
|
218
|
+
if result.get("source_channels"):
|
|
219
|
+
projected["source_channels"] = [
|
|
220
|
+
np.max(channel, axis=1).astype(np.float32, copy=False)
|
|
221
|
+
if getattr(channel, "ndim", 0) == 4 and channel.shape[1] > 1
|
|
222
|
+
else channel
|
|
223
|
+
for channel in result["source_channels"]
|
|
224
|
+
]
|
|
225
|
+
metadata = dict(result.get("metadata") or {})
|
|
226
|
+
metadata["projection"] = {"axis": "z", "method": "max"}
|
|
227
|
+
metadata["size_z"] = 1
|
|
228
|
+
projected["metadata"] = metadata
|
|
229
|
+
return projected
|
|
230
|
+
if channels[0].ndim != 3 or channels[0].shape[0] <= 1:
|
|
231
|
+
return result
|
|
232
|
+
|
|
233
|
+
import numpy as np
|
|
234
|
+
|
|
235
|
+
projected = dict(result)
|
|
236
|
+
projected["channels"] = [np.max(channel, axis=0).astype(np.float32, copy=False) for channel in channels]
|
|
237
|
+
if result.get("source_channels"):
|
|
238
|
+
projected["source_channels"] = [
|
|
239
|
+
np.max(channel, axis=0).astype(np.float32, copy=False)
|
|
240
|
+
if getattr(channel, "ndim", 0) == 3 and channel.shape[0] > 1
|
|
241
|
+
else channel
|
|
242
|
+
for channel in result["source_channels"]
|
|
243
|
+
]
|
|
244
|
+
metadata = dict(result.get("metadata") or {})
|
|
245
|
+
metadata["projection"] = {"axis": "z", "method": "max"}
|
|
246
|
+
metadata["size_z"] = 1
|
|
247
|
+
projected["metadata"] = metadata
|
|
248
|
+
return projected
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _format_metadata_value(value) -> str:
|
|
252
|
+
if value is None:
|
|
253
|
+
return "missing"
|
|
254
|
+
if isinstance(value, float):
|
|
255
|
+
return f"{value:g}"
|
|
256
|
+
return str(value)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _metadata_source(metadata: dict, key: str) -> str:
|
|
260
|
+
provenance = metadata.get("_metadata_provenance") or {}
|
|
261
|
+
fields = provenance.get("fields") if isinstance(provenance, dict) else {}
|
|
262
|
+
return str((fields or {}).get(key) or "unknown")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _channel_metadata_source(metadata: dict, index: int, key: str) -> str:
|
|
266
|
+
provenance = metadata.get("_metadata_provenance") or {}
|
|
267
|
+
channels = provenance.get("channels") if isinstance(provenance, dict) else []
|
|
268
|
+
if isinstance(channels, list) and index < len(channels) and isinstance(channels[index], dict):
|
|
269
|
+
return str(channels[index].get(key) or "unknown")
|
|
270
|
+
return "unknown"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _print_metadata_report(metadata: dict) -> None:
|
|
274
|
+
fields = [
|
|
275
|
+
("pixel_size_x", "pixel size X", "um"),
|
|
276
|
+
("pixel_size_y", "pixel size Y", "um"),
|
|
277
|
+
("pixel_size_z", "pixel size Z", "um"),
|
|
278
|
+
("na", "NA", ""),
|
|
279
|
+
("refractive_index", "immersion RI", ""),
|
|
280
|
+
("sample_refractive_index", "sample RI", ""),
|
|
281
|
+
("microscope_type", "microscope", ""),
|
|
282
|
+
]
|
|
283
|
+
print(" metadata:")
|
|
284
|
+
for key, label, unit in fields:
|
|
285
|
+
value = _format_metadata_value(metadata.get(key))
|
|
286
|
+
suffix = f" {unit}" if unit else ""
|
|
287
|
+
print(f" {label:14}: {value}{suffix} ({_metadata_source(metadata, key)})")
|
|
288
|
+
|
|
289
|
+
defaulted = sorted(str(key) for key in metadata.get("_defaulted_keys") or [])
|
|
290
|
+
inferred = sorted(str(key) for key in metadata.get("_inferred_keys") or [])
|
|
291
|
+
warnings = [str(item) for item in metadata.get("metadata_warnings") or [] if str(item).strip()]
|
|
292
|
+
if defaulted:
|
|
293
|
+
print(" defaults : " + ", ".join(defaulted))
|
|
294
|
+
if inferred:
|
|
295
|
+
print(" inferred : " + ", ".join(inferred))
|
|
296
|
+
if warnings:
|
|
297
|
+
print(" warnings : " + " | ".join(warnings))
|
|
298
|
+
|
|
299
|
+
channels = list(metadata.get("channels") or [])
|
|
300
|
+
for index, channel in enumerate(channels):
|
|
301
|
+
name = channel.get("name") or channel.get("label")
|
|
302
|
+
if not name and index < len(metadata.get("channel_names") or []):
|
|
303
|
+
name = metadata["channel_names"][index]
|
|
304
|
+
print(f" channel {index}{f' ({name})' if name else ''}:")
|
|
305
|
+
for key, label, unit in (
|
|
306
|
+
("emission_wavelength", "emission", "nm"),
|
|
307
|
+
("excitation_wavelength", "excitation", "nm"),
|
|
308
|
+
("pinhole_airy_units", "pinhole", "AU"),
|
|
309
|
+
):
|
|
310
|
+
value = _format_metadata_value(channel.get(key))
|
|
311
|
+
print(
|
|
312
|
+
f" {label:10}: {value} {unit} "
|
|
313
|
+
f"({_channel_metadata_source(metadata, index, key)})"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
178
317
|
def _run_one(input_path: Path, output_dir: Path, args: argparse.Namespace) -> Path:
|
|
179
318
|
from core.deconvolve import deconvolve_image, save_result
|
|
180
319
|
from core.ome_zarr_io import is_hcs_plate, save_result_ome_zarr
|
|
@@ -212,17 +351,20 @@ def _run_one(input_path: Path, output_dir: Path, args: argparse.Namespace) -> Pa
|
|
|
212
351
|
two_d_wf_bg_radius_um=max(0.1, float(args.two_d_wf_bg_radius_um)),
|
|
213
352
|
two_d_wf_bg_scale=max(0.1, float(args.two_d_wf_bg_scale)),
|
|
214
353
|
device=device,
|
|
354
|
+
cancel_checker=lambda: _CANCEL_REQUESTED,
|
|
215
355
|
)
|
|
356
|
+
result = _apply_projection(result, args.projection)
|
|
357
|
+
_print_metadata_report(result.get("metadata") or {})
|
|
216
358
|
|
|
217
359
|
out_path = _output_path(input_path, output_dir, args.output_format)
|
|
218
360
|
if args.output_format == "ome-zarr":
|
|
219
|
-
return save_result_ome_zarr(result, out_path)
|
|
361
|
+
return save_result_ome_zarr(result, out_path, pyramid=args.ome_zarr_pyramid)
|
|
220
362
|
|
|
221
363
|
tmp_dir = output_dir / ".ci_deconvolve_tmp"
|
|
222
364
|
tmp_dir.mkdir(parents=True, exist_ok=True)
|
|
223
365
|
tmp_path = tmp_dir / out_path.name
|
|
224
366
|
try:
|
|
225
|
-
save_result(result, tmp_path)
|
|
367
|
+
save_result(result, tmp_path, save_qc_mips=bool(args.save_qc_mips))
|
|
226
368
|
if out_path.exists():
|
|
227
369
|
out_path.unlink()
|
|
228
370
|
shutil.move(str(tmp_path), str(out_path))
|
|
@@ -231,6 +373,58 @@ def _run_one(input_path: Path, output_dir: Path, args: argparse.Namespace) -> Pa
|
|
|
231
373
|
return out_path
|
|
232
374
|
|
|
233
375
|
|
|
376
|
+
def _validate_environment() -> int:
|
|
377
|
+
print("CIDECONVOLVE_OK")
|
|
378
|
+
print(f"ci_deconvolve={__version__}")
|
|
379
|
+
print(f"python={sys.version.split()[0]}")
|
|
380
|
+
try:
|
|
381
|
+
import torch
|
|
382
|
+
print(f"torch={getattr(torch, '__version__', 'unknown')}")
|
|
383
|
+
print(f"cuda_available={bool(torch.cuda.is_available())}")
|
|
384
|
+
print(f"cuda_device_count={torch.cuda.device_count() if torch.cuda.is_available() else 0}")
|
|
385
|
+
except Exception as exc:
|
|
386
|
+
print(f"torch_error={type(exc).__name__}: {exc}")
|
|
387
|
+
return 1
|
|
388
|
+
try:
|
|
389
|
+
import zarr
|
|
390
|
+
print(f"zarr={getattr(zarr, '__version__', 'unknown')}")
|
|
391
|
+
print("ome_zarr_v2_write=ok")
|
|
392
|
+
except Exception as exc:
|
|
393
|
+
print(f"zarr_error={type(exc).__name__}: {exc}")
|
|
394
|
+
return 1
|
|
395
|
+
return 0
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _install_signal_handlers() -> None:
|
|
399
|
+
def _handler(signum, frame):
|
|
400
|
+
global _CANCEL_REQUESTED
|
|
401
|
+
_CANCEL_REQUESTED = True
|
|
402
|
+
print(f"Cancellation requested by signal {signum}.", file=sys.stderr)
|
|
403
|
+
|
|
404
|
+
for sig_name in ("SIGINT", "SIGTERM"):
|
|
405
|
+
sig = getattr(signal, sig_name, None)
|
|
406
|
+
if sig is not None:
|
|
407
|
+
try:
|
|
408
|
+
signal.signal(sig, _handler)
|
|
409
|
+
except Exception:
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _write_manifest(output_dir: Path, records: list[dict], args: argparse.Namespace) -> None:
|
|
414
|
+
manifest = {
|
|
415
|
+
"ci_deconvolve_version": __version__,
|
|
416
|
+
"created_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
417
|
+
"output_format": args.output_format,
|
|
418
|
+
"projection": args.projection,
|
|
419
|
+
"iterations": args.iterations,
|
|
420
|
+
"records": records,
|
|
421
|
+
}
|
|
422
|
+
(output_dir / "ci_deconvolve_manifest.json").write_text(
|
|
423
|
+
json.dumps(manifest, indent=2, default=str),
|
|
424
|
+
encoding="utf-8",
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
234
428
|
def run(argv: Iterable[str] | None = None) -> int:
|
|
235
429
|
parser = _build_parser()
|
|
236
430
|
args = parser.parse_args(list(argv) if argv is not None else None)
|
|
@@ -239,6 +433,12 @@ def run(argv: Iterable[str] | None = None) -> int:
|
|
|
239
433
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
240
434
|
)
|
|
241
435
|
|
|
436
|
+
if args.validate_env:
|
|
437
|
+
return _validate_environment()
|
|
438
|
+
if not args.output:
|
|
439
|
+
parser.error("--output is required unless --validate-env is used")
|
|
440
|
+
_install_signal_handlers()
|
|
441
|
+
|
|
242
442
|
input_value = args.input_option or args.input_path
|
|
243
443
|
if not input_value:
|
|
244
444
|
parser.error("an input path is required via --input or positional input_path")
|
|
@@ -254,20 +454,50 @@ def run(argv: Iterable[str] | None = None) -> int:
|
|
|
254
454
|
print(f" input count : {len(inputs)}")
|
|
255
455
|
print(f" output : {output_dir}")
|
|
256
456
|
print(f" output format: {args.output_format}")
|
|
457
|
+
print(f" projection : {args.projection}")
|
|
257
458
|
print(f" iterations : {', '.join(str(v) for v in args.iterations)}")
|
|
258
459
|
|
|
259
460
|
failures: list[tuple[Path, Exception]] = []
|
|
461
|
+
records: list[dict] = []
|
|
260
462
|
for index, item in enumerate(inputs, start=1):
|
|
463
|
+
if _CANCEL_REQUESTED:
|
|
464
|
+
print("\nCancelled before remaining inputs.", file=sys.stderr)
|
|
465
|
+
return 130
|
|
261
466
|
print(f"\n[{index}/{len(inputs)}] {item}")
|
|
262
467
|
start = time.time()
|
|
263
468
|
try:
|
|
264
469
|
out_path = _run_one(item, output_dir, args)
|
|
265
470
|
print(f" saved: {out_path}")
|
|
266
|
-
|
|
471
|
+
elapsed = time.time() - start
|
|
472
|
+
print(f" time : {elapsed:.1f}s")
|
|
473
|
+
records.append({
|
|
474
|
+
"input": str(item),
|
|
475
|
+
"output": str(out_path),
|
|
476
|
+
"status": "success",
|
|
477
|
+
"seconds": elapsed,
|
|
478
|
+
})
|
|
267
479
|
except Exception as exc:
|
|
480
|
+
if _CANCEL_REQUESTED or str(exc) == "Cancelled":
|
|
481
|
+
elapsed = time.time() - start
|
|
482
|
+
print(" CANCELLED", file=sys.stderr)
|
|
483
|
+
records.append({
|
|
484
|
+
"input": str(item),
|
|
485
|
+
"status": "cancelled",
|
|
486
|
+
"seconds": elapsed,
|
|
487
|
+
})
|
|
488
|
+
if args.write_manifest:
|
|
489
|
+
_write_manifest(output_dir, records, args)
|
|
490
|
+
return 130
|
|
268
491
|
failures.append((item, exc))
|
|
269
492
|
print(f" ERROR: {exc}", file=sys.stderr)
|
|
270
|
-
|
|
493
|
+
records.append({
|
|
494
|
+
"input": str(item),
|
|
495
|
+
"status": "failed",
|
|
496
|
+
"error": str(exc),
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
if args.write_manifest:
|
|
500
|
+
_write_manifest(output_dir, records, args)
|
|
271
501
|
if failures:
|
|
272
502
|
print(f"\nFailed inputs: {len(failures)}", file=sys.stderr)
|
|
273
503
|
for item, exc in failures:
|