Skip to content

OpenEXR color data is incorrect when using HalfVector4 #3194

Description

@mhourousha

Prerequisites

  • I have written a descriptive issue title
  • I have verified that I am running the latest version of ImageSharp
  • I have verified if the problem exist in both DEBUG and RELEASE mode
  • I have searched open and closed issues to ensure it has not already been reported

ImageSharp version

4.1.2

Other ImageSharp packages and versions

4.1.2

Environment (Operating system, version and so on)

windows11

.NET Framework version

10

Description

I encountered an issue where OpenEXR image data is being incorrectly altered during loading when using the HalfVector4 floating-point pixel formats

OpenEXR frequently stores High Dynamic Range (HDR) color data in floating-point formats, which means values often exceed the [0, 1] range. However, the current loading logic clamps and scales these values, destroying the original raw data.

I traced the issue to two specific areas in the codebase:
In ExrDecoderCore.cs (around line 225):
pixelRow[x] = TPixel.FromAssociatedScaledVector4(pixelValue);
If TPixel is HalfVector4 and the EXR pixel type is half or float, the RGB channels are clamped to [0, 1] and then scaled/biased to [FiniteMinimum, FiniteMaximum]. This breaks the original floating-point data. I believe the clamp and scale operations should be avoided here.

As a side note for other floating point pixel format like RgbaHalf and RgbaVector.The FromAssociatedScaledVector4 method calls FromVector4(Vector4 source). Inside FromVector4, the original values are explicitly clamped to [0, 1]. This also breaks the raw EXR data.

Steps to Reproduce

load a openEXR file in HalfVector4 or HalfVector4P format,then check the pixel value of ToVector4();

Images

No response

Activity

  1. JimBobSquarePants commented on Sep 17, 2026

    @JimBobSquarePants
    Member

    Thanks for the detailed report. I confirm the behavior that you describe. Here is what happens and what I plan to do.

    Why the HalfVector4 values look wrong. HalfVector4 is the XNA/MonoGame packed vector, not a color type. It has no white point, so its scaled range is the full finite binary16 range, [-65504, 65504]. The EXR decoder writes pixels through the scaled conversions, because it must work for every pixel format. ToVector4() then returns the native stored values, not the EXR values. ToScaledVector4() returns the normalized [0, 1] values.

    Why values outside [0, 1] clamp. Scaled values in ImageSharp are normalized to [0, 1], because processors need a fixed range. OpenEXR defines no range for color values, so the decoder cannot normalize HDR data without loss. It uses 1.0 as white and clamps the remaining values. This is a limitation of the current design, and it applies to every pixel format, including RgbaHalf and RgbaVector.

    What I can improve in 4.1.x. Your report led me to an inconsistency. The EXR decoder writes scaled values into the pixel format, but the EXR encoder reads native values. For a pixel format where those two are different, such as HalfVector4, a load and a save writes wrong values to the file. I will change the encoder so that it agrees with the decoder.

    What needs v5. Full HDR support means that the float color formats (RgbaVector, RgbaHalf, RgbaHalfP) keep values outside [0, 1] in their native storage, while their scaled view stays in [0, 1]. That changes the clamp behavior of those formats, and it adds API to PixelTypeInfo. Both are breaking changes, so I am moving this issue to the v5 milestone.

    Until then. Decode EXR data to RgbaHalfP or RgbaHalf, not to HalfVector4. ToVector4() then returns the color values directly. RgbaHalfP stores associated alpha, the same as EXR, so HALF data in [0, 1] decodes exactly. Values more than 1.0 clip at white in 4.x.

  2. added this to the v5.0.0 milestone on Sep 17, 2026
  3. mhourousha commented on Sep 17, 2026

    @mhourousha
    Author

    Thanks for the detailed report. I confirm the behavior that you describe. Here is what happens and what I plan to do.
    Why the HalfVector4 values look wrong. HalfVector4 is the XNA/MonoGame packed vector, not a color type. It has no white point, so its scaled range is the full finite binary16 range, [-65504, 65504]. The EXR decoder writes pixels through the scaled conversions, because it must work for every pixel format. ToVector4() then returns the native stored values, not the EXR values. ToScaledVector4() returns the normalized [0, 1] values.

    Thanks for the incredibly detailed explanation! I completely understand the historical context of the [0, 1] normalized pipeline and why a proper, unified fix for unbounded HDR values requires the breaking changes planned for v5.

    Since v5 might still be a while away, I'd like to propose a pragmatic, low-impact workaround for the 4.1.x branch. Would it be possible to implement a "fast path" specifically for HalfVector4 (for both the Decoder and Encoder) to allow it to act as a pure, lossless data container for OpenEXR?

    The Idea:
    In ExrDecoderCore, could we add a type check like if (typeof(TPixel) == typeof(HalfVector4))?

    If true, we bypass the generic FromAssociatedScaledVector4 pipeline entirely, and directly pack the raw unbounded EXR float values into the struct (effectively just doing a float-to-half precision truncation).

    If false, it falls back to the current clamp/scale logic, keeping all other pixel types and existing tests perfectly intact.

    My use case is reading OpenEXR files to build textures for a rendering pipeline — e.g. HDR environment maps, lighting data — where values well above 1.0 are the whole point.
    Would a targeted exception like this be acceptable for the 4.1.x milestone?

  4. JimBobSquarePants commented on Sep 21, 2026

    @JimBobSquarePants
    Member

    I understand the use case, but I don’t want to introduce a HalfVector4 exception in 4.1.x. Preserving the raw values during decoding would address loading, but it would leave those values inconsistent with the scaled conversions used elsewhere in ImageSharp. That inconsistency would surface again when converting or processing the image.

    The encoder correction described above fixes a mismatch within the existing contract. Your proposal changes that contract specifically for one pixel format and one codec. I’d prefer to address this consistently through the HDR work planned for v5, so preservation of values outside [0, 1] is a supported behavior rather than a special case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions