Futher changes to bring refactor up to feature parity + Updated Docs
This commit is contained in:
@@ -31,17 +31,17 @@ def get_nearest_power_of_two_downscale(value: int) -> int:
|
||||
If the value is already a power of two, it returns the value itself.
|
||||
Returns 1 if the value is less than 1.
|
||||
"""
|
||||
if value < 1:
|
||||
if value < 1:
|
||||
return 1
|
||||
if is_power_of_two(value):
|
||||
return value
|
||||
# Find the largest power of two strictly less than value,
|
||||
# unless value itself is POT.
|
||||
# (1 << (value.bit_length() - 1)) achieves this.
|
||||
# Example: value=7 (0111, bl=3), 1<<2 = 4.
|
||||
# Example: value=8 (1000, bl=4), 1<<3 = 8.
|
||||
# Example: value=9 (1001, bl=4), 1<<3 = 8.
|
||||
return 1 << (value.bit_length() - 1)
|
||||
# (1 << (value.bit_length() - 1)) achieves this.
|
||||
# Example: value=7 (0111, bl=3), 1<<2 = 4.
|
||||
# Example: value=8 (1000, bl=4), 1<<3 = 8.
|
||||
# Example: value=9 (1001, bl=4), 1<<3 = 8.
|
||||
return 1 << (value.bit_length() - 1)
|
||||
# --- Dimension Calculation ---
|
||||
|
||||
def calculate_target_dimensions(
|
||||
@@ -184,10 +184,12 @@ def calculate_image_stats(image_data: np.ndarray) -> Optional[Dict]:
|
||||
stats["min"] = float(np.min(data_float))
|
||||
stats["max"] = float(np.max(data_float))
|
||||
stats["mean"] = float(np.mean(data_float))
|
||||
stats["median"] = float(np.median(data_float))
|
||||
elif len(data_float.shape) == 3: # Color (H, W, C)
|
||||
stats["min"] = [float(v) for v in np.min(data_float, axis=(0, 1))]
|
||||
stats["max"] = [float(v) for v in np.max(data_float, axis=(0, 1))]
|
||||
stats["mean"] = [float(v) for v in np.mean(data_float, axis=(0, 1))]
|
||||
stats["median"] = [float(v) for v in np.median(data_float, axis=(0, 1))]
|
||||
else:
|
||||
return None # Unsupported shape
|
||||
return stats
|
||||
@@ -235,46 +237,67 @@ def normalize_aspect_ratio_change(original_width: int, original_height: int, res
|
||||
if abs(output_width - 1.0) < epsilon: output_width = 1
|
||||
if abs(output_height - 1.0) < epsilon: output_height = 1
|
||||
|
||||
# Helper to format the number part
|
||||
def format_value(val, dec):
|
||||
# Multiply by 10^decimals, convert to int to keep trailing zeros in effect
|
||||
# e.g. val=1.1, dec=2 -> 1.1 * 100 = 110
|
||||
# e.g. val=1.0, dec=2 -> 1.0 * 100 = 100 (though this might become "1" if it's exactly 1.0 before this)
|
||||
# The existing logic already handles output_width/height being 1.0 to produce "EVEN" or skip a component.
|
||||
# This formatting is for when output_width/height is NOT 1.0.
|
||||
return str(int(round(val * (10**dec))))
|
||||
|
||||
if abs(output_width - output_height) < epsilon: # Handles original square or aspect maintained
|
||||
output = "EVEN"
|
||||
elif output_width != 1 and abs(output_height - 1.0) < epsilon : # Width changed, height maintained relative to width
|
||||
output = f"X{str(output_width).replace('.', '')}"
|
||||
output = f"X{format_value(output_width, decimals)}"
|
||||
elif output_height != 1 and abs(output_width - 1.0) < epsilon: # Height changed, width maintained relative to height
|
||||
output = f"Y{str(output_height).replace('.', '')}"
|
||||
output = f"Y{format_value(output_height, decimals)}"
|
||||
else: # Both changed relative to each other
|
||||
output = f"X{str(output_width).replace('.', '')}Y{str(output_height).replace('.', '')}"
|
||||
output = f"X{format_value(output_width, decimals)}Y{format_value(output_height, decimals)}"
|
||||
return output
|
||||
|
||||
# --- Image Loading, Conversion, Resizing ---
|
||||
|
||||
def load_image(image_path: Union[str, Path], read_flag: int = cv2.IMREAD_UNCHANGED) -> Optional[np.ndarray]:
|
||||
"""Loads an image from the specified path."""
|
||||
"""Loads an image from the specified path. Converts BGR/BGRA to RGB/RGBA if color."""
|
||||
try:
|
||||
img = cv2.imread(str(image_path), read_flag)
|
||||
if img is None:
|
||||
# print(f"Warning: Failed to load image: {image_path}") # Optional: for debugging utils
|
||||
return None
|
||||
|
||||
# Ensure RGB/RGBA for color images
|
||||
if len(img.shape) == 3:
|
||||
if img.shape[2] == 4: # BGRA from OpenCV
|
||||
img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGBA)
|
||||
elif img.shape[2] == 3: # BGR from OpenCV
|
||||
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
||||
return img
|
||||
except Exception: # as e:
|
||||
# print(f"Error loading image {image_path}: {e}") # Optional: for debugging utils
|
||||
return None
|
||||
|
||||
def convert_bgr_to_rgb(image: np.ndarray) -> np.ndarray:
|
||||
"""Converts an image from BGR to RGB color space."""
|
||||
"""Converts an image from BGR/BGRA to RGB/RGBA color space."""
|
||||
if image is None or len(image.shape) < 3:
|
||||
return image # Return as is if not a color image or None
|
||||
|
||||
if image.shape[2] == 4: # BGRA
|
||||
return cv2.cvtColor(image, cv2.COLOR_BGRA2RGB)
|
||||
return cv2.cvtColor(image, cv2.COLOR_BGRA2RGBA) # Keep alpha, convert to RGBA
|
||||
elif image.shape[2] == 3: # BGR
|
||||
return cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||
return image # Return as is if not 3 or 4 channels
|
||||
|
||||
def convert_rgb_to_bgr(image: np.ndarray) -> np.ndarray:
|
||||
"""Converts an image from RGB to BGR color space."""
|
||||
if image is None or len(image.shape) < 3 or image.shape[2] != 3: # Only for 3-channel RGB
|
||||
return image # Return as is if not a 3-channel color image or None
|
||||
return cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
||||
"""Converts an image from RGB/RGBA to BGR/BGRA color space."""
|
||||
if image is None or len(image.shape) < 3:
|
||||
return image # Return as is if not a color image or None
|
||||
|
||||
if image.shape[2] == 4: # RGBA
|
||||
return cv2.cvtColor(image, cv2.COLOR_RGBA2BGRA)
|
||||
elif image.shape[2] == 3: # RGB
|
||||
return cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
||||
return image # Return as is if not 3 or 4 channels
|
||||
|
||||
|
||||
def resize_image(image: np.ndarray, target_width: int, target_height: int, interpolation: Optional[int] = None) -> np.ndarray:
|
||||
@@ -349,18 +372,19 @@ def save_image(
|
||||
elif img_to_save.dtype == np.float16: img_to_save = img_to_save.astype(np.float32)
|
||||
|
||||
|
||||
# 2. Color Space Conversion (RGB -> BGR)
|
||||
# Typically, OpenCV expects BGR for formats like PNG, JPG. EXR usually expects RGB.
|
||||
# The `convert_to_bgr_before_save` flag controls this.
|
||||
# If output_format is exr, this should generally be False.
|
||||
# 2. Color Space Conversion (Internal RGB/RGBA -> BGR/BGRA for OpenCV)
|
||||
# Input `image_data` is assumed to be in RGB/RGBA format (due to `load_image` changes).
|
||||
# OpenCV's `imwrite` typically expects BGR/BGRA for formats like PNG, JPG.
|
||||
# EXR format usually expects RGB/RGBA.
|
||||
# The `convert_to_bgr_before_save` flag controls this behavior.
|
||||
current_format = output_format if output_format else path_obj.suffix.lower().lstrip('.')
|
||||
|
||||
if convert_to_bgr_before_save and current_format != 'exr':
|
||||
if len(img_to_save.shape) == 3 and img_to_save.shape[2] == 3:
|
||||
img_to_save = convert_rgb_to_bgr(img_to_save)
|
||||
# BGRA is handled by OpenCV imwrite for PNGs, no explicit conversion needed if saving as RGBA.
|
||||
# If it's 4-channel and not PNG/TIFF with alpha, it might need stripping or specific handling.
|
||||
# For simplicity, this function assumes 3-channel RGB input if BGR conversion is active.
|
||||
# If image is 3-channel (RGB) or 4-channel (RGBA), convert to BGR/BGRA.
|
||||
if len(img_to_save.shape) == 3 and (img_to_save.shape[2] == 3 or img_to_save.shape[2] == 4):
|
||||
img_to_save = convert_rgb_to_bgr(img_to_save) # Handles RGB->BGR and RGBA->BGRA
|
||||
# If `convert_to_bgr_before_save` is False or format is 'exr',
|
||||
# the image (assumed RGB/RGBA) is saved as is.
|
||||
|
||||
# 3. Save Image
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user