增加模块;增加主调用命令

This commit is contained in:
2026-01-07 16:36:47 +08:00
commit 2d4b170a45
109 changed files with 55763 additions and 0 deletions

877
color_method/XYZ2RGB.py Normal file
View File

@ -0,0 +1,877 @@
#!/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())