diff --git a/README.md b/README.md index 3c2de64..4711d52 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Currently targets the ONNX backend. The pipeline is designed to support addition | [`mask-object-segnext-b2hq`](models/mask-object-segnext-b2hq/README.md) | mask | SegNext ViT-B SAx2 HQ for semantic masking | | [`rawdenoise-nind`](models/rawdenoise-nind/README.md) | rawdenoise | UtNet2 raw denoiser trained on RawNIND (Bayer + linear Rec.2020 variants) | | [`upscale-bsrgan`](models/upscale-bsrgan/README.md) | upscale | BSRGAN 2x and 4x blind super-resolution | +| [`upscale-realplksr`](models/upscale-realplksr/README.md) | upscale | RealPLKSR 2x and 4x super-resolution (real-world photos)| ## Repository structure diff --git a/models/upscale-bsrgan/model.yaml b/models/upscale-bsrgan/model.yaml index 86ccc5b..8e64171 100644 --- a/models/upscale-bsrgan/model.yaml +++ b/models/upscale-bsrgan/model.yaml @@ -2,7 +2,7 @@ id: upscale-bsrgan name: "upscale bsrgan" description: "BSRGAN 2x and 4x blind super-resolution" task: upscale -version: "0.1" +version: "0.2" backend: onnx tiling: true type: multi @@ -17,7 +17,7 @@ attributes: input_sizes: [256] model_card: - long_description: "BSRGAN blind image super-resolution using practical degradation model; includes both 2x and 4x upscaling variants with RRDBNet architecture" + long_description: "BSRGAN (Blind Super-Resolution GAN). 2x or 4x photo upscaling that also cleans up noise, JPEG artefacts, and mild blur. Best for high-ISO captures, scanned negatives or prints, and other less-than-pristine source photos" scope: "image upscaling (2x and 4x blind super-resolution)" author: "Kai Zhang (ETH Zurich)" source: "https://github.com/cszn/BSRGAN" diff --git a/models/upscale-realplksr/README.md b/models/upscale-realplksr/README.md new file mode 100644 index 0000000..24106df --- /dev/null +++ b/models/upscale-realplksr/README.md @@ -0,0 +1,89 @@ +# RealPLKSR + +Real-world variant of PLKSR (Partial Large Kernel CNN for Super-Resolution), +robust to typical photo artefacts (noise, blur, JPEG/WebP compression). +Lightweight pure-CNN architecture – faster than transformer-based upscalers. + +Includes both 2x and 4x upscaling variants. + +## Source + +- Architecture and weights: [dslisleedh/PLKSR](https://github.com/dslisleedh/PLKSR) (MIT) – Dongheon Lee et al., Machine Intelligence Laboratory, University of Seoul +- Paper: [Partial Large Kernel CNNs for Efficient Super-Resolution](https://arxiv.org/abs/2404.11848) (2024) +- Checkpoints: dslisleedh's MSSIM pretrain release – [see issue #4](https://github.com/dslisleedh/PLKSR/issues/4) (MIT, same as architecture) + - x2: `2x_realplksr_mssim_pretrain.pth` + - x4: `4x_realplksr_mssim_pretrain.pth` +- Trained via the [neosr](https://github.com/neosr-project/neosr) framework with the RealESRGAN degradation pipeline +- Loaded for conversion via [chaiNNer-org/spandrel](https://github.com/chaiNNer-org/spandrel), which auto-detects the RealPLKSR variant from the checkpoint state_dict + +## Architecture + +PLKSR replaces standard depthwise large-kernel convolutions with *partial* +large kernels – applied only to a subset of channels – reducing FLOPs while +keeping the receptive field that drives SR quality. The Real-world variant +swaps the upsampler for DySample and is trained with stronger augmentation +for robustness to typical photo artefacts (noise, blur, compression). + +The shipped weights are the MSSIM-pretrain stage (no GAN finetune) – +faithful, conservative output without the texture hallucination risk that +GAN-trained SR models exhibit. + +| Property | Value | +|--------------|------------------------------------------------------| +| Architecture | RealPLKSR | +| Parameters | ~7M | +| Receptive | Large (partial 17×17 kernels) | +| Upsampler | DySample | +| Loss | MSSIM (pretrain stage) | + +## ONNX Models + +| Property | model_x2.onnx | model_x4.onnx | +|------------|--------------------------------------|----------------------------------------| +| Input | `input` – float32 [1, 3, 512, 512] | `input` – float32 [1, 3, 256, 256] | +| Output | `output` – float32 [1, 3, 1024, 1024]| `output` – float32 [1, 3, 1024, 1024] | +| Resolution | Static, baked at 512×512 | Static, baked at 256×256 | +| Opset | 20 | 20 | +| Normalize | [0, 1] range (divide by 255) | [0, 1] range (divide by 255) | +| Tiling | Yes (`model_x2.input_sizes: [512]`) | Yes (`model_x4.input_sizes: [256]`) | + +Both variants produce a 1024×1024 output tile – x2 from a 512×512 input, +x4 from a 256×256 input. Per-stem tile sizes are declared in the manifest +so darktable picks the right size for each variant at runtime: + +```yaml +attributes: + model_x2: + input_sizes: [512] + model_x4: + input_sizes: [256] +``` + +## Notes + +- Input and output are RGB images in [0, 1] range. +- Output should be clipped to [0, 1] before converting back to uint8. +- Exported with FP32 precision. FP16 export is supported via `--fp16` in + convert args but off by default. +- Inputs are baked into the graph so JIT-compiling EPs (CoreML, + MIGraphX) only pay the compile cost once. Callers must tile at + exactly the declared size. +- Conversion uses [Spandrel](https://github.com/chaiNNer-org/spandrel) + to auto-detect the RealPLKSR variant from the checkpoint's state_dict, + avoiding the need to clone PLKSR or neosr. + +## Selection Criteria + +| Property | Value | +|--------------------------|-----------------------------------------------------------------------------------------------------| +| Model license | MIT (weights and architecture) | +| OSAID v1.0 | Open Source AI | +| MOF | Class II (Open Tooling) | +| Training data license | DF2K (DIV2K + Flickr2K) per neosr common practice; Flickr2K without an explicit open-source license | +| Training data provenance | Synthetic real-world degradations applied via the neosr framework | +| Training code | [PLKSR](https://github.com/dslisleedh/PLKSR) (MIT) + [neosr](https://github.com/neosr-project/neosr) (Apache-2.0) | +| Known limitations | MSSIM-pretrain checkpoints only (no GAN finetune) – conservative output, no hallucinated detail | +| Published research | [Partial Large Kernel CNNs for Efficient Super-Resolution](https://arxiv.org/abs/2404.11848) | +| Inference | Local only, no cloud dependencies | +| Scope | Image upscaling (2x and 4x super-resolution, robust to noise/blur/compression artefacts) | +| Reproducibility | Full pipeline (setup, convert, clean, demo) | diff --git a/models/upscale-realplksr/convert.py b/models/upscale-realplksr/convert.py new file mode 100644 index 0000000..e820bda --- /dev/null +++ b/models/upscale-realplksr/convert.py @@ -0,0 +1,151 @@ +"""Export RealPLKSR to ONNX via Spandrel. + +Spandrel auto-detects the RealPLKSR variant from the checkpoint's +state_dict and builds the network without needing to clone neosr or +the original PLKSR training framework. For a one-shot ONNX export +this is significantly easier than wiring up the training-time scaffolding. + +RealPLKSR is a pure-CNN architecture with no window-attention constraints, +so any input multiple of 4 works. We trace at the deployment dim directly +to keep the captured dims consistent with the static shape — same pattern +the DAT-2 export uses, applied here for simplicity. +""" + +import argparse +import os + +import torch + +try: + import onnxconverter_common + HAS_ONNX_CONVERTER = True +except ImportError: + HAS_ONNX_CONVERTER = False + +from spandrel import ImageModelDescriptor, ModelLoader + + +def _patch_plkconv_for_clean_export(): + """Force PLKConv2d to use its split+cat forward path (which is its + training-mode branch) regardless of mode. The eval-mode branch uses + in-place indexed assignment (`x[:, :idx] = conv(...)`), which + torch.onnx exports as ScatterND — ONNX Runtime then prints a + "may not be deterministic if indices are duplicated" warning for + each block at load time. The split+cat path is numerically + identical for contiguous-from-0 channels and exports as plain + Split+Conv+Concat, with no warning. + """ + from spandrel.architectures.PLKSR.__arch.RealPLKSR import PLKConv2d + + def forward_export(self, x: torch.Tensor) -> torch.Tensor: + x1, x2 = torch.split(x, [self.idx, x.size(1) - self.idx], dim=1) + x1 = self.conv(x1) + return torch.cat([x1, x2], dim=1) + + PLKConv2d.forward = forward_export + + +def export_to_onnx(model, output_path, scale, height=256, width=256, + dynamic_shapes=True, opset_version=20, fp16=False): + os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True) + + import onnx + + if dynamic_shapes: + trace_h, trace_w = 64, 64 + dynamic_axes = { + 'input': {0: 'batch', 2: 'height', 3: 'width'}, + 'output': {0: 'batch', 2: 'height', 3: 'width'}, + } + else: + trace_h, trace_w = height, width + dynamic_axes = None + + dummy_input = torch.randn(1, 3, trace_h, trace_w) + + torch.onnx.export( + model, + dummy_input, + output_path, + export_params=True, + opset_version=opset_version, + do_constant_folding=True, + input_names=['input'], + output_names=['output'], + dynamic_axes=dynamic_axes, + verbose=False, + ) + print(f"Exported: {output_path} (traced at {trace_h}x{trace_w})") + + onnx_model = onnx.load(output_path) + onnx.checker.check_model(onnx_model) + print(" ONNX verification passed.") + + if not dynamic_shapes: + print(f" Static dims baked: " + f"{height}x{width} -> {height * scale}x{width * scale}") + + if fp16: + if not HAS_ONNX_CONVERTER: + print("Warning: onnxconverter-common not installed. Skipping FP16 conversion.") + return + print("Converting to FP16...") + from onnxconverter_common import float16 + fp16_model = float16.convert_float_to_float16(onnx_model) + onnx.save(fp16_model, output_path) + print(f"FP16 model saved to {output_path}") + + +def convert(checkpoint, output, scale, height=256, width=256, + dynamic_shapes=True, opset=20, fp16=False, static=False): + """Entry point for programmatic conversion.""" + scale = int(scale) + + _patch_plkconv_for_clean_export() + + print(f"Loading RealPLKSR model via Spandrel: {checkpoint}") + descriptor = ModelLoader().load_from_file(checkpoint) + if not isinstance(descriptor, ImageModelDescriptor): + raise TypeError( + f"expected ImageModelDescriptor, got {type(descriptor).__name__}") + if descriptor.scale != scale: + raise ValueError( + f"checkpoint scale={descriptor.scale} does not match requested scale={scale}") + + model = descriptor.model + model.eval() + + param_count = sum(p.numel() for p in model.parameters()) + print(f" Architecture: {descriptor.architecture.id}") + print(f" Scale: x{descriptor.scale}") + print(f" Parameters: {param_count:,}") + + print("Exporting to ONNX...") + export_to_onnx(model, output, scale, + height=height, width=width, + dynamic_shapes=dynamic_shapes and not static, + opset_version=opset, fp16=fp16) + + +def main(): + parser = argparse.ArgumentParser(description='Export RealPLKSR to ONNX via Spandrel') + parser.add_argument('--checkpoint', required=True) + parser.add_argument('--output', required=True) + parser.add_argument('--scale', type=int, required=True, choices=[2, 3, 4]) + parser.add_argument('--height', type=int, default=256) + parser.add_argument('--width', type=int, default=256) + parser.add_argument('--opset', type=int, default=20) + parser.add_argument('--fp16', action='store_true', + help='convert weights to FP16 after export (default: FP32)') + parser.add_argument('--static', action='store_true', + help='bake input height/width into the graph ' + '(disables dynamic shape axes)') + args = parser.parse_args() + + convert(args.checkpoint, args.output, args.scale, + height=args.height, width=args.width, + opset=args.opset, fp16=args.fp16, static=args.static) + + +if __name__ == '__main__': + main() diff --git a/models/upscale-realplksr/demo.py b/models/upscale-realplksr/demo.py new file mode 100644 index 0000000..93501ea --- /dev/null +++ b/models/upscale-realplksr/demo.py @@ -0,0 +1,187 @@ +# Demo: run the RealPLKSR x4 ONNX model on an image. +# +# The ONNX export is static (256x256 input -> 1024x1024 output). +# The demo tiles the input, mirror-pads tile edges, and stitches with +# overlap trimming — matching what darktable does at runtime with +# OVERLAP_UPSCALE=16. + +import argparse +import glob +import os +import sys +import time + +import cv2 +import numpy as np +import onnxruntime as ort + + +def _onnx_type_to_numpy(type_str: str): + """Map an ONNX type string ('tensor(float)', 'tensor(float16)') to numpy.""" + if "float16" in type_str: + return np.float16 + return np.float32 + + +def _detect_scale_and_tile(session) -> tuple[int, int, np.dtype]: + """Infer scale, input tile size, and input dtype from the ONNX session. + + Reads the static input shape and runs a dummy inference to read the + output shape; scale = output_h / input_h. Returns the input dtype too + so callers can match it (FP16 models reject float32 input). + """ + inp = session.get_inputs()[0] + in_shape = inp.shape + tile_h = int(in_shape[2]) if isinstance(in_shape[2], int) else 256 + tile_w = int(in_shape[3]) if isinstance(in_shape[3], int) else 256 + assert tile_h == tile_w, f"non-square tile: {tile_h}x{tile_w}" + + in_dtype = _onnx_type_to_numpy(inp.type) + dummy = np.zeros((1, 3, tile_h, tile_w), dtype=in_dtype) + out = session.run(None, {inp.name: dummy})[0] + scale = out.shape[2] // tile_h + return scale, tile_h, in_dtype + + +def _run_tiled(session, input_name, arr: np.ndarray, + tile_size: int, scale: int, in_dtype: np.dtype = np.float32, + overlap: int = 16) -> np.ndarray: + """Tiled inference with mirror-padded edges and overlap-trimmed stitching. + + `arr` is (1, 3, H, W) float32. Returns (1, 3, H*scale, W*scale) float32. + Tiles are cast to `in_dtype` before inference (FP16 models need float16). + + step = T - 2·overlap; each tile reads a T×T input window with `overlap` + border on each side (mirror-padded at the image boundary), and only the + core (step·scale × step·scale) region of each tile's output is written + to the final image — which keeps tile seams seamless. + + overlap=16 matches darktable's OVERLAP_UPSCALE constant. + """ + _, _, H, W = arr.shape + T = tile_size + O = overlap + S = scale + step = T - 2 * O + assert step > 0, "tile_size must exceed 2 * overlap" + + n_y = (H + step - 1) // step + n_x = (W + step - 1) // step + + pad_before = O + pad_after_y = max(O, (n_y - 1) * step + T - H - O) + pad_after_x = max(O, (n_x - 1) * step + T - W - O) + padded = np.pad( + arr, + ((0, 0), (0, 0), (pad_before, pad_after_y), (pad_before, pad_after_x)), + mode="reflect", + ) + + out = np.zeros((1, 3, H * S, W * S), dtype=np.float32) + for ty in range(n_y): + core_y = ty * step + core_h = min(step, H - core_y) + for tx in range(n_x): + core_x = tx * step + core_w = min(step, W - core_x) + tile = padded[:, :, core_y:core_y + T, core_x:core_x + T] + tile = np.ascontiguousarray(tile, dtype=in_dtype) + [tile_out] = session.run(None, {input_name: tile}) + out[:, :, + core_y * S:(core_y + core_h) * S, + core_x * S:(core_x + core_w) * S] = \ + tile_out[:, :, + O * S:(O + core_h) * S, + O * S:(O + core_w) * S].astype(np.float32) + return out + + +def run_inference(model_path, input_path, output_path, max_size=512, + overlap=16): + if not os.path.exists(model_path): + raise FileNotFoundError(f"Model not found at {model_path}") + if not os.path.exists(input_path): + raise FileNotFoundError(f"Input image not found at {input_path}") + + t0 = time.perf_counter() + print(f"Loading ONNX model: {model_path}") + session = ort.InferenceSession(model_path, providers=["CPUExecutionProvider"]) + + scale, tile_size, in_dtype = _detect_scale_and_tile(session) + input_name = session.get_inputs()[0].name + print(f" Scale: x{scale}") + print(f" Tile size: {tile_size}x{tile_size}") + print(f" Input dtype: {np.dtype(in_dtype).name}") + + print(f"Reading image: {input_path}") + img = cv2.imread(input_path) + if img is None: + raise ValueError(f"Could not read image: {input_path}") + h, w = img.shape[:2] + print(f" Original size: {w}x{h}") + if max_size > 0 and max(h, w) > max_size: + s = max_size / max(h, w) + img = cv2.resize(img, (int(w * s), int(h * s)), interpolation=cv2.INTER_AREA) + print(f" Resized to: {img.shape[1]}x{img.shape[0]}") + + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + arr = img.astype(np.float32) / 255.0 + arr = arr.transpose(2, 0, 1)[np.newaxis] + + print(f"Running tiled inference (T={tile_size}, overlap={overlap}, scale={scale})...") + output = _run_tiled(session, input_name, arr, + tile_size=tile_size, scale=scale, + in_dtype=in_dtype, overlap=overlap) + + output = output[0].transpose(1, 2, 0) + output = np.clip(output, 0, 1) + output = (output * 255.0).astype(np.uint8) + output = cv2.cvtColor(output, cv2.COLOR_RGB2BGR) + + out_h, out_w = output.shape[:2] + print(f" Output size: {out_w}x{out_h}") + + print(f"Saving result to: {output_path}") + cv2.imwrite(output_path, output) + print(f"Total: {time.perf_counter() - t0:.3f}s") + + +def demo(model_dir, image, output, **kwargs): + """Entry point for programmatic demo. Runs all *.onnx in model_dir.""" + max_size = kwargs.get("max_size", 512) + overlap = kwargs.get("overlap", 16) + models = sorted(glob.glob(os.path.join(model_dir, "*.onnx"))) + if not models: + raise FileNotFoundError(f"No ONNX models found in {model_dir}") + base, ext = os.path.splitext(output) + for model_path in models: + model_name = os.path.splitext(os.path.basename(model_path))[0] + output_path = f"{base}_{model_name}{ext}" + print(f"\n--- {model_name} ---") + run_inference(model_path, image, output_path, + max_size=max_size, overlap=overlap) + + +def main(): + parser = argparse.ArgumentParser(description='Run RealPLKSR ONNX Demo') + parser.add_argument('--model', type=str) + parser.add_argument('--model-dir', type=str) + parser.add_argument('--image', type=str, required=True) + parser.add_argument('--output', type=str, required=True) + parser.add_argument('--max-size', type=int, default=512) + parser.add_argument('--overlap', type=int, default=16) + args = parser.parse_args() + + if args.model_dir: + demo(args.model_dir, args.image, args.output, + max_size=args.max_size, overlap=args.overlap) + elif args.model: + run_inference(args.model, args.image, args.output, + max_size=args.max_size, overlap=args.overlap) + else: + print("Error: provide --model or --model-dir") + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/models/upscale-realplksr/model.yaml b/models/upscale-realplksr/model.yaml new file mode 100644 index 0000000..98083de --- /dev/null +++ b/models/upscale-realplksr/model.yaml @@ -0,0 +1,59 @@ +id: upscale-realplksr +name: "upscale realplksr" +description: "RealPLKSR 2x and 4x super-resolution, robust to typical photo artefacts (noise, blur, compression)" +task: upscale +version: "0.2" +backend: onnx +tiling: true +type: multi +dep_group: realplksr +coreml_format: mlprogram + +attributes: + # RealPLKSR is a pure-CNN architecture (Partial Large Kernel) with no + # window-attention constraints; any input multiple of 4 works. Tile + # sizes match BSRGAN's pattern for predictable cost — x2 from 512, + # x4 from 256, both emitting 1024x1024 output tiles. + model_x2: + input_sizes: [512] + model_x4: + input_sizes: [256] + +model_card: + long_description: "RealPLKSR (Real-world Partial Large Kernel CNN for SR). Faithful 2x or 4x upscaling that preserves detail without smoothing or inventing texture. Best for clean sources like edited RAW exports or high-quality scans" + scope: "image upscaling (2x and 4x super-resolution, robust to noise/blur/compression artefacts)" + author: "Dongheon Lee et al. (Machine Intelligence Laboratory, University of Seoul)" + source: "https://github.com/dslisleedh/PLKSR" + paper: "https://arxiv.org/abs/2404.11848" + license: "MIT" + training_data: "realistic degradation pipeline (neosr framework)" + training_data_license: "see neosr / RealESRGAN degradation references" + notes: "trained on DF2K (DIV2K + Flickr2K) per neosr common practice; Flickr2K does not carry an explicit open-source license" + +checkpoints: + - url: "https://drive.google.com/file/d/1GAdf5VOqYa5ntswT9sYsKKZ2Z7OQp7gO/view" + path: "temp/upscale-realplksr/2x_realplksr_mssim_pretrain.pth" + - url: "https://drive.google.com/file/d/12ek1vitEporWc5qqaYo6AMy0-RYlRqu8/view" + path: "temp/upscale-realplksr/4x_realplksr_mssim_pretrain.pth" + +convert: + - script: convert.py + args: + checkpoint: "{temp}/2x_realplksr_mssim_pretrain.pth" + output: "{output}/model_x2.onnx" + scale: 2 + opset: 20 + height: 512 + width: 512 + static: true + fp16: false + - script: convert.py + args: + checkpoint: "{temp}/4x_realplksr_mssim_pretrain.pth" + output: "{output}/model_x4.onnx" + scale: 4 + opset: 20 + height: 256 + width: 256 + static: true + fp16: false diff --git a/pyproject.toml b/pyproject.toml index a58ba89..27d678a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,12 @@ segnext = [ bsrgan = [ {include-group = "core"}, ] +realplksr = [ + {include-group = "core"}, + "onnxconverter-common", + "spandrel>=0.4.0", + "safetensors", +] sam21 = [ {include-group = "core"}, "torch>=2.5.1", @@ -77,6 +83,7 @@ all-models = [ {include-group = "rawnind"}, {include-group = "segnext"}, {include-group = "bsrgan"}, + {include-group = "realplksr"}, {include-group = "sam21"}, ] eval = [ diff --git a/uv.lock b/uv.lock index 19f65bf..b14b052 100644 --- a/uv.lock +++ b/uv.lock @@ -251,9 +251,11 @@ all-models = [ { name = "pytorch-msssim" }, { name = "rawpy" }, { name = "requests" }, + { name = "safetensors" }, { name = "scikit-image" }, { name = "scipy" }, { name = "setuptools" }, + { name = "spandrel" }, { name = "tb-nightly" }, { name = "tifffile" }, { name = "torch" }, @@ -330,6 +332,18 @@ rawnind = [ { name = "torch" }, { name = "torchvision" }, ] +realplksr = [ + { name = "numpy" }, + { name = "onnx" }, + { name = "onnxconverter-common" }, + { name = "onnxruntime" }, + { name = "onnxsim" }, + { name = "opencv-python-headless" }, + { name = "safetensors" }, + { name = "spandrel" }, + { name = "torch" }, + { name = "torchvision" }, +] sam21 = [ { name = "hydra-core" }, { name = "iopath" }, @@ -383,9 +397,11 @@ all-models = [ { name = "pytorch-msssim" }, { name = "rawpy" }, { name = "requests" }, + { name = "safetensors" }, { name = "scikit-image" }, { name = "scipy" }, { name = "setuptools" }, + { name = "spandrel", specifier = ">=0.4.0" }, { name = "tb-nightly" }, { name = "tifffile" }, { name = "torch", specifier = ">=2.2.0,<2.7" }, @@ -463,6 +479,18 @@ rawnind = [ { name = "torch", specifier = ">=2.2.0,<2.7" }, { name = "torchvision", specifier = ">=0.17.0,<0.22" }, ] +realplksr = [ + { name = "numpy", specifier = "<2.0" }, + { name = "onnx", specifier = ">=1.10.0" }, + { name = "onnxconverter-common" }, + { name = "onnxruntime" }, + { name = "onnxsim" }, + { name = "opencv-python-headless" }, + { name = "safetensors" }, + { name = "spandrel", specifier = ">=0.4.0" }, + { name = "torch", specifier = ">=2.2.0,<2.7" }, + { name = "torchvision", specifier = ">=0.17.0,<0.22" }, +] sam21 = [ { name = "hydra-core", specifier = ">=1.3.2" }, { name = "iopath", specifier = ">=0.1.10" }, @@ -498,6 +526,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/ec/fa6963f1198172c2b75c9ab6ecefb3045991f92f75f5eb41b6621b198123/easydict-1.13-py3-none-any.whl", hash = "sha256:6b787daf4dcaf6377b4ad9403a5cee5a86adbc0ca9a5bcf5410e9902002aeac2", size = 6804, upload-time = "2024-03-04T12:04:39.508Z" }, ] +[[package]] +name = "einops" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, +] + [[package]] name = "filelock" version = "3.25.2" @@ -1466,6 +1503,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, +] + [[package]] name = "scikit-image" version = "0.26.0" @@ -1573,6 +1632,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "spandrel" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "einops" }, + { name = "numpy" }, + { name = "safetensors" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/8f/ab4565c23dd67a036ab72101a830cebd7ca026b2fddf5771bbf6284f6228/spandrel-0.4.2.tar.gz", hash = "sha256:fefa4ea966c6a5b7721dcf24f3e2062a5a96a395c8bedcb570fb55971fdcbccb", size = 247544, upload-time = "2026-02-21T01:52:26.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/31/411ea965835534c43d4b98d451968354876e0e867ea1fd42669e4cca0732/spandrel-0.4.2-py3-none-any.whl", hash = "sha256:6c93e3ecbeb0e548fd2df45a605472b34c1614287c56b51bb33cdef7ae5235b5", size = 320811, upload-time = "2026-02-21T01:52:25.015Z" }, +] + [[package]] name = "stringzilla" version = "4.6.0"