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.
- hdr_conversion-0.1.0.dist-info/METADATA +78 -0
- hdr_conversion-0.1.0.dist-info/RECORD +18 -0
- hdr_conversion-0.1.0.dist-info/WHEEL +4 -0
- hdr_conversion-0.1.0.dist-info/licenses/LICENSE +8 -0
- hdrconv/__init__.py +18 -0
- hdrconv/convert/__init__.py +24 -0
- hdrconv/convert/apple.py +100 -0
- hdrconv/convert/colorspace.py +74 -0
- hdrconv/convert/gainmap.py +261 -0
- hdrconv/convert/transfer.py +84 -0
- hdrconv/core/__init__.py +20 -0
- hdrconv/core/types.py +137 -0
- hdrconv/identify/__init__.py +12 -0
- hdrconv/identify/apple_heic.py +48 -0
- hdrconv/io/__init__.py +23 -0
- hdrconv/io/apple_heic.py +189 -0
- hdrconv/io/iso21496.py +797 -0
- hdrconv/io/iso22028.py +111 -0
|
@@ -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,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
|
+
]
|
hdrconv/convert/apple.py
ADDED
|
@@ -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
|