Logolabs/inkvec-denoiser-001

Model

Inkvec Denoiser (inkvec-denoiser-001)

0

21 commits

1 linked in READMEs

updated Sep 18, 2026

See the code
denoising
image-restoration
image-to-image
jpeg-artifact-removal
onnx
pytorch
vectorization

README

LogoLabs

Inkvec Denoiser (inkvec-denoiser-001)

Inkvec Denoiser β€” LogoLabs research release

A small (19.7M-parameter) image-restoration network that removes JPEG, WebP and AI-decoder (VAE round-trip) damage from logos, icons and other flat artwork before vectorising it. It is the standalone release of the pre-trace "restorer" shipped inside Inkvec, LogoLabs' raster-to-vector tracer β€” useful on its own for anyone who wants a JPEG/WebP cleanup pass tuned for flat, few-colour art rather than photographs.

Licence: Both the model weights (restorer.onnx) and the accompanying code are released under the Apache-2.0 Licence. Commercial, independent, and academic use are fully supported.

What it's for

Feed it a raster that has been through JPEG or WebP compression, or decoded out of a diffusion model's VAE, and it hands back a same-size image with the ringing, block noise and decode softness reduced β€” sharper edges, flatter interiors, colours pulled back toward what a clean render of the same artwork would have. It was trained and evaluated specifically as a pre-processor for a vectoriser, not for photographs and not for general-purpose super-resolution: the loss and the evaluation both score how much better a boundary-tracing pipeline does on the output, not PSNR or a perceptual metric in isolation (see Evaluation).

Architecture

ConvNeXt-U-Net Restorer Architecture

ConvNeXtRestorer, a 5-scale ConvNeXt U-Net (source: svg_depth/model_v4.py, class ConvNeXtRestorer):

  • Encoder: 5 hierarchical scales (channel widths 48/96/192/384/512, depths 2/2/3/4/4), built from Central Difference Convolution (CDC) blocks β€” a conv variant that also computes a local gradient/flux term, meant to keep the network aware of edges through the downsampling stack. Effective receptive field above 700 px at the 16x bottleneck.
  • Decoder: UpWithFiLM β€” four upsampling stages with skip connections, each modulated by a FiLM (feature-wise linear modulation) vector computed from a global average-pooled summary of the bottleneck, so the decoder can condition on whole-image context (e.g. "this image is 97% white background") and not just local features.
  • Output head: a 3-channel conv head. This checkpoint was trained with residual=True: the network predicts x_out = x_in + head(h) with a zero-initialised head, so at initialisation (and wherever the head has nothing useful to add) it is the identity β€” clean input passes through unchanged by construction. Output is clamped to [0, 1] at inference; training sees the unclamped value so gradients survive an overshoot.

Parameter count: 19,664,163 (19.7M) β€” counted directly from the ONNX export's initializers (sum of numel across graph.initializer), not copied from a training log.

Files

filewhat it is
restorer.onnxThe network, ONNX opset 17. Not committed to this repo's git history (see .gitignore); regenerate it locally with prepare_release.py, or download it from this Hugging Face repo directly.
SHA256SUMSChecksum of restorer.onnx as shipped.
prepare_release.pyCopies the weights from the inkvec source tree and writes SHA256SUMS.

Input / output contract

  • Input: tensor named image, shape [1, 3, height, width], float32, values in [0, 1], RGB, NCHW. Height and width must each be a multiple of 16 (four 2x downsamplings) β€” pad with edge-replicate padding on the bottom and right only (not centred) if they are not, i.e. F.pad(x, (0, pad_w, 0, pad_h), mode="replicate") in PyTorch, then crop the output back to the original size. This is exactly what crates/inkvec-restore/src/planar.rs does in the Rust build.
  • Output: tensor named restored, same shape, float32. Already clamped to [0, 1] β€” no further clamping needed, but see the recommended post-processing below.
  • Both spatial axes are dynamic (dynamic_axes on export), so one exported graph runs at any padded resolution; there is no fixed input size baked in.

Recommended post-processing (do this β€” the reference implementation does)

