#!/usr/bin/env python3 """ XYZ to RGB Color Space Converter Features: - Convert XYZ color space data to RGB color space - Support multiple RGB working spaces (sRGB, Adobe RGB, DCI-P3, etc.) - Configurable compression/gamma correction methods - Support various output data types (uint8, uint16, float32, etc.) - Input: 3-band .dat file containing XYZ data - Output: 3-band .bip file (Band Interleaved by Pixel format) Dependencies: - numpy, colour-science: color space conversion - spectral: ENVI file processing - tqdm: progress bar (optional) Installation: pip install numpy colour-science spectral tqdm """ import numpy as np import os import sys import warnings import argparse from pathlib import Path from typing import Optional, Tuple, Union, Dict, Any from enum import Enum # Try importing optional dependencies try: import colour from colour import XYZ_to_RGB, RGB_COLOURSPACES COLOUR_AVAILABLE = True except ImportError: COLOUR_AVAILABLE = False print("Warning: colour-science library not available, please install: pip install colour-science") try: import spectral SPECTRAL_AVAILABLE = True except ImportError: SPECTRAL_AVAILABLE = False print("Warning: spectral library not available, will not be able to process ENVI format files") try: from tqdm import tqdm TQDM_AVAILABLE = True except ImportError: TQDM_AVAILABLE = False print("Note: tqdm library not available, progress bar will be disabled. Install: pip install tqdm") try: from osgeo import gdal GDAL_AVAILABLE = True except ImportError: GDAL_AVAILABLE = False print("Note: GDAL library not available, will use spectral as fallback. Install: pip install GDAL") class RGBColorSpace(Enum): """RGB color space enumeration""" SRGB = "sRGB" ADOBE_RGB = "Adobe RGB (1998)" DCI_P3 = "DCI-P3" BT_709 = "ITU-R BT.709" BT_2020 = "ITU-R BT.2020" ACES2065_1 = "ACES2065-1" ACESCG = "ACEScg" PROPHOTO_RGB = "ProPhoto RGB" APPLE_RGB = "Apple RGB" PAL_SECAM = "PAL/SECAM" NTSC = "NTSC (1953)" class GammaMethod(Enum): """Gamma correction method enumeration""" NONE = "none" SRGB = "sRGB" BT_709 = "BT.709" GAMMA_2_2 = "gamma_2.2" GAMMA_1_8 = "gamma_1.8" GAMMA_2_4 = "gamma_2.4" L_STAR = "L*" BT_1886 = "BT.1886" ST_2084 = "ST 2084" HLG = "HLG" LOG = "log" class OutputDataType(Enum): """Output data type enumeration""" UINT8 = "uint8" UINT10 = "uint10" UINT12 = "uint12" UINT16 = "uint16" INT8 = "int8" INT16 = "int16" FLOAT16 = "float16" FLOAT32 = "float32" FLOAT64 = "float64" class XYZ2RGBConverter: """ XYZ to RGB Color Space Converter Converts XYZ color space data to specified RGB color space """ def __init__(self, rgb_space: RGBColorSpace = RGBColorSpace.SRGB, gamma_method: GammaMethod = GammaMethod.SRGB, output_dtype: OutputDataType = OutputDataType.UINT8): """ Initialize XYZ to RGB converter Parameters: rgb_space: Target RGB color space gamma_method: Gamma correction method output_dtype: Output data type """ if not COLOUR_AVAILABLE: raise ImportError("colour-science library is required: pip install colour-science") self.rgb_space = rgb_space self.gamma_method = gamma_method self.output_dtype = output_dtype # Get colour space definition self.colour_space = RGB_COLOURSPACES.get(rgb_space.value) if self.colour_space is None: available_spaces = list(RGB_COLOURSPACES.keys()) raise ValueError(f"Unsupported RGB color space: {rgb_space.value}. " f"Available: {available_spaces}") print(f"Initialized XYZ to RGB converter:") print(f" Target RGB space: {rgb_space.value}") print(f" Gamma method: {gamma_method.value}") print(f" Output data type: {output_dtype.value}") def convert_xyz_to_rgb(self, xyz_data: np.ndarray) -> np.ndarray: """ Convert XYZ color data to RGB Parameters: xyz_data: XYZ color data array (..., 3) Returns: rgb_data: RGB color data array (..., 3) """ print("Starting XYZ to RGB conversion...") # Validate input shape if xyz_data.shape[-1] != 3: raise ValueError(f"Input must have 3 bands (X, Y, Z), got {xyz_data.shape[-1]}") # Reshape to 2D for processing original_shape = xyz_data.shape xyz_flat = xyz_data.reshape(-1, 3) print(f"Input shape: {original_shape}") print(f"Processing {xyz_flat.shape[0]} pixels...") # Apply gamma correction to XYZ data if needed xyz_processed = self._apply_gamma_to_xyz(xyz_flat) # Convert XYZ to RGB using colour library rgb_linear = XYZ_to_RGB(xyz_processed, self.colour_space.whitepoint, self.colour_space.whitepoint, self.colour_space.matrix_XYZ_to_RGB) # Apply gamma correction to RGB data rgb_corrected = self._apply_gamma_to_rgb(rgb_linear) # Clip to valid range rgb_corrected = np.clip(rgb_corrected, 0.0, 1.0) # Convert to output data type rgb_output = self._convert_to_output_dtype(rgb_corrected) # Reshape back to original shape rgb_result = rgb_output.reshape(original_shape) print(f"Conversion completed. Output shape: {rgb_result.shape}") print(f"Output data type: {rgb_result.dtype}") print(f"Value range: [{rgb_result.min()}, {rgb_result.max()}]") return rgb_result def _apply_gamma_to_xyz(self, xyz_data: np.ndarray) -> np.ndarray: """ Apply gamma correction to XYZ data if needed Parameters: xyz_data: XYZ data array Returns: Processed XYZ data """ # For XYZ to RGB conversion, XYZ data should typically be linear # We assume input XYZ data is already in linear space return xyz_data def _apply_gamma_to_rgb(self, rgb_linear: np.ndarray) -> np.ndarray: """ Apply gamma correction to RGB data Parameters: rgb_linear: Linear RGB data Returns: Gamma-corrected RGB data """ if self.gamma_method == GammaMethod.NONE: return rgb_linear elif self.gamma_method == GammaMethod.SRGB: # sRGB gamma correction mask = rgb_linear <= 0.0031308 rgb_corrected = np.where(mask, rgb_linear * 12.92, 1.055 * np.power(rgb_linear, 1/2.4) - 0.055) return rgb_corrected elif self.gamma_method == GammaMethod.BT_709: # BT.709 gamma correction (same as sRGB for most purposes) mask = rgb_linear <= 0.018 rgb_corrected = np.where(mask, rgb_linear * 4.5, 1.099 * np.power(rgb_linear, 0.45) - 0.099) return rgb_corrected elif self.gamma_method == GammaMethod.GAMMA_2_2: return np.power(rgb_linear, 1/2.2) elif self.gamma_method == GammaMethod.GAMMA_1_8: return np.power(rgb_linear, 1/1.8) elif self.gamma_method == GammaMethod.GAMMA_2_4: return np.power(rgb_linear, 1/2.4) elif self.gamma_method == GammaMethod.L_STAR: # L* (CIE 1976) lightness correction # L* = 116 * (Y/Yn)^(1/3) - 16 for Y/Yn > 0.008856 # L* = 903.3 * (Y/Yn) for Y/Yn <= 0.008856 # For simplicity, approximate with power function return np.power(rgb_linear, 1/3.0) elif self.gamma_method == GammaMethod.BT_1886: # BT.1886 gamma (for reference displays) # V = a * L^γ where γ ≈ 2.4, a is chosen so V=1 when L=1 return np.power(rgb_linear, 1/2.4) elif self.gamma_method == GammaMethod.ST_2084: # SMPTE ST 2084 (PQ curve) for HDR # This is a complex EOTF, simplified approximation m1 = 0.1593017578125 m2 = 78.84375 c1 = 0.8359375 c2 = 18.8515625 c3 = 18.6875 rgb_corrected = np.power((c1 + c2 * np.power(rgb_linear, m1)) / (1 + c3 * np.power(rgb_linear, m1)), m2) return rgb_corrected elif self.gamma_method == GammaMethod.HLG: # Hybrid Log-Gamma (HLG) for HDR # Simplified approximation a = 0.17883277 b = 0.28466892 c = 0.55991073 mask = rgb_linear <= 0.5 rgb_corrected = np.where(mask, np.sqrt(3 * rgb_linear), a * np.log(12 * rgb_linear - b) + c) return rgb_corrected elif self.gamma_method == GammaMethod.LOG: # Logarithmic encoding return np.log1p(rgb_linear) else: warnings.warn(f"Unknown gamma method: {self.gamma_method}, using linear") return rgb_linear def _convert_to_output_dtype(self, rgb_data: np.ndarray) -> np.ndarray: """ Convert RGB data to output data type Parameters: rgb_data: RGB data in [0, 1] range Returns: RGB data in specified output format """ if self.output_dtype == OutputDataType.UINT8: return (rgb_data * 255).astype(np.uint8) elif self.output_dtype == OutputDataType.UINT10: return (rgb_data * 1023).astype(np.uint16) # Use uint16 to store uint10 elif self.output_dtype == OutputDataType.UINT12: return (rgb_data * 4095).astype(np.uint16) # Use uint16 to store uint12 elif self.output_dtype == OutputDataType.UINT16: return (rgb_data * 65535).astype(np.uint16) elif self.output_dtype == OutputDataType.INT8: # Convert [0,1] to [-128,127] range return ((rgb_data * 255) - 128).astype(np.int8) elif self.output_dtype == OutputDataType.INT16: # Convert [0,1] to [-32768,32767] range return ((rgb_data * 65535) - 32768).astype(np.int16) elif self.output_dtype == OutputDataType.FLOAT16: return rgb_data.astype(np.float16) elif self.output_dtype == OutputDataType.FLOAT32: return rgb_data.astype(np.float32) elif self.output_dtype == OutputDataType.FLOAT64: return rgb_data.astype(np.float64) else: return rgb_data class EnviFileHandler: """ENVI file handler for XYZ/RGB data""" def __init__(self): if not SPECTRAL_AVAILABLE: raise ImportError("spectral library is required for ENVI file processing") def load_xyz_image(self, image_path: Union[str, Path]) -> Tuple[np.ndarray, Dict[str, Any]]: """ Load XYZ image from ENVI format file Parameters: image_path: Path to ENVI image file (.dat, .bip, .bsq, etc.) Returns: xyz_data: XYZ data array (height, width, 3) metadata: ENVI file metadata dictionary """ image_path = Path(image_path) print(f"Loading XYZ image: {image_path}") try: # Open image using spectral library img = spectral.open_image(str(image_path)) print(f"Image shape: {img.shape}") print(f"Data type: {img.dtype}") print(f"Number of bands: {img.shape[2] if len(img.shape) > 2 else 1}") except Exception as e: error_msg = f"Failed to open ENVI image {image_path}: {str(e)}" # Check if file exists if not image_path.exists(): error_msg += f"\nFile does not exist: {image_path}" # Suggest possible ENVI files in directory parent_dir = image_path.parent if parent_dir.exists(): envi_files = [] for ext in ['*.hdr', '*.dat', '*.bip', '*.bsq', '*.bil']: envi_files.extend(list(parent_dir.glob(ext))) if envi_files: error_msg += f"\n\nENVI files in directory:" for f in envi_files[:10]: # Show max 10 files error_msg += f"\n {f.name}" else: error_msg += f"\n\nNo ENVI files found in directory" error_msg += f"\n\nSuggestions:" error_msg += f"\n1. Ensure file path is correct" error_msg += f"\n2. Try using data file (.bip, .dat) instead of header file (.hdr)" error_msg += f"\n3. Or use basename without extension to let spectral find files automatically" raise IOError(error_msg) # Check number of bands if img.shape[2] != 3: raise ValueError(f"XYZ image must have 3 bands (X, Y, Z), got {img.shape[2]}") # Load data xyz_data = img.load().astype(np.float32) print(f"Loaded XYZ data shape: {xyz_data.shape}") print(f"Data type: {xyz_data.dtype}") # Validate XYZ value ranges self._print_xyz_range(xyz_data, "Input XYZ") # Extract metadata metadata = {} if hasattr(img, 'metadata') and img.metadata: metadata = dict(img.metadata) return xyz_data, metadata def save_rgb_image(self, rgb_data: np.ndarray, output_path: Union[str, Path], rgb_space: str, input_hdr_path: Optional[Union[str, Path]] = None) -> None: """ Save RGB data as BIP format ENVI file Parameters: rgb_data: RGB data array (height, width, 3) output_path: Output file path (.bip) rgb_space: RGB color space name input_hdr_path: Input HDR file path to copy metadata from """ output_path = Path(output_path) output_path.parent.mkdir(parents=True, exist_ok=True) print(f"Saving RGB data to BIP file: {output_path}") print(f"RGB color space: {rgb_space}") print(f"Data shape: {rgb_data.shape}") print(f"Data type: {rgb_data.dtype}") # Band names for RGB band_names = ['R', 'G', 'B'] if GDAL_AVAILABLE and rgb_data.shape[2] == 3: # Use GDAL to save BIP file driver = gdal.GetDriverByName('ENVI') height, width, channels = rgb_data.shape # Create output dataset out_dataset = driver.Create( str(output_path), width, height, channels, self._gdal_dtype_from_numpy(rgb_data.dtype), options=['INTERLEAVE=BIP'] ) if out_dataset is None: raise ValueError(f"Failed to create output file: {output_path}") # Write each band for band_idx in range(channels): band_data = rgb_data[:, :, band_idx] out_band = out_dataset.GetRasterBand(band_idx + 1) out_band.WriteArray(band_data.astype(rgb_data.dtype)) out_band.SetDescription(band_names[band_idx]) # Set metadata out_dataset.SetMetadataItem('RGB_color_space', rgb_space) out_dataset.SetMetadataItem('data_type', str(rgb_data.dtype)) out_dataset.SetMetadataItem('interleave', 'bip') out_dataset.FlushCache() out_dataset = None print(f"Saved BIP file using GDAL: {output_path}") elif SPECTRAL_AVAILABLE: # Fallback to spectral library # spectral expects (channels, height, width) format rgb_transposed = np.transpose(rgb_data, (2, 0, 1)) # Create HDR file path hdr_path = output_path.with_suffix('.hdr') # Save using spectral spectral.envi.save_image(str(hdr_path), rgb_transposed, dtype=rgb_data.dtype, interleave='bip') print(f"Saved BIP file using spectral: HDR={hdr_path}, DAT={output_path}") else: raise ImportError("Need GDAL or spectral library to save ENVI format") # Create or update HDR file with RGB-specific metadata self._create_rgb_hdr_file(output_path, rgb_data.shape, rgb_data.dtype, rgb_space, input_hdr_path) print(f"RGB data saved successfully: {output_path}") def _gdal_dtype_from_numpy(self, dtype: np.dtype) -> int: """Convert numpy dtype to GDAL data type""" dtype_map = { np.uint8: gdal.GDT_Byte, np.uint16: gdal.GDT_UInt16, np.int8: gdal.GDT_Int8, np.int16: gdal.GDT_Int16, np.uint32: gdal.GDT_UInt32, np.int32: gdal.GDT_Int32, np.float16: gdal.GDT_Float32, # GDAL doesn't have Float16, use Float32 np.float32: gdal.GDT_Float32, np.float64: gdal.GDT_Float64, } return dtype_map.get(dtype.type, gdal.GDT_Float32) def _create_rgb_hdr_file(self, bip_path: Union[str, Path], shape: Tuple[int, int, int], dtype: np.dtype, rgb_space: str, input_hdr_path: Optional[Union[str, Path]] = None) -> None: """ Create ENVI header file for RGB data Parameters: bip_path: BIP file path shape: Data shape (height, width, channels) dtype: Data type rgb_space: RGB color space name input_hdr_path: Input HDR file to copy metadata from """ hdr_path = Path(bip_path).with_suffix('.hdr') height, width, channels = shape # Data type mapping for ENVI dtype_map = { np.uint8: '1', np.int8: '1', # ENVI doesn't distinguish signed/unsigned byte np.int16: '2', np.int32: '3', np.float32: '4', np.float64: '5', np.complex64: '6', np.complex128: '9', np.uint16: '12', np.uint32: '13', np.int64: '14', np.uint64: '15', np.float16: '4', # Use float32 for float16 } envi_dtype = dtype_map.get(dtype.type, '4') # Default to float32 with open(hdr_path, 'w') as f: f.write("ENVI\n") f.write("description = {\n") f.write(f" RGB Color Data - Converted from XYZ to {rgb_space}\n") f.write(" Band Interleaved by Pixel (BIP) format\n") f.write("}\n") f.write(f"samples = {width}\n") f.write(f"lines = {height}\n") f.write(f"bands = {channels}\n") f.write("header offset = 0\n") f.write("file type = ENVI Standard\n") f.write(f"data type = {envi_dtype}\n") f.write("interleave = bip\n") f.write("sensor type = Unknown\n") f.write("byte order = 0\n") # Band names f.write("band names = {\n") band_names = ['Red', 'Green', 'Blue'] for i, name in enumerate(band_names): f.write(f' "{name}"') if i < len(band_names) - 1: f.write(",") f.write("\n") f.write("}\n") # RGB-specific metadata f.write(f"RGB_color_space = {rgb_space}\n") f.write(f"data_type_description = {dtype}\n") # Copy metadata from input file if available if input_hdr_path and Path(input_hdr_path).exists(): try: self._copy_hdr_metadata(input_hdr_path, f) except Exception as e: print(f"Warning: Failed to copy metadata from input HDR: {e}") print(f"ENVI header file created: {hdr_path}") def _copy_hdr_metadata(self, input_hdr_path: Union[str, Path], output_file) -> None: """ Copy relevant metadata from input HDR file Parameters: input_hdr_path: Input HDR file path output_file: Output file object """ try: with open(input_hdr_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() lines = content.split('\n') metadata_to_copy = [] # Fields to copy fields_to_copy = [ 'wavelength units', 'wavelength', 'fwhm', 'bbl', 'map info', 'coordinate system string', 'projection info', 'pixel size', 'acquisition time', 'sensor type' ] i = 0 while i < len(lines): line = lines[i].strip() if '=' in line: key = line.split('=')[0].strip().lower() if any(field in key for field in fields_to_copy): metadata_to_copy.append(lines[i]) i += 1 # Continue reading multi-line values while i < len(lines) and not ('=' in lines[i] and not lines[i].strip().endswith(',')): if lines[i].strip(): metadata_to_copy.append(lines[i]) i += 1 if i >= len(lines): break continue i += 1 # Write copied metadata if metadata_to_copy: output_file.write("\n") for line in metadata_to_copy: if line.strip(): output_file.write(line + "\n") print(f"Copied {len(metadata_to_copy)} metadata lines from input HDR") except Exception as e: print(f"Error reading input HDR file: {e}") def _print_xyz_range(self, xyz_data: np.ndarray, data_name: str) -> None: """Print XYZ value ranges""" X_min, X_max = xyz_data[..., 0].min(), xyz_data[..., 0].max() Y_min, Y_max = xyz_data[..., 1].min(), xyz_data[..., 1].max() Z_min, Z_max = xyz_data[..., 2].min(), xyz_data[..., 2].max() print(f"{data_name} XYZ value ranges:") print(f" X: [{X_min:.3f}, {X_max:.3f}]") print(f" Y: [{Y_min:.3f}, {Y_max:.3f}]") print(f" Z: [{Z_min:.3f}, {Z_max:.3f}]") class XYZ2RGBApp: """Main application class for XYZ to RGB conversion""" def __init__(self): self.converter = None self.file_handler = None if not SPECTRAL_AVAILABLE else EnviFileHandler() def run(self, args): """Run the conversion application""" print("=" * 60) print("XYZ to RGB Color Space Converter") print("=" * 60) # Initialize converter rgb_space = RGBColorSpace(args.rgb_space) gamma_method = GammaMethod(args.gamma) output_dtype = OutputDataType(args.output_dtype) self.converter = XYZ2RGBConverter(rgb_space, gamma_method, output_dtype) # Load XYZ image xyz_data, metadata = self.file_handler.load_xyz_image(args.input) # Convert XYZ to RGB rgb_data = self.converter.convert_xyz_to_rgb(xyz_data) # Save RGB image output_path = Path(args.output) self.file_handler.save_rgb_image(rgb_data, output_path, rgb_space.value, args.input) print("=" * 60) print("Conversion completed successfully!") print("=" * 60) def parse_arguments(): """Parse command line arguments""" parser = argparse.ArgumentParser( description='XYZ to RGB Color Space Converter', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Usage examples: # Convert XYZ to sRGB with uint8 output python XYZ2RGB.py --input xyz_image.dat --output rgb_image.bip # Convert to Adobe RGB with gamma correction python XYZ2RGB.py --input xyz_image.dat --output rgb_image.bip \\ --rgb-space "Adobe RGB (1998)" --gamma sRGB # Convert to DCI-P3 with float32 output python XYZ2RGB.py --input xyz_image.dat --output rgb_image.bip \\ --rgb-space "DCI-P3" --output-dtype float32 """ ) # Required arguments parser.add_argument('--input', required=True, help='Input XYZ image file path (.dat, .bip, .bsq, etc.)') parser.add_argument('--output', required=True, help='Output RGB image file path (.bip)') # Conversion parameters parser.add_argument('--rgb-space', default='sRGB', choices=[space.value for space in RGBColorSpace], help='Target RGB color space (default: sRGB)') parser.add_argument('--gamma', default='sRGB', choices=[method.value for method in GammaMethod], help='Gamma correction method (default: sRGB)') parser.add_argument('--output-dtype', default='uint8', choices=[dtype.value for dtype in OutputDataType], help='Output data type (default: uint8)') return parser.parse_args() # Direct function call interface def convert_xyz_to_rgb(input_path: Union[str, Path], output_path: Union[str, Path], rgb_space: str = 'sRGB', gamma_method: str = 'sRGB', output_dtype: str = 'uint8') -> np.ndarray: """ Directly convert XYZ image to RGB Parameters: input_path: Input XYZ image file path output_path: Output RGB image file path rgb_space: Target RGB color space gamma_method: Gamma correction method output_dtype: Output data type Returns: rgb_data: Converted RGB data array Example: >>> rgb_data = convert_xyz_to_rgb( ... input_path="xyz_image.dat", ... output_path="rgb_image.bip", ... rgb_space="Adobe RGB (1998)", ... output_dtype="float32" ... ) """ # Validate dependencies if not SPECTRAL_AVAILABLE: raise ImportError("spectral library is required: pip install spectral") if not COLOUR_AVAILABLE: raise ImportError("colour-science library is required: pip install colour-science") # Create application instance app = XYZ2RGBApp() try: # Initialize converter rgb_space_enum = RGBColorSpace(rgb_space) gamma_enum = GammaMethod(gamma_method) dtype_enum = OutputDataType(output_dtype) app.converter = XYZ2RGBConverter(rgb_space_enum, gamma_enum, dtype_enum) # Load and convert xyz_data, metadata = app.file_handler.load_xyz_image(input_path) rgb_data = app.converter.convert_xyz_to_rgb(xyz_data) # Save result app.file_handler.save_rgb_image(rgb_data, output_path, rgb_space, input_path) print(f"Conversion completed! RGB data saved to: {output_path}") return rgb_data except Exception as e: print(f"Conversion failed: {e}") raise def main(): """Main function""" try: # Parse command line arguments args = parse_arguments() # Validate required libraries if not SPECTRAL_AVAILABLE: print("Error: spectral library is required for ENVI file processing") print("Install with: pip install spectral") sys.exit(1) if not COLOUR_AVAILABLE: print("Error: colour-science library is required for color space conversion") print("Install with: pip install colour-science") sys.exit(1) # Create and run application app = XYZ2RGBApp() app.run(args) except ImportError as e: print(f"Import error: {e}") print("Please ensure all required libraries are installed:") print(" pip install numpy colour-science spectral") if TQDM_AVAILABLE: print(" pip install tqdm # Optional, for progress bars") sys.exit(1) except FileNotFoundError as e: print(f"File error: File not found - {e}") sys.exit(1) except ValueError as e: print(f"Data error: {e}") sys.exit(1) except Exception as e: print(f"Unexpected error: {e}") import traceback traceback.print_exc() sys.exit(1) # Direct execution example def main(): """主函数:命令行接口""" import argparse parser = argparse.ArgumentParser( description='XYZ到RGB颜色空间转换工具', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 使用示例: 1. 基本转换 (sRGB, uint8): python XYZ2RGB.py input_xyz.hdr -o output_rgb.bip 2. 指定RGB色彩空间和数据类型: python XYZ2RGB.py input_xyz.dat -s "Adobe RGB (1998)" -g gamma_2.2 -t uint16 -o output.bip 3. 无伽马校正 (保持线性): python XYZ2RGB.py input_xyz.hdr -g none -t float32 -o output_linear.bip 支持的RGB色彩空间: sRGB, Adobe RGB (1998), DCI-P3, ITU-R BT.709, ITU-R BT.2020, ACES2065-1, ACEScg, ProPhoto RGB, Apple RGB, PAL/SECAM, NTSC (1953) 支持的伽马方法: sRGB, BT.709, gamma_2.2, gamma_1.8, L*, BT.1886, ST 2084, HLG, none 支持的输出数据类型: uint8, uint10, uint12, uint16, int8, int16, float16, float32, float64 """ ) parser.add_argument('input_path', help='输入XYZ文件路径 (.hdr 或 .dat)') parser.add_argument('-o', '--output', required=True, help='输出RGB文件路径') parser.add_argument('-s', '--rgb_space', default='sRGB', choices=['sRGB', 'Adobe RGB (1998)', 'DCI-P3', 'ITU-R BT.709', 'ITU-R BT.2020', 'ACES2065-1', 'ACEScg', 'ProPhoto RGB', 'Apple RGB', 'PAL/SECAM', 'NTSC (1953)'], help='RGB色彩空间 (默认: sRGB)') parser.add_argument('-g', '--gamma_method', default='sRGB', choices=['sRGB', 'BT.709', 'gamma_2.2', 'gamma_1.8', 'L*', 'BT.1886', 'ST 2084', 'HLG', 'none'], help='伽马校正方法 (默认: sRGB)') parser.add_argument('-t', '--output_dtype', default='uint8', choices=['uint8', 'uint10', 'uint12', 'uint16', 'int8', 'int16', 'float16', 'float32', 'float64'], help='输出数据类型 (默认: uint8)') args = parser.parse_args() try: print("=" * 60) print("XYZ到RGB颜色空间转换工具") print("=" * 60) print(f"输入文件: {args.input_path}") print(f"输出文件: {args.output}") print(f"RGB色彩空间: {args.rgb_space}") print(f"伽马方法: {args.gamma_method}") print(f"输出数据类型: {args.output_dtype}") print() rgb_result = convert_xyz_to_rgb( input_path=args.input_path, output_path=args.output, rgb_space=args.rgb_space, gamma_method=args.gamma_method, output_dtype=args.output_dtype ) print("\n" + "=" * 60) print("转换完成!") print(f"RGB数据形状: {rgb_result.shape}") print(f"RGB数据类型: {rgb_result.dtype}") print(f"值范围: [{rgb_result.min()}, {rgb_result.max()}]") print("=" * 60) except Exception as e: print(f"✗ 处理失败: {e}") import traceback traceback.print_exc() return 1 return 0 if __name__ == '__main__': exit(main())