hdr-conversion 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: hdr-conversion
3
+ Version: 0.1.0
4
+ Summary: tools for converting HDR images between different formats
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: colour-science>=0.4.6
8
+ Requires-Dist: imagecodecs>=2025.11.11
9
+ Requires-Dist: pillow-heif>=0.22.0
10
+ Requires-Dist: pillow>=11.3.0
11
+ Requires-Dist: pyexiftool>=0.5.6
12
+ Description-Content-Type: text/markdown
13
+
14
+ # HDR Format Conversion Tool
15
+
16
+ [简体中文](README-zhCN.md) | [English](README.md)
17
+
18
+ > Note: In alpha stage, API may change frequently. Currently, color conversion may be incorrect.
19
+
20
+ API Reference: [https://jackchou.top/hdr-conversion](https://jackchou.top/hdr-conversion/)
21
+
22
+ ## Project Overview
23
+
24
+ This project provides Python-based research on HDR format parsing and writing, supporting parsing, writing, and conversion of multiple formats including UltraHDR, Adaptive Gainmap (ISO 21496-1), and pure PQ/HLG formats (ISO 22028-5).
25
+
26
+ Note: This project is for research and learning purposes only and does not aim for production readiness.
27
+
28
+ ## Getting Started
29
+
30
+ To install, using `uv` (recommended):
31
+
32
+ ```bash
33
+ uv add hdr-conversion
34
+ ```
35
+
36
+ or use `pip`:
37
+
38
+ ```bash
39
+ pip install hdr-conversion
40
+ ```
41
+
42
+ The package can be imported as follows:
43
+
44
+ ```python
45
+ import hdrconv
46
+ ```
47
+
48
+ ## Features
49
+
50
+ ### Parsing
51
+
52
+ For UltraHDR and Adaptive Gainmap formats, supports structured extraction of:
53
+
54
+ - Main image data
55
+ - Gainmap image data
56
+ - Gainmap metadata
57
+
58
+ For pure PQ/HLG formats, supports extraction of image data and related metadata.
59
+
60
+ ### Writing
61
+
62
+ Writes image data and structured metadata into corresponding formats.
63
+
64
+ UltraHDR and Adaptive Gainmap formats are implemented through manual byte stream editing combined with existing library JPEG encoding capabilities, while pure PQ/HLG formats are implemented through existing libraries.
65
+
66
+ ### Conversion
67
+
68
+ Calculates alternate images based on metadata to enable conversion between Gainmap and pure HDR formats.
69
+
70
+ ## Reference Standards
71
+
72
+ - [UltraHDR](https://developer.android.com/media/platform/hdr-image-format): Version 1.1 released in April 2025
73
+ - [ISO 21496-1](https://www.iso.org/standard/86775.html)
74
+ - [ISO 22028-5](https://www.iso.org/standard/81863.html)
75
+
76
+ ## License
77
+
78
+ MIT. Please refer to the respective LICENSE files for specific format and dependency details.
@@ -0,0 +1,18 @@
1
+ hdrconv/__init__.py,sha256=NROgdFe10SnJuT0PsdVxFwQ5AYCMl9jBIjEMXAWW0O8,342
2
+ hdrconv/convert/__init__.py,sha256=hRpXwiDeJ486uEMzTJ4K425mwR-okcOVjRX7JbEsR0M,745
3
+ hdrconv/convert/apple.py,sha256=8gWAiMXWvPn3vb0m8OEUTJZzhDEBtKOBxq3SGxlZ3jc,3585
4
+ hdrconv/convert/colorspace.py,sha256=xEo9j7dTwzx7cBb2rY0MwmKDyz4hCOIKZU2B5_LqcW8,2504
5
+ hdrconv/convert/gainmap.py,sha256=q0r0kMiEf4H4qfGjpU2jdFqWEidoFlIZdKBXptjRFE4,9455
6
+ hdrconv/convert/transfer.py,sha256=EznALcPcFUBj9_idkwTMpSkxNzE7c0OPU_sg8zUvmro,2875
7
+ hdrconv/core/__init__.py,sha256=maPeWAh0bXJssiu01OFw8JfQ3fFf1Jf5MVhipyte9LY,589
8
+ hdrconv/core/types.py,sha256=cmno3kiPzKPQCup_XxPluzjs8M7OkYmjKsPw-ORO7Ts,5574
9
+ hdrconv/identify/__init__.py,sha256=kMiFBbWgYPthAYBog6MMMFnJ4fyCP4ceY2heLpqHu7E,288
10
+ hdrconv/identify/apple_heic.py,sha256=bnPA9n-Xr4Kl6vkKjS7WQJo-jj4y82TyMr0OdBl1QPk,1557
11
+ hdrconv/io/__init__.py,sha256=e_TVKDTTJDAsd1uL6N3qUQwJR1RtD0S0v3Kd3ceEBJg,542
12
+ hdrconv/io/apple_heic.py,sha256=_DamZpxoc9nxdA80IG5Y3Pkd68hnvgPXs2dAkmgpUx0,6383
13
+ hdrconv/io/iso21496.py,sha256=Qk0TEZ5_gNFFXhPIFH03upvq6AyspCZWVGw4TaCneAg,28713
14
+ hdrconv/io/iso22028.py,sha256=_DBqCImxDHM3ovTrEFdl-2hNEOnWGvjE8LwcS6cf9Tc,3908
15
+ hdr_conversion-0.1.0.dist-info/METADATA,sha256=MQapN8HhE5yvdxmMS_qaPUxyznq3G8ZLmxOjeff5eDs,2271
16
+ hdr_conversion-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
17
+ hdr_conversion-0.1.0.dist-info/licenses/LICENSE,sha256=yt-PreLOGv7lJBruB4S8-HO4yxZSJayyVD9jEMz6I-0,1083
18
+ hdr_conversion-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,8 @@
1
+ The MIT License (MIT)
2
+ Copyright © 2025 <Jack Chou>
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
+
6
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
+
8
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
hdrconv/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """HDR conversion research library.
2
+
3
+ Public usage is via submodules (two-level imports), e.g.:
4
+ - ``hdrconv.io.read_21496``
5
+ - ``hdrconv.convert.gainmap_to_hdr``
6
+ - ``hdrconv.core.GainmapImage``
7
+ """
8
+
9
+ __version__ = "0.1.0"
10
+
11
+ from . import convert, core, identify, io
12
+
13
+ __all__ = [
14
+ "convert",
15
+ "core",
16
+ "identify",
17
+ "io",
18
+ ]
@@ -0,0 +1,24 @@
1
+ """HDR conversion algorithms.
2
+
3
+ This module provides functions for converting between HDR formats and applying
4
+ color space and transfer function transformations:
5
+
6
+ - Gainmap conversion: `gainmap_to_hdr`, `hdr_to_gainmap`
7
+ - Apple HEIC conversion: `apple_heic_to_hdr`
8
+ - Color space conversion: `convert_color_space`
9
+ - Transfer functions: `apply_pq`, `inverse_pq`
10
+ """
11
+
12
+ from hdrconv.convert.apple import apple_heic_to_hdr
13
+ from hdrconv.convert.colorspace import convert_color_space
14
+ from hdrconv.convert.gainmap import gainmap_to_hdr, hdr_to_gainmap
15
+ from hdrconv.convert.transfer import apply_pq, inverse_pq
16
+
17
+ __all__ = [
18
+ "gainmap_to_hdr",
19
+ "hdr_to_gainmap",
20
+ "apple_heic_to_hdr",
21
+ "convert_color_space",
22
+ "apply_pq",
23
+ "inverse_pq",
24
+ ]
@@ -0,0 +1,100 @@
1
+ """Apple HEIC HDR conversion utilities.
2
+
3
+ This module provides functions for converting Apple's proprietary HEIC
4
+ HDR format (with gain map) to standard linear HDR representation.
5
+
6
+ Apple's HDR format uses a single-channel gain map with the formula:
7
+ hdr_rgb = sdr_rgb * (1.0 + (headroom - 1.0) * gainmap)
8
+
9
+ Public APIs:
10
+ - `apple_heic_to_hdr`: Convert AppleHeicData to HDRImage
11
+
12
+ See Also:
13
+ Apple Developer Documentation:
14
+ https://developer.apple.com/documentation/appkit/applying-apple-hdr-effect-to-your-photos
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import numpy as np
20
+ from PIL import Image
21
+
22
+ from hdrconv.core import AppleHeicData, HDRImage
23
+
24
+
25
+ def apple_heic_to_hdr(data: AppleHeicData) -> HDRImage:
26
+ """Convert Apple HEIC gain map data to linear HDR.
27
+
28
+ Applies Apple's proprietary gain map formula to reconstruct the HDR image
29
+ from the SDR base and single-channel gain map.
30
+
31
+ The reconstruction formula is:
32
+ hdr_rgb = sdr_rgb * (1.0 + (headroom - 1.0) * gainmap)
33
+
34
+ Where all values are in linear light space.
35
+
36
+ Args:
37
+ data: AppleHeicData dict containing:
38
+ - `base`: SDR image, uint8, shape (H, W, 3), Display P3.
39
+ - `gainmap`: Gain map, uint8, shape (H, W, 1).
40
+ - `headroom`: Peak luminance multiplier.
41
+
42
+ Returns:
43
+ HDRImage dict with the following keys:
44
+ - ``data`` (np.ndarray): Linear HDR array, float32, shape (H, W, 3).
45
+ - ``color_space`` (str): 'p3' (Display P3, Apple's default).
46
+ - ``transfer_function`` (str): 'linear'.
47
+ - ``icc_profile`` (bytes | None): None.
48
+
49
+ Note:
50
+ The gain map is upscaled from 1/4 resolution using bilinear interpolation.
51
+ Both base image (sRGB transfer) and gain map (Rec. 709 transfer) are
52
+ linearized before applying the formula.
53
+
54
+ See Also:
55
+ - `read_apple_heic`: Read AppleHeicData from HEIC file.
56
+ - `convert_color_space`: Convert output to BT.2020 if needed.
57
+ """
58
+
59
+ def apply_gain_map(
60
+ base_image: np.ndarray, gain_map: np.ndarray, headroom: float
61
+ ) -> np.ndarray:
62
+ if base_image is None or gain_map is None:
63
+ raise ValueError("Both base_image and gain_map must be provided.")
64
+
65
+ gain_map_resized = np.array(
66
+ Image.fromarray(gain_map).resize(
67
+ (base_image.shape[1], base_image.shape[0]), Image.BILINEAR
68
+ )
69
+ )
70
+
71
+ gain_map_norm = gain_map_resized.astype(np.float32) / 255.0
72
+ gain_map_linear = np.where(
73
+ gain_map_norm <= 0.08145,
74
+ gain_map_norm / 4.5,
75
+ np.power((gain_map_norm + 0.099) / 1.099, 1 / 0.45),
76
+ )
77
+ gain_map_linear = np.clip(gain_map_linear, 0.0, 1.0)
78
+
79
+ base_image_norm = base_image.astype(np.float32) / 255.0
80
+ base_image_linear = np.where(
81
+ base_image_norm <= 0.04045,
82
+ base_image_norm / 12.92,
83
+ np.power((base_image_norm + 0.055) / 1.055, 2.4),
84
+ )
85
+ base_image_linear = np.clip(base_image_linear, 0.0, 1.0)
86
+
87
+ hdr_image_linear = base_image_linear * (
88
+ 1.0 + (headroom - 1.0) * gain_map_linear[..., np.newaxis]
89
+ )
90
+ hdr_image_linear = np.clip(hdr_image_linear, 0.0, None)
91
+ return hdr_image_linear
92
+
93
+ hdr_linear = apply_gain_map(data["base"], data["gainmap"], data["headroom"])
94
+
95
+ return HDRImage(
96
+ data=hdr_linear,
97
+ color_space="p3", # Apple uses Display P3
98
+ transfer_function="linear",
99
+ icc_profile=None,
100
+ )
@@ -0,0 +1,74 @@
1
+ """Color space conversion utilities.
2
+
3
+ This module provides functions for converting between different RGB color
4
+ spaces used in HDR imaging workflows.
5
+
6
+ Supported color spaces:
7
+ - 'bt709': ITU-R BT.709 (sRGB primaries)
8
+ - 'p3': DCI-P3 / Display P3
9
+ - 'bt2020': ITU-R BT.2020 (wide gamut HDR)
10
+
11
+ Public APIs:
12
+ - `convert_color_space`: Transform between color spaces
13
+
14
+ Note:
15
+ All conversions require linear light input. Apply EOTF first if
16
+ working with gamma-encoded data.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import warnings
22
+
23
+ import numpy as np
24
+
25
+ with warnings.catch_warnings():
26
+ warnings.filterwarnings("ignore")
27
+ import colour
28
+
29
+
30
+ def convert_color_space(
31
+ image: np.ndarray, source_space: str, target_space: str, clip: bool = False
32
+ ) -> np.ndarray:
33
+ """Convert image between color spaces.
34
+
35
+ Transforms RGB values from one color space to another using chromatic
36
+ adaptation and matrix transformations. Input must be in linear light.
37
+
38
+ Args:
39
+ image: Linear RGB image data, float32, shape (H, W, 3).
40
+ Values should be in linear light (not gamma-encoded).
41
+ source_space: Source color space identifier.
42
+ Options: 'bt709' (Rec. 709), 'p3' (Display P3), 'bt2020' (Rec. 2020).
43
+ target_space: Target color space identifier.
44
+ Options: 'bt709', 'p3', 'bt2020'.
45
+ clip: Whether to clip output to [0, inf). Default: False.
46
+ Enable when negative values from gamut mapping are undesirable.
47
+
48
+ Returns:
49
+ Converted image in target color space, same shape as input.
50
+ Values remain in linear light.
51
+
52
+ Note:
53
+ If source_space equals target_space, returns input unchanged.
54
+ Uses colour-science library for accurate color transformations.
55
+
56
+ See Also:
57
+ - `apply_pq`: Apply PQ transfer function after color space conversion.
58
+ - `gainmap_to_hdr`: Includes color space conversion in HDR reconstruction.
59
+ """
60
+ space_map = {"bt709": "ITU-R BT.709", "p3": "DCI-P3", "bt2020": "ITU-R BT.2020"}
61
+
62
+ if source_space == target_space:
63
+ return image
64
+
65
+ source_name = space_map.get(source_space, source_space)
66
+ target_name = space_map.get(target_space, target_space)
67
+
68
+ target_image = colour.RGB_to_RGB(
69
+ image, input_colourspace=source_name, output_colourspace=target_name
70
+ )
71
+
72
+ if clip:
73
+ target_image = np.clip(target_image, 0.0, None)
74
+ return target_image
@@ -0,0 +1,261 @@
1
+ """ISO 21496-1 Gainmap conversion utilities.
2
+
3
+ This module provides functions for converting between ISO 21496-1 Gainmap
4
+ format and linear HDR representation.
5
+
6
+ The ISO 21496-1 standard defines a method for encoding HDR content using
7
+ a baseline (SDR) image plus a gainmap that encodes the HDR-to-SDR ratio
8
+ in log2 space.
9
+
10
+ Public APIs:
11
+ - `gainmap_to_hdr`: Reconstruct HDR from gainmap
12
+ - `hdr_to_gainmap`: Create gainmap from HDR
13
+
14
+ See Also:
15
+ ISO/TS 21496-1: Adaptive gain map for HDR still image
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Optional
21
+ import warnings
22
+
23
+ from PIL import Image
24
+ import numpy as np
25
+
26
+ with warnings.catch_warnings():
27
+ warnings.filterwarnings("ignore")
28
+ import colour
29
+
30
+ from hdrconv.core import GainmapImage, GainmapMetadata, HDRImage
31
+ from hdrconv.convert.colorspace import convert_color_space
32
+
33
+
34
+ def gainmap_to_hdr(
35
+ data: GainmapImage,
36
+ baseline_color_space: str = "p3",
37
+ alt_color_space: str = "p3",
38
+ target_color_space: str = "bt2020",
39
+ ) -> HDRImage:
40
+ """Convert ISO 21496-1 Gainmap to linear HDR image.
41
+
42
+ Applies the gainmap to the baseline image to reconstruct the alternate
43
+ (HDR) representation using the ISO 21496-1 formula:
44
+
45
+ - G' = (G^(1/gamma)) * (max - min) + min
46
+ - L = 2^G'
47
+ - HDR = L * (baseline + baseline_offset) - alternate_offset
48
+
49
+ Args:
50
+ data: GainmapImage dict containing baseline, gainmap, and metadata.
51
+ baseline_color_space: Color space of baseline image.
52
+ Options: 'bt709', 'p3', 'bt2020'. Default: 'p3'.
53
+ alt_color_space: Color space of alternate/HDR image.
54
+ Options: 'bt709', 'p3', 'bt2020'. Default: 'p3'.
55
+ target_color_space: Target output color space.
56
+ Options: 'bt709', 'p3', 'bt2020'. Default: 'bt2020'.
57
+
58
+ Returns:
59
+ HDRImage dict with the following keys:
60
+ - ``data`` (np.ndarray): Linear HDR array, float32, shape (H, W, 3).
61
+ - ``color_space`` (str): Target color space string.
62
+ - ``transfer_function`` (str): Always 'linear'.
63
+ - ``icc_profile`` (bytes | None): Always None.
64
+
65
+ Note:
66
+ The baseline image is assumed to be sRGB-encoded. The function
67
+ automatically applies EOTF conversion to linear light before
68
+ applying the gainmap formula.
69
+
70
+ See Also:
71
+ - `hdr_to_gainmap`: Inverse operation, create gainmap from HDR.
72
+ - `convert_color_space`: For additional color space transformations.
73
+ """
74
+ baseline = data["baseline"].astype(np.float32) / 255.0 # Normalize to [0, 1]
75
+ baseline = colour.eotf(baseline, function="sRGB")
76
+ gainmap = data["gainmap"].astype(np.float32) / 255.0
77
+ metadata = data["metadata"]
78
+
79
+ use_base_colour_space = metadata["use_base_colour_space"]
80
+ if not use_base_colour_space:
81
+ baseline = convert_color_space(
82
+ baseline,
83
+ source_space=baseline_color_space,
84
+ target_space=alt_color_space,
85
+ )
86
+ else:
87
+ gainmap = convert_color_space(
88
+ gainmap,
89
+ source_space=alt_color_space,
90
+ target_space=baseline_color_space,
91
+ )
92
+
93
+ # Resize gainmap to match baseline if needed
94
+ h, w = baseline.shape[:2]
95
+ if gainmap.shape[:2] != (h, w):
96
+ # Use Pillow for resizing: convert float32 [0,1] -> uint8 [0,255] -> resize -> back to float32
97
+ gainmap_uint8 = np.clip(gainmap * 255.0, 0, 255).astype(np.uint8)
98
+
99
+ # Handle 2D grayscale and 3D RGB arrays
100
+ if gainmap_uint8.ndim == 2:
101
+ pil_image = Image.fromarray(gainmap_uint8, mode="L")
102
+ else:
103
+ pil_image = Image.fromarray(gainmap_uint8, mode="RGB")
104
+
105
+ # Resize using bilinear interpolation (equivalent to cv2.INTER_LINEAR)
106
+ pil_image_resized = pil_image.resize((w, h), Image.BILINEAR)
107
+
108
+ # Convert back to float32 [0,1]
109
+ gainmap = np.array(pil_image_resized, dtype=np.float32) / 255.0
110
+
111
+ # Ensure gainmap is 3-channel for calculations
112
+ if gainmap.ndim == 2:
113
+ gainmap = gainmap[:, :, np.newaxis]
114
+ if gainmap.shape[2] == 1:
115
+ gainmap = np.repeat(gainmap, 3, axis=2)
116
+
117
+ # Extract metadata (convert to arrays for broadcasting)
118
+ gainmap_min = np.array(metadata["gainmap_min"], dtype=np.float32)
119
+ gainmap_max = np.array(metadata["gainmap_max"], dtype=np.float32)
120
+ gainmap_gamma = np.array(metadata["gainmap_gamma"], dtype=np.float32)
121
+ baseline_offset = np.array(metadata["baseline_offset"], dtype=np.float32)
122
+ alternate_offset = np.array(metadata["alternate_offset"], dtype=np.float32)
123
+
124
+ gainmap = np.clip(gainmap, 0.0, 1.0)
125
+
126
+ # Decode gainmap: apply gamma, scale, and offset
127
+ gainmap_decoded = (gainmap ** (1 / gainmap_gamma)) * (
128
+ gainmap_max - gainmap_min
129
+ ) + gainmap_min
130
+
131
+ # Convert to linear multiplier
132
+ gainmap_linear = np.exp2(gainmap_decoded)
133
+
134
+ # Reconstruct alternate (HDR) image
135
+ hdr_linear = gainmap_linear * (baseline + baseline_offset) - alternate_offset
136
+
137
+ # Color space conversion
138
+ if not use_base_colour_space:
139
+ hdr_linear = convert_color_space(
140
+ hdr_linear, source_space=alt_color_space, target_space=target_color_space
141
+ )
142
+ else:
143
+ hdr_linear = convert_color_space(
144
+ hdr_linear,
145
+ source_space=baseline_color_space,
146
+ target_space=target_color_space,
147
+ )
148
+
149
+ hdr_linear = np.clip(hdr_linear, 0.0, None)
150
+
151
+ return HDRImage(
152
+ data=hdr_linear,
153
+ color_space=target_color_space,
154
+ transfer_function="linear",
155
+ icc_profile=None,
156
+ )
157
+
158
+
159
+ def hdr_to_gainmap(
160
+ hdr: HDRImage,
161
+ baseline: Optional[np.ndarray] = None,
162
+ color_space: str = "bt709",
163
+ icc_profile: Optional[bytes] = None,
164
+ gamma: float = 1.0,
165
+ ) -> GainmapImage:
166
+ """Convert linear HDR image to ISO 21496-1 Gainmap format.
167
+
168
+ Creates a gainmap by computing the log2 ratio between HDR and SDR images.
169
+ If baseline is not provided, generates one by clipping HDR to [0, 1].
170
+
171
+ Args:
172
+ hdr: HDRImage dict with linear HDR data in any supported color space.
173
+ baseline: Optional pre-computed baseline (SDR) image.
174
+ If None, generated by clipping HDR to [0, 1].
175
+ Expected format: float32, shape (H, W, 3), range [0, 1].
176
+ color_space: Target color space for output.
177
+ Options: 'bt709', 'p3', 'bt2020'. Default: 'bt709'.
178
+ icc_profile: Optional ICC profile bytes to embed in output.
179
+ Should match the specified color_space.
180
+ gamma: Gainmap gamma parameter for encoding.
181
+ Higher values compress highlights. Default: 1.0.
182
+
183
+ Returns:
184
+ GainmapImage dict containing:
185
+ - ``baseline`` (np.ndarray): SDR image, uint8, shape (H, W, 3).
186
+ - ``gainmap`` (np.ndarray): Gain map, uint8, shape (H, W, 3).
187
+ - ``metadata`` (GainmapMetadata): Computed transformation parameters.
188
+ - ``baseline_icc`` (bytes | None): Provided ICC profile.
189
+ - ``gainmap_icc`` (bytes | None): Provided ICC profile.
190
+
191
+ Note:
192
+ Uses fixed offsets of 1/64 for both baseline and alternate to
193
+ avoid division by zero in dark regions.
194
+
195
+ See Also:
196
+ - `gainmap_to_hdr`: Inverse operation, reconstruct HDR from gainmap.
197
+ - `write_21496`: Write GainmapImage to ISO 21496-1 JPEG.
198
+ """
199
+ hdr_data = hdr["data"].astype(np.float32)
200
+
201
+ # convert to target colour space
202
+ hdr_data = convert_color_space(
203
+ hdr_data, source_space=hdr["color_space"], target_space=color_space
204
+ )
205
+
206
+ hdr_data = np.clip(hdr_data, 0.0, None)
207
+
208
+ # Generate baseline if not provided
209
+ if baseline is None:
210
+ baseline = hdr_data.copy()
211
+ baseline = np.clip(baseline, 0.0, 1.0)
212
+
213
+ # Compute alt headroom
214
+ alt_headroom = np.log2(hdr_data.max() + 1e-6)
215
+
216
+ # preset offset for both baseline and alternate = 1/64
217
+ alt_offset = float(1 / 64)
218
+ base_offset = float(1 / 64)
219
+
220
+ ratio = (hdr_data + alt_offset) / (baseline + base_offset)
221
+ ratio = np.clip(ratio, 1e-6, None)
222
+
223
+ gainmap_log = np.log2(ratio)
224
+
225
+ gainmap_min_val = np.min(gainmap_log, axis=(0, 1))
226
+ gainmap_max_val = np.max(gainmap_log, axis=(0, 1))
227
+
228
+ gainmap_norm = (gainmap_log - gainmap_min_val) / (gainmap_max_val - gainmap_min_val)
229
+ gainmap_norm = np.clip(gainmap_norm, 0, 1)
230
+
231
+ gainmap_norm = gainmap_norm**gamma
232
+
233
+ gainmap_uint8 = (gainmap_norm * 255).astype(np.uint8)
234
+
235
+ baseline = colour.eotf_inverse(baseline, function="sRGB")
236
+ baseline_uint8 = (baseline * 255).astype(np.uint8)
237
+
238
+ gainmap_min_val = tuple(gainmap_min_val.tolist())
239
+ gainmap_max_val = tuple(gainmap_max_val.tolist())
240
+
241
+ metadata = GainmapMetadata(
242
+ minimum_version=0,
243
+ writer_version=0,
244
+ baseline_hdr_headroom=1.0,
245
+ alternate_hdr_headroom=float(alt_headroom),
246
+ is_multichannel=True,
247
+ use_base_colour_space=True,
248
+ gainmap_min=gainmap_min_val,
249
+ gainmap_max=gainmap_max_val,
250
+ gainmap_gamma=(gamma, gamma, gamma),
251
+ baseline_offset=(base_offset, base_offset, base_offset),
252
+ alternate_offset=(alt_offset, alt_offset, alt_offset),
253
+ )
254
+
255
+ return GainmapImage(
256
+ baseline=baseline_uint8,
257
+ gainmap=gainmap_uint8,
258
+ metadata=metadata,
259
+ baseline_icc=icc_profile,
260
+ gainmap_icc=icc_profile,
261
+ )
@@ -0,0 +1,84 @@
1
+ """Transfer function utilities for HDR encoding.
2
+
3
+ This module provides functions for applying and inverting transfer functions
4
+ used in HDR content delivery, particularly PQ (Perceptual Quantizer).
5
+
6
+ Supported transfer functions:
7
+ - PQ (SMPTE ST 2084): Used in HDR10, Dolby Vision
8
+ - HLG (Hybrid Log-Gamma): Used in broadcast HDR
9
+
10
+ Public APIs:
11
+ - `apply_pq`: Encode linear light to PQ
12
+ - `inverse_pq`: Decode PQ to linear light
13
+
14
+ Note:
15
+ PQ uses 203 nits as reference white (1.0 in linear scale).
16
+ Values above 1.0 represent HDR highlights up to 10000 nits.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import warnings
22
+
23
+ import numpy as np
24
+
25
+ with warnings.catch_warnings():
26
+ warnings.filterwarnings("ignore")
27
+ import colour
28
+
29
+
30
+ def apply_pq(linear_rgb: np.ndarray) -> np.ndarray:
31
+ """Apply PQ (Perceptual Quantizer) transfer function.
32
+
33
+ Encodes linear light RGB values to PQ (SMPTE ST 2084) transfer function
34
+ as specified in ITU-R BT.2100 for HDR content.
35
+
36
+ Args:
37
+ linear_rgb: Linear RGB data, float32, shape (H, W, 3).
38
+ Values should be normalized where 1.0 = 203 nits (reference white).
39
+ Values above 1.0 represent HDR highlights up to ~49x (10000 nits).
40
+
41
+ Returns:
42
+ PQ-encoded data, float32, shape (H, W, 3), range [0, 1].
43
+ Output is clipped to valid PQ range.
44
+
45
+ Note:
46
+ Uses 203 nits as reference white (PQ specification).
47
+ Linear value of 1.0 maps to ~58% in PQ code values.
48
+
49
+ See Also:
50
+ - `inverse_pq`: Decode PQ back to linear light.
51
+ - `write_22028_pq`: Write PQ-encoded data to AVIF file.
52
+ """
53
+ # Normalize to reference white (203 nits in PQ)
54
+ pq_encoded = colour.models.eotf_inverse_BT2100_PQ(linear_rgb * 203.0)
55
+ pq_encoded = np.clip(pq_encoded, 0.0, 1.0)
56
+ return pq_encoded
57
+
58
+
59
+ def inverse_pq(pq_encoded: np.ndarray) -> np.ndarray:
60
+ """Decode PQ-encoded values to linear light RGB.
61
+
62
+ Applies the inverse PQ (SMPTE ST 2084) EOTF to convert PQ-encoded
63
+ values back to linear light as specified in ITU-R BT.2100.
64
+
65
+ Args:
66
+ pq_encoded: PQ-encoded data, float32, shape (H, W, 3), range [0, 1].
67
+ Values represent 0-10000 nits in PQ perceptual scale.
68
+
69
+ Returns:
70
+ Linear RGB data, float32, shape (H, W, 3).
71
+ Normalized where 1.0 = 203 nits (reference white).
72
+ HDR highlights may exceed 1.0 up to ~49x.
73
+
74
+ Note:
75
+ Uses 203 nits as reference white (PQ specification).
76
+ PQ code value of ~0.58 maps to linear 1.0.
77
+
78
+ See Also:
79
+ - `apply_pq`: Encode linear light to PQ.
80
+ - `read_22028_pq`: Read PQ-encoded AVIF file.
81
+ """
82
+ linear_normalized = colour.models.eotf_BT2100_PQ(pq_encoded)
83
+ linear_rgb = linear_normalized / 203.0
84
+ return linear_rgb