增加模块;增加主调用命令
This commit is contained in:
877
color_method/XYZ2RGB.py
Normal file
877
color_method/XYZ2RGB.py
Normal 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())
|
||||
Reference in New Issue
Block a user