from typing import Optional, Any import numpy as np from utils import generate_rotation_matrices, compute_histogram from histogram_transfer.pdf_transfer_1d import pdf_transfer_1d from histogram_transfer.regrain import regrain def apply_histogram( source_img: np.ndarray | Any, target_histogram: np.ndarray, *, iterations: int = 25, alpha: float = 0.3, source_histogram: Optional[np.ndarray] = None, should_regrain: bool = True, ): if not isinstance(source_img, np.ndarray): source_img = np.array(source_img) assert ( target_histogram.shape[0] == target_histogram.shape[1] == target_histogram.shape[2] ), "Histograms must be 3D" bins = target_histogram.shape[0] assert 256 % bins == 0, "Bin size must be a factor of 256" if source_histogram is None: source_histogram = compute_histogram(source_img, bins=bins) else: assert ( source_histogram.shape == target_histogram.shape ), "Source and target histograms must be the same shape" source_colours = np.array( [ index for index, value in np.ndenumerate(source_histogram) if value > 0 ] # we ignore colours without occurences ).T # unique rgb colours, 3xN source_colours_original = source_colours.copy() source_counts = np.array( [value for value in np.nditer(source_histogram) if value > 0] ) # cardinality of each rgb colour in source_colours, N target_histogram_colours = np.array( [index for index, value in np.ndenumerate(target_histogram) if value > 0] ).T target_histogram_counts = np.array( [value for value in np.nditer(target_histogram) if value > 0] ) source_colour_adjustment = np.zeros_like(source_colours) for iteration, rotation in enumerate(generate_rotation_matrices(iterations)): rotated_source_colours = rotation @ source_colours rotated_target_histogram_colours = rotation @ target_histogram_colours rotated_source_colours.round(out=rotated_source_colours) rotated_target_histogram_colours.round(out=rotated_target_histogram_colours) assert rotation.shape[0] == 3 for axis in range(rotation.shape[0]): # for each axis (rgb) sorted_source_colours = sorted( enumerate(source_counts), key=lambda x: rotated_source_colours[axis, x[0]], ) sorted_source_indices = [i for i, _ in sorted_source_colours] sorted_source_counts = [v for _, v in sorted_source_colours] sorted_target_histogram_counts = [ v for _, v in sorted( enumerate(target_histogram_counts), key=lambda x: rotated_target_histogram_colours[axis, x[0]], ) ] sorted_target_histogram_colours = [ v for v in sorted(rotated_target_histogram_colours[axis, :]) ] sorted_new_source_colours = pdf_transfer_1d( sorted_source_counts, sorted_target_histogram_counts, sorted_target_histogram_colours, ) source_colour_adjustment[axis, :] = [ v for _, v in sorted( enumerate(sorted_new_source_colours), key=lambda x: sorted_source_indices[x[0]], ) ] source_colours = source_colours + (1 - (iteration / iterations) ** alpha) * ( rotation.T @ (source_colour_adjustment - rotated_source_colours) ) scale = 256 // bins source_colours = source_colours.T * scale source_colours.clip(0, 255, out=source_colours) lut = np.zeros((256, 256, 256, 3), dtype=np.uint8) for old, new in zip(source_colours_original.T, source_colours): for x in range(scale): for y in range(scale): for z in range(scale): lut[old[0] * scale + x, old[1] * scale + y, old[2] * scale + z] = ( new ) result = lut[source_img[:, :, 0], source_img[:, :, 1], source_img[:, :, 2]] return regrain(source_img, result) if should_regrain else result