LatentOperationNormalizePercentile
Min-max normalization that ignores the weird values
- op
LatentOperationNormalizeMinMax has one fatal flaw: a single outlier sets your 0 and 1 and crushes everything else into a thin gray band. LatentOperationNormalizePercentile is the fix - same stretch-to-a-range idea, but it anchors the range on percentiles instead of the raw min and max, so a few stray extreme values don't get to ruin the whole mapping. If you're normalizing noisy latents, this is the version you actually want.
How it works
v1 = torch.quantile(latent, percentile)
v2 = torch.quantile(latent, 1 - percentile)
lo, hi = min(v1, v2), max(v1, v2)
return (latent - lo) / (hi - lo + eps)
The single input is percentile (FLOAT, default 0.99, range 0–1). With the default 0.99, the node takes the 99th percentile as the top bound and the 1st percentile as the bottom bound - effectively trimming the outer 1% on each side before the stretch. Values outside those bounds still get mapped (they just clamp past 0 or 1, since the formula doesn't clamp them), but they no longer dictate the range. The min/max around the two quantiles is a belt-and-suspenders guard so the math works even if you set percentile below 0.5.
That makes it the robust cousin in the pack's normalization ladder: Normalize (global z-score), NormalizeAlongAxis, LayerNorm (per sample), NormalizeMinMax (raw 0–1), and this - min-max that survives outliers. Where NormalizeMinMax gets burned by one hot channel value, this one just shrugs and stretches the middle 98%.
When you'd use it
Feeding a latent into something that expects a bounded, comparable range - a preview path, a custom op, a control signal - is where this earns its keep, especially if the latent's provenance is messy (came out of a CFG manipulation, an interpolation, or another model's sampler). It's also the gentler way to build that intuition: sweep percentile from 0.99 down toward 0.5 and you watch the mapping get more and more aggressive. The trade-off: it's not a true 0–1 mapping anymore - extremes do poke past the bounds - so don't use it where the consumer requires everything in [0,1]. For that, NormalizeMinMax (accepting the outlier risk) is the literal answer.
The apply gotcha
Zero optional inputs here, one required float, and the output is op of type LATENT_OPERATION - a deferred closure, exactly like every node in this pack. ComfyUI-latent-ops builds operations and ships no apply node, so you need a LATENT_OPERATION consumer (Sonar's SonarApplyLatentOperationCFG is the one that exists) or your own apply node. Plugging op into a VAE Decode gets you a type mismatch. It's the pack's universal first trap and it's not your fault - that's just how the toolkit is shaped.
Install
The usual. ComfyUI Manager → search ComfyUI-latent-ops, or:
cd ComfyUI/custom_nodes
git clone https://github.com/hnmr293/ComfyUI-latent-ops
Restart. No requirements.txt, no model downloads - pure PyTorch, which ComfyUI already ships. This is hnmr293's personal workbench (sd-webui-cutoff, llul), essentially invisible to the community, so the source in latent_ops/normalize.py is your documentation. Everything lands under hnmr/latent_ops.
Inputs (1)
| Name | Type | Default | Description |
|---|---|---|---|
| percentile | FLOAT | 0.99000–1 | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| op | LATENT_OPERATION | — |