mirror of
https://github.com/ciromattia/kcc
synced 2025-12-13 01:36:27 +00:00
* Add rainbow_artifacts_eraser helper This helper file contains the methods necessary to perform a fourier transform on the picture, to remove frequencies responsible for rainbow artifacts on Kaleido screens, and performe the reverse fourier transform * Replace blurring method with frequency removal method to erase rainbow effect on Kaleido 3 screens * High performance improvements by using rfft2 instead of fft2 * Fine-tuned the settings and added the perpendicular direction for a better final rendering The finer settings allow for more information to be retained in the final image, while still effectively removing the rainbow effect. Adding the perpendicular direction results in a better rendering of the final image (avoiding visual artifacts related to suppression at the main angle). * Revert the addition of perpendicular angles and lower attenuation_factor It was a mistake to add the perpendicular angles in the previous commit: I had the rainbow effect removal process called 2 times when I did this, for testing purposes (One before downscale and one after downscale). The proper way to call the process is only after the downscale. And in this case it is not necessary to remove frequencies along the perpendicular angles. In the mean time, attenuation_factor=0.15 has proven to work well along a collection of testing images. It should be my latest commit for this feature * Also attenuate high frequencies at 45° CFA is sometimes orientated at 135°, sometimes at 45° so until we find if there is a law depending on the screen size, e-reader model or something, the best we can do is attenuate high frequencies on those two directions * fix imports * Update comic2ebook.py Calculate is_color with (opt.forcecolor and img.color) pass is_color to img.optimizeForDisplay * Update image.py Remove color check condition, because now we process colored images too. Pass is_color to erase_rainbow_artifacts * Update rainbow_artifacts_eraser.py Add support for colored images: Convert to YUV, extract luminance channel, do FFT -> Filter -> IFFT on luminance channel, insert back to YUV, convert back to RGB To maximize compatibility until we know for sure the orientation of CFA for each device, filtering is now done on 135° + 45° axis After more testing, attenuation_factor is decreased to 0.10 * Update comic2ebook.py Rename rainbow eraser param * Update image.py rename rainbow eraser param * Update KCC.ui Rename rainbow eraser checkbox and tooltip * Update KCC_ui.py Rename erase rainbow checkbox and tooltip * Update KCC_gui.py Rename erase rainbow checkbox and option * Update README.md rename erase rainbow param * Update KCC_gui.py correct param name for eraserainbow --------- Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
241 lines
8.4 KiB
Python
241 lines
8.4 KiB
Python
import numpy as np
|
|
from PIL import Image
|
|
|
|
def fourier_transform_image(img):
|
|
"""
|
|
Memory-optimized version that modifies the array in place when possible.
|
|
"""
|
|
# Convert with minimal copy
|
|
img_array = np.asarray(img, dtype=np.float32)
|
|
|
|
# Use rfft2 if the image is real to save memory
|
|
# and computation time (approximately 2x faster)
|
|
fft_result = np.fft.rfft2(img_array)
|
|
|
|
return fft_result
|
|
|
|
def attenuate_diagonal_frequencies(fft_spectrum, freq_threshold=0.30, target_angle=135,
|
|
angle_tolerance=10, attenuation_factor=0.10):
|
|
"""
|
|
Attenuates specific frequencies in the Fourier domain (optimized version for rfft2).
|
|
|
|
Args:
|
|
fft_spectrum: Result of 2D real Fourier transform (from rfft2)
|
|
freq_threshold: Frequency threshold in cycles/pixel (default: 0.3, theoretical max: 0.5)
|
|
target_angle: Target angle in degrees (default: 135)
|
|
angle_tolerance: Angular tolerance in degrees (default: 15)
|
|
attenuation_factor: Attenuation factor (0.1 = 90% attenuation)
|
|
|
|
Returns:
|
|
np.ndarray: Modified FFT with applied attenuation (same format as input)
|
|
"""
|
|
|
|
# Get dimensions of the rfft2 result
|
|
if fft_spectrum.ndim == 2:
|
|
height, width_rfft = fft_spectrum.shape
|
|
else: # 3D array (color channels)
|
|
height, width_rfft = fft_spectrum.shape[:2]
|
|
|
|
# For rfft2, the original width is (width_rfft - 1) * 2
|
|
width_original = (width_rfft - 1) * 2
|
|
|
|
# Create frequency grids for rfft2 format
|
|
freq_y = np.fft.fftfreq(height, d=1.0)
|
|
freq_x = np.fft.rfftfreq(width_original, d=1.0) # Use rfftfreq for the X dimension
|
|
|
|
|
|
# Use broadcasting to create grids without meshgrid (more efficient)
|
|
freq_y_grid = freq_y.reshape(-1, 1) # Column
|
|
freq_x_grid = freq_x.reshape(1, -1) # Row
|
|
|
|
# Calculate squared radial frequencies (avoid sqrt)
|
|
freq_radial_sq = freq_x_grid**2 + freq_y_grid**2
|
|
freq_threshold_sq = freq_threshold**2
|
|
|
|
# Frequency condition
|
|
freq_condition = freq_radial_sq >= freq_threshold_sq
|
|
|
|
# Early exit if no frequency satisfies the condition
|
|
if not np.any(freq_condition):
|
|
return fft_spectrum
|
|
|
|
# Calculate angles only where necessary
|
|
# Use atan2 directly with broadcasting
|
|
angles_rad = np.arctan2(freq_y_grid, freq_x_grid)
|
|
|
|
# Convert to degrees and normalize in a single operation
|
|
angles_deg = np.rad2deg(angles_rad) % 360
|
|
|
|
# Calculation of complementary angle
|
|
target_angle_2 = (target_angle + 180) % 360
|
|
|
|
# Calulation of perpendicular angles (135° + 45° to maximize compatibility until we know for sure which angle configure for each device)
|
|
target_angle_3 = (target_angle + 90) % 360
|
|
target_angle_4 = (target_angle_3 + 180) % 360
|
|
|
|
# Create angular conditions in a vectorized way
|
|
angle_condition = np.zeros_like(angles_deg, dtype=bool)
|
|
|
|
# Process both angles simultaneously
|
|
for angle in [target_angle, target_angle_2, target_angle_3, target_angle_4]:
|
|
min_angle = (angle - angle_tolerance) % 360
|
|
max_angle = (angle + angle_tolerance) % 360
|
|
|
|
if min_angle > max_angle: # Interval crosses 0°
|
|
angle_condition |= (angles_deg >= min_angle) | (angles_deg <= max_angle)
|
|
else: # Normal interval
|
|
angle_condition |= (angles_deg >= min_angle) & (angles_deg <= max_angle)
|
|
|
|
# Combine conditions
|
|
combined_condition = freq_condition & angle_condition
|
|
|
|
# Apply attenuation directly (avoid creating a full mask)
|
|
if attenuation_factor == 0:
|
|
# Special case: complete suppression
|
|
if fft_spectrum.ndim == 2:
|
|
fft_spectrum[combined_condition] = 0
|
|
else: # 3D array
|
|
fft_spectrum[combined_condition, :] = 0
|
|
return fft_spectrum
|
|
elif attenuation_factor == 1:
|
|
# Special case: no attenuation
|
|
return fft_spectrum
|
|
else:
|
|
# General case: partial attenuation
|
|
if fft_spectrum.ndim == 2:
|
|
fft_spectrum[combined_condition] *= attenuation_factor
|
|
else: # 3D array
|
|
fft_spectrum[combined_condition, :] *= attenuation_factor
|
|
return fft_spectrum
|
|
|
|
def inverse_fourier_transform_image(fft_spectrum, is_color):
|
|
"""
|
|
Performs an optimized inverse Fourier transform to reconstruct a PIL image.
|
|
|
|
Args:
|
|
fft_spectrum: Fourier transform result (complex array from rfft2)
|
|
is_color: Boolean indicating if the image is to be treated as color
|
|
|
|
Returns:
|
|
PIL.Image: Reconstructed image
|
|
"""
|
|
# Perform inverse Fourier transform
|
|
img_reconstructed = np.fft.irfft2(fft_spectrum)
|
|
|
|
# Normalize values between 0 and 255
|
|
img_reconstructed = np.clip(img_reconstructed, 0, 255)
|
|
img_reconstructed = img_reconstructed.astype(np.uint8)
|
|
|
|
# Convert to PIL image
|
|
if is_color and img_reconstructed.ndim == 3:
|
|
pil_image = Image.fromarray(img_reconstructed, mode='RGB')
|
|
else:
|
|
pil_image = Image.fromarray(img_reconstructed, mode='L')
|
|
|
|
return pil_image
|
|
|
|
def rgb_to_yuv(rgb_array):
|
|
"""
|
|
Convert RGB to YUV color space.
|
|
Y = luminance, U and V = chrominance
|
|
"""
|
|
# Coefficients for RGB to YUV conversion
|
|
rgb_to_yuv_matrix = np.array([
|
|
[0.299, 0.587, 0.114], # Y
|
|
[-0.14713, -0.28886, 0.436], # U
|
|
[0.615, -0.51499, -0.10001] # V
|
|
])
|
|
|
|
# Reshape for matrix multiplication
|
|
original_shape = rgb_array.shape
|
|
rgb_flat = rgb_array.reshape(-1, 3)
|
|
|
|
# Apply transformation
|
|
yuv_flat = rgb_flat @ rgb_to_yuv_matrix.T
|
|
|
|
# Reshape back
|
|
yuv_array = yuv_flat.reshape(original_shape)
|
|
|
|
return yuv_array
|
|
|
|
def yuv_to_rgb(yuv_array):
|
|
"""
|
|
Convert YUV to RGB color space.
|
|
"""
|
|
# Coefficients for YUV to RGB conversion
|
|
yuv_to_rgb_matrix = np.array([
|
|
[1.0, 0.0, 1.13983], # R
|
|
[1.0, -0.39465, -0.58060], # G
|
|
[1.0, 2.03211, 0.0] # B
|
|
])
|
|
|
|
# Reshape for matrix multiplication
|
|
original_shape = yuv_array.shape
|
|
yuv_flat = yuv_array.reshape(-1, 3)
|
|
|
|
# Apply transformation
|
|
rgb_flat = yuv_flat @ yuv_to_rgb_matrix.T
|
|
|
|
# Reshape back
|
|
rgb_array = rgb_flat.reshape(original_shape)
|
|
|
|
return rgb_array
|
|
|
|
def erase_rainbow_artifacts(img, is_color):
|
|
"""
|
|
Remove rainbow artifacts from grayscale or color images.
|
|
|
|
Args:
|
|
img: PIL Image (grayscale or RGB)
|
|
is_color: Boolean indicating if the image is to be treated as color
|
|
|
|
Returns:
|
|
PIL.Image: Cleaned image
|
|
"""
|
|
# Auto-detect color mode if not specified
|
|
if is_color is None:
|
|
color = img.mode in ('RGB', 'RGBA', 'L') and len(np.array(img).shape) == 3
|
|
|
|
if is_color and img.mode in ('RGB', 'RGBA'):
|
|
# Convert to RGB if needed
|
|
if img.mode == 'RGBA':
|
|
img = img.convert('RGB')
|
|
|
|
# Convert to numpy array
|
|
img_array = np.array(img, dtype=np.float32)
|
|
|
|
# Convert to YUV color space
|
|
yuv_array = rgb_to_yuv(img_array)
|
|
|
|
# Extract luminance channel (Y)
|
|
luminance = yuv_array[:, :, 0]
|
|
|
|
# Process only the luminance channel
|
|
fft_spectrum = fourier_transform_image(luminance)
|
|
clean_spectrum = attenuate_diagonal_frequencies(fft_spectrum)
|
|
clean_luminance = np.fft.irfft2(clean_spectrum)
|
|
|
|
# Normalize and clip luminance
|
|
clean_luminance = np.clip(clean_luminance, 0, 255)
|
|
|
|
# Replace luminance in YUV array
|
|
yuv_array[:, :, 0] = clean_luminance
|
|
|
|
# Convert back to RGB
|
|
rgb_array = yuv_to_rgb(yuv_array)
|
|
rgb_array = np.clip(rgb_array, 0, 255).astype(np.uint8)
|
|
|
|
# Convert back to PIL image
|
|
clean_image = Image.fromarray(rgb_array, mode='RGB')
|
|
|
|
else:
|
|
# Grayscale processing (original behavior)
|
|
if img.mode != 'L':
|
|
img = img.convert('L')
|
|
|
|
fft_spectrum = fourier_transform_image(img)
|
|
clean_spectrum = attenuate_diagonal_frequencies(fft_spectrum)
|
|
clean_image = inverse_fourier_transform_image(clean_spectrum, is_color)
|
|
|
|
return clean_image
|