Convert AVIF images from Identity to YUV BT2020 Matrix Coefficients #2717
-
|
I have a bunch of images using CICP values of MC=0/Identity (so RGB stored in YUV), but Windows doesn't seem to be handling that well at all (it not supposed to be just pure red): So I've tried to get it converted to YUV BT2020 (so HDR10, TC=9/TC=16/MC=9) and I came as for as to being able to dump it as y4m and the reverse, but I've not been able to get the raw pixels converted to YUV BT2020 from the RGB. I have tried ffmpeg, but it can basically only convert everything, but identity. I've also tried to get an LLM to help me write code for converting those pixels from RGB to YUV BT2020, but the result is not working, as expected... Is there anyway to easily do this conversion? I don't seem to be able to do it myself manually with my very limited understanding of color science... For reference this is the last code the LLM cam up with: convert.pydef process_yuv_planes(y_plane, u_plane, v_plane, bit_depth, chroma_subsampling):
"""
Performs BGR (in YUV) to BT.2020 NCL Y'CbCr color conversion with proper level offsets and scaling.
Input YUV planes are interpreted as B, G, R channels respectively (BGR in YUV), assumed to be in the full range [0, (1<<bit_depth)-1].
Output is BT.2020 NCL Y'CbCr in the limited range [0, (1<<bit_depth)-1] as typically used in video.
This version incorporates level offsets and proper scaling for standard YUV ranges.
Gamma handling is still deferred but is a VERY likely next step if this doesn't resolve the issue.
"""
print(f"Processing YUV planes: BGR (in YUV) to BT.2020 NCL Y'CbCr. Bit depth: {bit_depth}, Chroma subsampling: {chroma_subsampling}")
# 1. Conversion matrix from BT.2020 RGB to BT.2020 Y'CbCr (Rec.2100-1 coefficients - verified and standard)
# These coefficients are for *normalized* RGB in the range [0, 1] to normalized Y'CbCr in the range [0, 1] (approximately, luma might slightly exceed [0,1])
conversion_matrix_rgb_to_yuv = np.array([
[0.6274, 0.3293, 0.0433],
[-0.3726, -0.6207, 1.0000], # Cb Row (U) - Note different coefficients, these are correct for BT.2020
[1.0000, -0.9414, -0.0586] # Cr Row (V) - Note different coefficients, these are correct for BT.2020
])
# Debug: Input plane shapes
print("Debug process_yuv_planes - Input Y plane shape:", y_plane.shape)
print("Debug process_yuv_planes - Input U plane shape:", u_plane.shape)
print("Debug process_yuv_planes - Input V plane shape:", v_plane.shape)
# 2. INPUT IS BGR in YUV planes: Y=B, U=G, V=R
# 3. REARRANGE INPUT PLANES to BGR order and NORMALIZE to 0-1 range
bgr_planes = np.stack([y_plane, u_plane, v_plane], axis=-1).astype(np.float64) # Stack B, G, R, and convert to float64
print("Debug process_yuv_planes - BGR planes shape (after stack):", bgr_planes.shape) # DEBUG
dtype_max_value = (1 << bit_depth) - 1
bgr_normalized = bgr_planes / dtype_max_value # Normalize BGR to [0, 1] range
print("Debug process_yuv_planes - BGR normalized shape:", bgr_normalized.shape) # DEBUG
# Reshape to (N, 3) for matrix multiplication, where N = H * W (number of pixels)
original_shape = bgr_normalized.shape[:-1] # Store original shape (H, W)
print("Debug process_yuv_planes - Original shape:", original_shape) # DEBUG
bgr_pixels = bgr_normalized.reshape(-1, 3) # Reshape to (H*W, 3)
print("Debug process_yuv_planes - BGR pixels shape:", bgr_pixels.shape) # DEBUG
# 4. APPLY BT.2020 RGB to Y'CbCr CONVERSION MATRIX.
yuv_pixels = np.dot(bgr_pixels, conversion_matrix_rgb_to_yuv.T) # Transpose matrix for correct multiplication
print("Debug process_yuv_planes - YUV pixels shape (after matrix multiply):", yuv_pixels.shape) # DEBUG
# 5. Y'CbCr LEVEL SCALING and OFFSET (Crucial step for standard YUV ranges!)
# BT.2020 NCL Y'CbCr is typically stored in a "limited range" (also called "video range" or "TV range").
# Luma (Y') range is typically [0, 1] which maps to [16/255, 235/255] in 8-bit, and proportionally for higher bit depths.
# Chroma (Cb, Cr) range is typically centered around 0.5, mapping to [16/255, 240/255] in 8-bit, centered at 128.
# However, for simplicity and common practice in digital video (especially with higher bit depths),
# let's try a simpler scaling to the full bit-depth range [0, dtype_max_value], but with an offset for Cb, Cr.
luma_offset = 0 # No offset for Luma for now, range [0, 1] -> [0, dtype_max_value]
chroma_offset = 0.5 # Chroma is often centered around 0.5 in normalized range. Let's offset by 0.5 before scaling.
corrected_y_plane = yuv_pixels[:, 0] # Luma is first column - CORRECTED INDEXING - removed second ":"
corrected_u_plane = yuv_pixels[:, 1] + chroma_offset # Cb with offset - CORRECTED INDEXING
corrected_v_plane = yuv_pixels[:, 2] + chroma_offset # Cr with offset - CORRECTED INDEXING
print("Debug process_yuv_planes - Corrected Y plane shape (before reshape):", corrected_y_plane.shape) # DEBUG
# 6. *** CLAMP and SCALE back to output bit depth (important for valid YUV) ***
corrected_y_plane = np.clip(corrected_y_plane * dtype_max_value, 0, dtype_max_value).astype(np.uint16 if bit_depth > 8 else np.uint8) # Clip and cast
corrected_u_plane = np.clip(corrected_u_plane * dtype_max_value, 0, dtype_max_value).astype(np.uint16 if bit_depth > 8 else np.uint8)
corrected_v_plane = np.clip(corrected_v_plane * dtype_max_value, 0, dtype_max_value).astype(np.uint16 if bit_depth > 8 else np.uint8)
print("Debug process_yuv_planes - Corrected Y plane shape (after clip/cast):", corrected_y_plane.shape) # DEBUG
return corrected_y_plane, corrected_u_plane, corrected_v_planeThat code already got the gamma working again I think, but resulted in an image with Green / Purple hue instead of red... (which is slightly better than just replacing the metadata which just results in Green / Purple and destroyed gamma) |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 7 replies
-
|
It shoudl be possible to write a C or C++ program to perform this conversion by calling the
Then The comment in your convert.py script says "Input YUV planes are interpreted as B, G, R channels respectively (BGR in YUV)". But for MC=0/Identity, input YUV planes should be interpreted as G, B, R channels respectively. Are you sure the B, G, R channels order is correct? |
Beta Was this translation helpful? Give feedback.


It shoudl be possible to write a C or C++ program to perform this conversion by calling the
avifImageYUVToRGB()andavifImageRGBToYUV()functions.avifImageYUVToRGB()can be used to convert the "RGB stored in YUV" to RGB.Then
avifImageRGBToYUV()can be used to convert RGB to YUV with BT.2020 matrix coefficients.The comment in your convert.py script says "Input YUV planes are interpreted as B, G, R channels respectively (BGR in YUV)". But for MC=0/Identity, input YUV planes should be interpreted as G, B, R channels respectively. Are you sure the B, G, R channels order is correct?