Nodes/ComfyUI-lhyNodes/Save UltraHDR Image
ComfyUI Node

Save UltraHDR Image

A JPEG that glows on your phone, and forgets your workflow

By lihaoyun6·Created about a year ago·Updated about 9 hours ago· 21
Save UltraHDR Image
  • images
  • hdr_params
    ◄filename_prefixUltraHDR►

    What it actually does

    Every other save node in ComfyUI hands you a PNG with the entire workflow welded inside it. This one hands you a JPEG that looks brighter on a modern phone - and the author's own node description says the quiet part out loud: "UltraHDR JPG image don't contain workflow metadata!"

    What you get is an Ultra HDR / gain-map JPEG, the same container Android phones and Google Photos use. On an HDR display, in a viewer that understands gain maps, the highlights sit roughly 2.3 stops above normal SDR white. Everywhere else it's a perfectly ordinary JPEG.

    Be clear about what this is, though, because it's easy to oversell. ComfyUI's IMAGE is an 8-bit-ish, sRGB-encoded float tensor in a 0–1 range (post-processing.md). There is no scene-referenced HDR in your decode - the model never had any. So this node runs inverse tone mapping: it guesses a plausible expansion into HDR nits and stores the difference as a gain map. A blown-out window in your source stays blown out, just brighter.

    That's fine. The use case is display, not data: wallpapers, photo-ish shots you'll view on an HDR screen, anything posted somewhere that keeps your original file.

    How the file gets built

    The node is a port of hanfeisun/pyultrahdr (the source says so in its first line), and it does the whole thing in Python with numpy and PIL - no CUDA, no extra models. Six steps:

    1. Save the base JPEG at your quality, with chroma subsampling disabled (4:4:4), so the SDR layer is already about as clean as JPEG gets.
    2. Decode sRGB → linear.
    3. Inverse tone map. Luminance is Rec.709-weighted, and the boost is shadow_boost + (peak_boost - shadow_boost) × L^hi_gamma, where peak_boost is peak_nits ÷ sdr_white. Then linear RGB × white × boost → nits.
    4. Convert BT.709 primaries to BT.2020, the wide-gamut container HDR JPEGs expect.
    5. Compute the gain map as log2(HDR luminance) - log2(SDR luminance), clamped at zero - a one-way lift that can only ever add light.
    6. Pack it: base JPEG, an XMP hdrgm block (holding GainMapMax and HDRCapacityMax = log2(peak_nits / sdr_white)), an MPF APP2 segment declaring a second image, then the grayscale gain-map JPEG appended after it. That's the standard Ultra HDR layout Google's libultrahdr writes.

    The payoff of that structure: nothing else has to change. A viewer that doesn't understand gain maps reads the base JPEG and shows exactly the picture you've been looking at in the node preview.

    The inputs that matter

    • images - the IMAGE batch. You get one .jpg per image in it.
    • filename_prefix - defaults to UltraHDR. It behaves like SaveImage's: slashes make subfolders and the counter auto-increments, so you land UltraHDR_00001_.jpg (trailing underscore included) in ComfyUI/output/.
    • hdr_params (optional) - wire an UltraHDRParameters node here. Leave it empty and the node falls back to its built-in defaults (1000 nits peak, 203 white, 1.3 shadow boost, gamma 1.0, quality 92, gain map 85).

    There are no outputs - it's an output node, so ComfyUI caches backwards from it and it always runs when the graph runs (comfyui-node-plumbing.md).

    Installing it

    The pack installs the normal way. In ComfyUI Manager search lhyNodes, or manually:

    cd ComfyUI/custom_nodes
    git clone https://github.com/lihaoyun6/ComfyUI-lhyNodes.git
    python -m pip install -r ComfyUI-lhyNodes/requirements.txt
    

    Then restart ComfyUI. The UltraHDR code itself needs nothing beyond numpy, torch and PIL - but do not skip the requirements step. The pack's loader imports every module in its nodes/ folder, and nodes/nodes.py imports cv2 and ultralytics at the top. Miss those and the whole pack fails to load, UltraHDR nodes included, with an IMPORT FAILED line in the console and no nodes in the menu.

    One navigation note: the README doesn't list these nodes at all - SaveUltraHDRImage is the newest commit in the repo - so type UltraHDR into the node search instead of browsing the tree.

    Where people get burned

    The metadata loss is real and deliberate. JPEG has no PNG text chunks, so the graph can't travel - image-io-metadata.md makes the general point that PNG is the master and JPEG is a derivative. Treat this node as a publishing step: keep the PNG from a normal SaveImage alongside it, and don't expect CivitAI to parse a generation string out of the upload.

    Judge it on an HDR screen, not in the node preview. The preview in the canvas is the SDR base. If you tune parameters staring at that, you're tuning something you can't see.

    Don't use it as an EXR substitute. If the destination is a color-managed comp, you want linear float EXR via something like comfy_oiio, not a gain-map JPEG.

    Watch the OpenCV install on headless boxes. The requirements pull plain opencv-python; when a server or Docker box won't import the pack, mixed cv2 builds or a missing libGL are the usual suspects and the headless build is the standard fix. That's an import failure with no node in the menu - read the console.

    CategorylhyNodes/Image

    Inputs (3)

    NameTypeDefaultDescription
    imagesIMAGE—
    filename_prefixSTRINGUltraHDR—
    hdr_paramsoptHDR_PARAMS—

    Outputs (0)

    No outputs