The network's own L1 loss has zero gradient once its internal clamp is active, so it settles a few levels short of true black/white on strongly-clamped pixels (#fdffff rather than pure white, #040101 rather than pure black β€” see Known defect, fixed at inference below). Two cheap fixes, applied in this order, both implemented in crates/inkvec-restore/src/lib.rs:

  1. Quantise to 8-bit: round(clamp(x, 0, 1) * 255) / 255. Every accuracy number below was measured on output read back through an 8-bit PNG, so quantising in-process keeps you on the recipe that was actually validated.
  2. Snap near-extremes: for each pixel, if every channel is within 6/255 of 1.0, set all three to 1.0; if every channel is within 6/255 of 0.0, set all three to 0.0. Leave everything else untouched. (SNAP_LEVELS = 6 in the Rust source.)

Usage

Python (onnxruntime)

import numpy as np
import onnxruntime as ort
from PIL import Image

MULTIPLE = 16
SNAP_LEVELS = 6 / 255.0

def restore(sess, rgb01: np.ndarray) -> np.ndarray:
    """rgb01: float32 HWC in [0, 1]. Returns the same shape."""
    h, w, _ = rgb01.shape
    ph, pw = -(-h // MULTIPLE) * MULTIPLE, -(-w // MULTIPLE) * MULTIPLE  # round up to x16
    padded = np.pad(rgb01, ((0, ph - h), (0, pw - w), (0, 0)), mode="edge")
    chw = padded.transpose(2, 0, 1)[None].astype(np.float32)  # 1x3xHxW
    (out,) = sess.run(["restored"], {"image": chw})
    out = out[0].transpose(1, 2, 0)[:h, :w]                    # crop back, HWC

    out = np.round(np.clip(out, 0, 1) * 255) / 255              # quantise to 8-bit
    lo, hi = out <= SNAP_LEVELS, out >= 1 - SNAP_LEVELS
    snap_hi = hi.all(axis=-1, keepdims=True)
    snap_lo = lo.all(axis=-1, keepdims=True)
    out = np.where(snap_hi, 1.0, np.where(snap_lo, 0.0, out))
    return out.astype(np.float32)

sess = ort.InferenceSession("restorer.onnx", providers=["CPUExecutionProvider"])
img = np.asarray(Image.open("damaged.jpg").convert("RGB"), dtype=np.float32) / 255.0
clean = restore(sess, img)
Image.fromarray((clean * 255 + 0.5).astype(np.uint8)).save("restored.png")

From Inkvec

inkvec damaged.jpg -o clean.svg --restore on
# or let it decide for itself:
inkvec damaged.jpg -o clean.svg --restore auto

Requires the CLI built with the restore-model (ONNX Runtime) or restore-burn (pure Rust) feature β€” see the main README. --restore on always restores first; --restore auto traces once, checks whether the trace disagrees with the input where it claims to be flat, and only restores and retraces if it looks damaged. Restoring forces lossy/noise-aware intake for the retrace, the same way --lossy on does.

Provenance: checkpoint and export

  • Checkpoint: svg_depth/runs/restorer_widermask_2492368/last.pt, training step 12,000, run name restorer_widermask. Initialised from an earlier run in the same lineage, restorer_resid_flat_512mix_2484738/last.pt, and fine-tuned further with a wider masking augmentation (profile=production, vae_prob=0.15, clean_prob=0.25, flat_weight=0.02).
  • Export: svg_depth/export_restorer_onnx.py, run against that checkpoint. Two inference-only rewrites, both numerically verified during export, not just asserted:
    1. The CDC blocks' central-difference convolution is reparameterised into a single plain grouped convolution (CDCBlock.reparameterize()), replacing a conv-then-subtract with one conv.
    2. The decoder's upsampling is exported as a static scale_factor=2 resize instead of a size=skip.shape[-2:] resize β€” the two are mathematically identical once the input is padded to a multiple of 16 (every skip connection is then exactly 2x the tensor being upsampled), but the size-based form exports a Shape/Gather/Resize chain that ONNX importers other than PyTorch's own handle unevenly.
    • Parity, reproduced independently for this release (not copied from a log): re-running export_restorer_onnx.py against last.pt produces a byte-identical .onnx file (sha256 matches SHA256SUMS), and onnxruntime vs. unmodified PyTorch on two input shapes (256x256, 192x320) agree to max absolute difference 1.31e-6 (mean ~1.6e-7) β€” a rounding error many times smaller than a single 8-bit level (0.0039).
    • Opset 17. 15 op types in the exported graph: Add, Clip, Concat, Constant, Conv, Div, Gemm, Identity, Mul, ReduceMean, Resize, Sigmoid, Slice, Sqrt, Unsqueeze.

Known defect, fixed at inference (not retrained)

Two inference-time defects were found and are corrected by the recommended post-processing above rather than by retraining (a retrain that fixes them structurally is recommended future work β€” see Limitations):

  1. Clamp bias. The head is direct-output with an eval-time clamp; L1 has zero gradient once the clamp is saturated, so the network settles a few levels short of true black/white (#fdffff / #040101). On a 90%-background image this alone dominates mean colour error. Fixed by the extreme-snap step above.
  2. Global-context wash. The FiLM conditioning vector is a global average over the bottleneck; at whole-image inference on a mostly-white 512 px image that statistic is out of the training crop distribution, and the decoder paints a faint grey wash over the background (correlates with white-pixel fraction, r=+0.41). Tiling inference at the training crop size (128 px, 32 px overlap) fixes this β€” median error on true-white pixels drops from 0.15 levels (whole-image) to 0.01 (tiled), with no visible seams (worst horizontal step at a seam boundary measured at 1.12x the non-seam baseline, i.e. negligible). The production Rust/Python paths described above trace at whatever resolution the input already is and do not currently tile; tiling is what the accuracy numbers below used to get the reported gains, and batching the tile loop (measured: 25 tiles in one forward call, 512 px, 0.43 s vs. 1.08 s looped) is flagged as unfinished work for anyone picking this up.

Evaluation

Benchmark Performance Across Degradation Types

End-to-End Visual Recovery

All numbers below are from docs/research/experiments-2026-09-12.md Β§10p in the Inkvec repository ("The restorer works; what was wrong with it; the five ink rules; the remaining gate", 2026-09-15) β€” read that document for the full experimental detail, including the earlier (superseded) diagnosis in Β§10o.

End to end (restorer_ship_eval.py, 144 image-format pairs across 48 images; damaged input, restorer tiled + extreme-snapped + traced with lenient intake, vs. the shipped tracer on the same damaged input with no restoration):

dE00DISTSparameter count
restored, tiled + snap + lenient intake-28%-53%-31%

Per format: JPEG q40 -15%/-57%/-45%; WebP q70 -24%/-68%/-24%; VAE round-trip -20%/-38%/-21%. Every content class tested (fonts, stock art, emoji, brand logos, diagrams, icons) improved on every metric.

The clean-input gate (restorer_clean_eval.py, same 48 images, undamaged): dE00 and DISTS both get worse if the restorer runs unconditionally on clean input (+92% dE00, +73% DISTS in the worst-case unconditional arm). This is why Inkvec's --restore defaults to off, and why auto measures the input before deciding rather than always restoring.

Training data

Clean training targets are rendered from SVGs drawn from a pooled corpus (source: svg_depth/data_v3.py, FullPool/SOURCES), reading four named third-party datasets, each split into a "kept" and a "null"-tagged partition in the pool:

  • SVG-Stack ("mixed web" SVGs)
  • SVG-Icons
  • SVG-Emoji
  • FIGR-SVG (its "null"-tagged partition only)

The training script (svg_depth/train_fixer.py) renders these to clean raster targets at multiple crop sizes (128/256/384/512 px) and synthesises degraded inputs on the fly (degrade(): random JPEG q50-90 or WebP re-encoding, 25% left clean) β€” the network never sees a real-world damaged/clean pair, only clean-render/synthetic-damage pairs. Both the model weights and training definitions are released under the Apache-2.0 Licence.

Limitations

  • Costs a few percent of colour accuracy on clean input, which is why it ships as an opt-in / auto-gated pass, not a default (see the clean-input gate above).
  • Trained on opaque renders only. No degradation condition in the training corpus involves alpha; the network is RGB-only and Inkvec composites onto white before calling it. There is no alpha-aware restoration.
  • Global-context wash at whole-image inference unless the caller tiles at the training crop size (see Known defect above) β€” the ONNX graph itself does not tile for you.
  • Never evaluated on genuinely clean, non-synthetic-damage input in production; its behaviour on e.g. a clean photograph or a clean but unusual illustration style is untested.
  • Never run end to end on VAE-decoded input at scale β€” the VAE numbers above come from a smaller paired run than the JPEG/WebP ones.
  • Resolution trained at 384 px on 128 px crops; evaluated mainly at 512 px. A native-resolution sweep was never run.

Citation

@software{inkvec_denoiser2026,
  title  = {Inkvec Denoiser: a JPEG/WebP/VAE-damage restoration network for vectorisation pre-processing},
  author = {LogoLabs and Deleanu, Stefan-Lucian},
  year   = {2026},
  url    = {https://huggingface.co/Logolabs/inkvec-denoiser-001}
}

See also RELEASE_NOTES_restorer.md for the GitHub release draft, and the main Inkvec repository for the tracer this network was built for.

LogoLabs Β· Deleanu, Stefan-Lucian Β· Inkvec Denoiser Β· 2026

Acknowledgements

EuroHPC JU and Arrhenius β€” Project EHPC-AIF-2026PG01-907; Arrhenius GPU at NAISS, Sweden

We acknowledge EuroHPC JU for awarding the project ID EHPC-AIF-2026PG01-907 access to resources on Arrhenius GPU at NAISS, Sweden. The Arrhenius system is operated by the National Academic Infrastructure for Supercomputing in Sweden (NAISS). Compute time on Arrhenius was instrumental in the training and evaluation of this denoiser/restorer model.

Contributors

Incorporo-user

21 commits

Logolabs/inkvec-denoiser-001

Model

Inkvec Denoiser (inkvec-denoiser-001)

0

21 commits

1 linked in READMEs

updated Sep 18, 2026

See the code
denoising
image-restoration
image-to-image
jpeg-artifact-removal
onnx
pytorch
vectorization

README

LogoLabs

Inkvec Denoiser (inkvec-denoiser-001)

Inkvec Denoiser β€” LogoLabs research release

A small (19.7M-parameter) image-restoration network that removes JPEG, WebP and AI-decoder (VAE round-trip) damage from logos, icons and other flat artwork before vectorising it. It is the standalone release of the pre-trace "restorer" shipped inside Inkvec, LogoLabs' raster-to-vector tracer β€” useful on its own for anyone who wants a JPEG/WebP cleanup pass tuned for flat, few-colour art rather than photographs.

Licence: Both the model weights (restorer.onnx) and the accompanying code are released under the Apache-2.0 Licence. Commercial, independent, and academic use are fully supported.

What it's for

Feed it a raster that has been through JPEG or WebP compression, or decoded out of a diffusion model's VAE, and it hands back a same-size image with the ringing, block noise and decode softness reduced β€” sharper edges, flatter interiors, colours pulled back toward what a clean render of the same artwork would have. It was trained and evaluated specifically as a pre-processor for a vectoriser, not for photographs and not for general-purpose super-resolution: the loss and the evaluation both score how much better a boundary-tracing pipeline does on the output, not PSNR or a perceptual metric in isolation (see Evaluation).

Architecture

ConvNeXt-U-Net Restorer Architecture

ConvNeXtRestorer, a 5-scale ConvNeXt U-Net (source: svg_depth/model_v4.py, class ConvNeXtRestorer):

  • Encoder: 5 hierarchical scales (channel widths 48/96/192/384/512, depths 2/2/3/4/4), built from Central Difference Convolution (CDC) blocks β€” a conv variant that also computes a local gradient/flux term, meant to keep the network aware of edges through the downsampling stack. Effective receptive field above 700 px at the 16x bottleneck.
  • Decoder: UpWithFiLM β€” four upsampling stages with skip connections, each modulated by a FiLM (feature-wise linear modulation) vector computed from a global average-pooled summary of the bottleneck, so the decoder can condition on whole-image context (e.g. "this image is 97% white background") and not just local features.
  • Output head: a 3-channel conv head. This checkpoint was trained with residual=True: the network predicts x_out = x_in + head(h) with a zero-initialised head, so at initialisation (and wherever the head has nothing useful to add) it is the identity β€” clean input passes through unchanged by construction. Output is clamped to [0, 1] at inference; training sees the unclamped value so gradients survive an overshoot.

Parameter count: 19,664,163 (19.7M) β€” counted directly from the ONNX export's initializers (sum of numel across graph.initializer), not copied from a training log.

Files

filewhat it is
restorer.onnxThe network, ONNX opset 17. Not committed to this repo's git history (see .gitignore); regenerate it locally with prepare_release.py, or download it from this Hugging Face repo directly.
SHA256SUMSChecksum of restorer.onnx as shipped.
prepare_release.pyCopies the weights from the inkvec source tree and writes SHA256SUMS.

Input / output contract

  • Input: tensor named image, shape [1, 3, height, width], float32, values in [0, 1], RGB, NCHW. Height and width must each be a multiple of 16 (four 2x downsamplings) β€” pad with edge-replicate padding on the bottom and right only (not centred) if they are not, i.e. F.pad(x, (0, pad_w, 0, pad_h), mode="replicate") in PyTorch, then crop the output back to the original size. This is exactly what crates/inkvec-restore/src/planar.rs does in the Rust build.
  • Output: tensor named restored, same shape, float32. Already clamped to [0, 1] β€” no further clamping needed, but see the recommended post-processing below.
  • Both spatial axes are dynamic (dynamic_axes on export), so one exported graph runs at any padded resolution; there is no fixed input size baked in.

Recommended post-processing (do this β€” the reference implementation does)

The network's own L1 loss has zero gradient once its internal clamp is active, so it settles a few levels short of true black/white on strongly-clamped pixels (#fdffff rather than pure white, #040101 rather than pure black β€” see Known defect, fixed at inference below). Two cheap fixes, applied in this order, both implemented in crates/inkvec-restore/src/lib.rs:

  1. Quantise to 8-bit: round(clamp(x, 0, 1) * 255) / 255. Every accuracy number below was measured on output read back through an 8-bit PNG, so quantising in-process keeps you on the recipe that was actually validated.
  2. Snap near-extremes: for each pixel, if every channel is within 6/255 of 1.0, set all three to 1.0; if every channel is within 6/255 of 0.0, set all three to 0.0. Leave everything else untouched. (SNAP_LEVELS = 6 in the Rust source.)

Usage

Python (onnxruntime)

import numpy as np
import onnxruntime as ort
from PIL import Image

MULTIPLE = 16
SNAP_LEVELS = 6 / 255.0

def restore(sess, rgb01: np.ndarray) -> np.ndarray:
    """rgb01: float32 HWC in [0, 1]. Returns the same shape."""
    h, w, _ = rgb01.shape
    ph, pw = -(-h // MULTIPLE) * MULTIPLE, -(-w // MULTIPLE) * MULTIPLE  # round up to x16
    padded = np.pad(rgb01, ((0, ph - h), (0, pw - w), (0, 0)), mode="edge")
    chw = padded.transpose(2, 0, 1)[None].astype(np.float32)  # 1x3xHxW
    (out,) = sess.run(["restored"], {"image": chw})
    out = out[0].transpose(1, 2, 0)[:h, :w]                    # crop back, HWC

    out = np.round(np.clip(out, 0, 1) * 255) / 255              # quantise to 8-bit
    lo, hi = out <= SNAP_LEVELS, out >= 1 - SNAP_LEVELS
    snap_hi = hi.all(axis=-1, keepdims=True)
    snap_lo = lo.all(axis=-1, keepdims=True)
    out = np.where(snap_hi, 1.0, np.where(snap_lo, 0.0, out))
    return out.astype(np.float32)

sess = ort.InferenceSession("restorer.onnx", providers=["CPUExecutionProvider"])
img = np.asarray(Image.open("damaged.jpg").convert("RGB"), dtype=np.float32) / 255.0
clean = restore(sess, img)
Image.fromarray((clean * 255 + 0.5).astype(np.uint8)).save("restored.png")

From Inkvec

inkvec damaged.jpg -o clean.svg --restore on
# or let it decide for itself:
inkvec damaged.jpg -o clean.svg --restore auto

Requires the CLI built with the restore-model (ONNX Runtime) or restore-burn (pure Rust) feature β€” see the main README. --restore on always restores first; --restore auto traces once, checks whether the trace disagrees with the input where it claims to be flat, and only restores and retraces if it looks damaged. Restoring forces lossy/noise-aware intake for the retrace, the same way --lossy on does.

Provenance: checkpoint and export

  • Checkpoint: svg_depth/runs/restorer_widermask_2492368/last.pt, training step 12,000, run name restorer_widermask. Initialised from an earlier run in the same lineage, restorer_resid_flat_512mix_2484738/last.pt, and fine-tuned further with a wider masking augmentation (profile=production, vae_prob=0.15, clean_prob=0.25, flat_weight=0.02).
  • Export: svg_depth/export_restorer_onnx.py, run against that checkpoint. Two inference-only rewrites, both numerically verified during export, not just asserted:
    1. The CDC blocks' central-difference convolution is reparameterised into a single plain grouped convolution (CDCBlock.reparameterize()), replacing a conv-then-subtract with one conv.
    2. The decoder's upsampling is exported as a static scale_factor=2 resize instead of a size=skip.shape[-2:] resize β€” the two are mathematically identical once the input is padded to a multiple of 16 (every skip connection is then exactly 2x the tensor being upsampled), but the size-based form exports a Shape/Gather/Resize chain that ONNX importers other than PyTorch's own handle unevenly.
    • Parity, reproduced independently for this release (not copied from a log): re-running export_restorer_onnx.py against last.pt produces a byte-identical .onnx file (sha256 matches SHA256SUMS), and onnxruntime vs. unmodified PyTorch on two input shapes (256x256, 192x320) agree to max absolute difference 1.31e-6 (mean ~1.6e-7) β€” a rounding error many times smaller than a single 8-bit level (0.0039).
    • Opset 17. 15 op types in the exported graph: Add, Clip, Concat, Constant, Conv, Div, Gemm, Identity, Mul, ReduceMean, Resize, Sigmoid, Slice, Sqrt, Unsqueeze.

Known defect, fixed at inference (not retrained)

Two inference-time defects were found and are corrected by the recommended post-processing above rather than by retraining (a retrain that fixes them structurally is recommended future work β€” see Limitations):

  1. Clamp bias. The head is direct-output with an eval-time clamp; L1 has zero gradient once the clamp is saturated, so the network settles a few levels short of true black/white (#fdffff / #040101). On a 90%-background image this alone dominates mean colour error. Fixed by the extreme-snap step above.
  2. Global-context wash. The FiLM conditioning vector is a global average over the bottleneck; at whole-image inference on a mostly-white 512 px image that statistic is out of the training crop distribution, and the decoder paints a faint grey wash over the background (correlates with white-pixel fraction, r=+0.41). Tiling inference at the training crop size (128 px, 32 px overlap) fixes this β€” median error on true-white pixels drops from 0.15 levels (whole-image) to 0.01 (tiled), with no visible seams (worst horizontal step at a seam boundary measured at 1.12x the non-seam baseline, i.e. negligible). The production Rust/Python paths described above trace at whatever resolution the input already is and do not currently tile; tiling is what the accuracy numbers below used to get the reported gains, and batching the tile loop (measured: 25 tiles in one forward call, 512 px, 0.43 s vs. 1.08 s looped) is flagged as unfinished work for anyone picking this up.

Evaluation

Benchmark Performance Across Degradation Types

End-to-End Visual Recovery

All numbers below are from docs/research/experiments-2026-09-12.md Β§10p in the Inkvec repository ("The restorer works; what was wrong with it; the five ink rules; the remaining gate", 2026-09-15) β€” read that document for the full experimental detail, including the earlier (superseded) diagnosis in Β§10o.

End to end (restorer_ship_eval.py, 144 image-format pairs across 48 images; damaged input, restorer tiled + extreme-snapped + traced with lenient intake, vs. the shipped tracer on the same damaged input with no restoration):

dE00DISTSparameter count
restored, tiled + snap + lenient intake-28%-53%-31%

Per format: JPEG q40 -15%/-57%/-45%; WebP q70 -24%/-68%/-24%; VAE round-trip -20%/-38%/-21%. Every content class tested (fonts, stock art, emoji, brand logos, diagrams, icons) improved on every metric.

The clean-input gate (restorer_clean_eval.py, same 48 images, undamaged): dE00 and DISTS both get worse if the restorer runs unconditionally on clean input (+92% dE00, +73% DISTS in the worst-case unconditional arm). This is why Inkvec's --restore defaults to off, and why auto measures the input before deciding rather than always restoring.

Training data

Clean training targets are rendered from SVGs drawn from a pooled corpus (source: svg_depth/data_v3.py, FullPool/SOURCES), reading four named third-party datasets, each split into a "kept" and a "null"-tagged partition in the pool:

  • SVG-Stack ("mixed web" SVGs)
  • SVG-Icons
  • SVG-Emoji
  • FIGR-SVG (its "null"-tagged partition only)

The training script (svg_depth/train_fixer.py) renders these to clean raster targets at multiple crop sizes (128/256/384/512 px) and synthesises degraded inputs on the fly (degrade(): random JPEG q50-90 or WebP re-encoding, 25% left clean) β€” the network never sees a real-world damaged/clean pair, only clean-render/synthetic-damage pairs. Both the model weights and training definitions are released under the Apache-2.0 Licence.

Limitations

  • Costs a few percent of colour accuracy on clean input, which is why it ships as an opt-in / auto-gated pass, not a default (see the clean-input gate above).
  • Trained on opaque renders only. No degradation condition in the training corpus involves alpha; the network is RGB-only and Inkvec composites onto white before calling it. There is no alpha-aware restoration.
  • Global-context wash at whole-image inference unless the caller tiles at the training crop size (see Known defect above) β€” the ONNX graph itself does not tile for you.
  • Never evaluated on genuinely clean, non-synthetic-damage input in production; its behaviour on e.g. a clean photograph or a clean but unusual illustration style is untested.
  • Never run end to end on VAE-decoded input at scale β€” the VAE numbers above come from a smaller paired run than the JPEG/WebP ones.
  • Resolution trained at 384 px on 128 px crops; evaluated mainly at 512 px. A native-resolution sweep was never run.

Citation

@software{inkvec_denoiser2026,
  title  = {Inkvec Denoiser: a JPEG/WebP/VAE-damage restoration network for vectorisation pre-processing},
  author = {LogoLabs and Deleanu, Stefan-Lucian},
  year   = {2026},
  url    = {https://huggingface.co/Logolabs/inkvec-denoiser-001}
}

See also RELEASE_NOTES_restorer.md for the GitHub release draft, and the main Inkvec repository for the tracer this network was built for.

LogoLabs Β· Deleanu, Stefan-Lucian Β· Inkvec Denoiser Β· 2026

Acknowledgements

EuroHPC JU and Arrhenius β€” Project EHPC-AIF-2026PG01-907; Arrhenius GPU at NAISS, Sweden

We acknowledge EuroHPC JU for awarding the project ID EHPC-AIF-2026PG01-907 access to resources on Arrhenius GPU at NAISS, Sweden. The Arrhenius system is operated by the National Academic Infrastructure for Supercomputing in Sweden (NAISS). Compute time on Arrhenius was instrumental in the training and evaluation of this denoiser/restorer model.

Contributors

Incorporo-user

21 commits