diff --git a/HW01/.gitignore b/HW01/.gitignore index 6fcf43c..303b4a8 100644 --- a/HW01/.gitignore +++ b/HW01/.gitignore @@ -1,2 +1,5 @@ +# Python execution related +__pycache__/ + # IDE files .idea/ diff --git a/HW01/scripts/demo.py b/HW01/scripts/demo.py new file mode 100644 index 0000000..a9e8291 --- /dev/null +++ b/HW01/scripts/demo.py @@ -0,0 +1,90 @@ +# +# Digital Image Processing HW01 assignment demo file +# +# +# author: Christos Choutouridis +# date: 29/04/2025 +# + +try: + import os + import numpy as np + import matplotlib.pyplot as plt + from PIL import Image +except ImportError as e: + print("Missing package: ", e) + print("Run: pip install -r requirements.txt to install.") + exit(1) + +from hist_utils import calculate_hist_of_img +from hist_modif import perform_hist_eq, perform_hist_matching + +# Define filenames +input_filename = "input_img.jpg" +ref_filename = "ref_img.jpg" + +# Load images +input_img = Image.open(input_filename).convert("L") +ref_img = Image.open(ref_filename).convert("L") + +# Convert to numpy arrays in [0,1] +input_array = np.array(input_img).astype(float) / 255.0 +ref_array = np.array(ref_img).astype(float) / 255.0 + +# Create output directory +os.makedirs("demo_outputs", exist_ok=True) + + + + +def plot_comparison(input_array, output_array, title, filename): + """ + Create a 2x2 plot: input image, output image, input histogram, output histogram. + Save it to filename. + """ + fig, axes = plt.subplots(2, 2, figsize=(10, 8)) + + # Plot input image + axes[0, 0].imshow(input_array, cmap="gray", vmin=0, vmax=1) + axes[0, 0].set_title("Input Image") + axes[0, 0].axis("off") + + # Plot output image + axes[0, 1].imshow(output_array, cmap="gray", vmin=0, vmax=1) + axes[0, 1].set_title("Output Image") + axes[0, 1].axis("off") + + # Plot input histogram + input_hist = calculate_hist_of_img(input_array, return_normalized=True) + axes[1, 0].bar(list(input_hist.keys()), list(input_hist.values()), width=0.01) + axes[1, 0].set_title("Input Histogram") + + # Plot output histogram + output_hist = calculate_hist_of_img(output_array, return_normalized=True) + axes[1, 1].bar(list(output_hist.keys()), list(output_hist.values()), width=0.01) + axes[1, 1].set_title("Output Histogram") + + # Set overall title + fig.suptitle(title, fontsize=16) + + # Adjust layout + plt.tight_layout(rect=[0, 0, 1, 0.95]) + + # Save + plt.savefig(filename) + plt.close() + +# Modes to test +modes = ["greedy", "non-greedy", "post-disturbance"] + +# Run equalization +for mode in modes: + equalized_img = perform_hist_eq(input_array, mode) + out_filename = f"demo_outputs/equalization_{mode}.png" + plot_comparison(input_array, equalized_img, f"Histogram Equalization ({mode})", out_filename) + +# Run matching +for mode in modes: + matched_img = perform_hist_matching(input_array, ref_array, mode) + out_filename = f"demo_outputs/matching_{mode}.png" + plot_comparison(input_array, matched_img, f"Histogram Matching ({mode})", out_filename) diff --git a/HW01/scripts/demo_outputs/equalization_greedy.png b/HW01/scripts/demo_outputs/equalization_greedy.png new file mode 100644 index 0000000..2fb9d6a Binary files /dev/null and b/HW01/scripts/demo_outputs/equalization_greedy.png differ diff --git a/HW01/scripts/demo_outputs/equalization_non-greedy.png b/HW01/scripts/demo_outputs/equalization_non-greedy.png new file mode 100644 index 0000000..1ccd87a Binary files /dev/null and b/HW01/scripts/demo_outputs/equalization_non-greedy.png differ diff --git a/HW01/scripts/demo_outputs/equalization_post-disturbance.png b/HW01/scripts/demo_outputs/equalization_post-disturbance.png new file mode 100644 index 0000000..cdcb37b Binary files /dev/null and b/HW01/scripts/demo_outputs/equalization_post-disturbance.png differ diff --git a/HW01/scripts/demo_outputs/matching_greedy.png b/HW01/scripts/demo_outputs/matching_greedy.png new file mode 100644 index 0000000..80854a8 Binary files /dev/null and b/HW01/scripts/demo_outputs/matching_greedy.png differ diff --git a/HW01/scripts/demo_outputs/matching_non-greedy.png b/HW01/scripts/demo_outputs/matching_non-greedy.png new file mode 100644 index 0000000..1374719 Binary files /dev/null and b/HW01/scripts/demo_outputs/matching_non-greedy.png differ diff --git a/HW01/scripts/demo_outputs/matching_post-disturbance.png b/HW01/scripts/demo_outputs/matching_post-disturbance.png new file mode 100644 index 0000000..e64194c Binary files /dev/null and b/HW01/scripts/demo_outputs/matching_post-disturbance.png differ diff --git a/HW01/scripts/hist_modif.py b/HW01/scripts/hist_modif.py index e69de29..e5a0e94 100644 --- a/HW01/scripts/hist_modif.py +++ b/HW01/scripts/hist_modif.py @@ -0,0 +1,373 @@ +# +# histogram modification file +# +# +# author: Christos Choutouridis +# date: 29/04/2025 +# + +try: + import numpy as np +except ImportError as e: + print("Missing package: ", e) + print("Run: pip install -r requirements.txt to install.") + exit(1) + +import hist_utils + + +def perform_hist_modification( + img_array: np.ndarray, + hist_ref: dict, + mode: str, + L: int = 256 +) -> np.ndarray: + """ + Perform histogram modification on an image according to the desired histogram and selected mode. + + Args: + img_array (np.ndarray): Input grayscale image with float values in [0, 1]. + hist_ref (dict): Desired output histogram (levels -> relative frequencies). + mode (str): One of ["greedy", "non-greedy", "post-disturbance"]. + L (int): Number of levels to clip + Returns: + np.ndarray: Modified image array. + """ + if mode not in ["greedy", "non-greedy", "post-disturbance"]: + raise ValueError(f"Unknown mode: {mode}") + + # Step 1: Optional post-disturbance (add noise) + if mode == "post-disturbance": + unique_levels = np.unique(img_array) + if len(unique_levels) < 2: + raise ValueError("Post-disturbance mode requires at least two unique input levels.") + + d = unique_levels[1] - unique_levels[0] + noise = np.random.uniform(-d / 2, d / 2, size=img_array.shape) + + # Add noise + img_array = img_array + noise + + # Clip to [0, 1] and quantize to 256 levels to avoid infinite precision + img_array = np.clip(img_array, 0, 1) + img_array = np.round(img_array * L) / float(L) + + # Step 2: Calculate input histogram (not normalized) + input_hist = hist_utils.calculate_hist_of_img(img_array, return_normalized=False) + + # Step 3: Build the modification transform + modification_transform = {} + + # Prepare input levels (sorted) + input_levels = sorted(input_hist.keys()) + input_counts = [input_hist[level] for level in input_levels] + + # Prepare target output levels and target counts + output_levels = sorted(hist_ref.keys()) + output_frequencies = [hist_ref[level] for level in output_levels] + total_samples = sum(input_counts) + output_target_counts = [freq * total_samples for freq in output_frequencies] + + # Initialize pointers + in_idx = 0 + out_idx = 0 + current_in_count = 0 + current_out_target = output_target_counts[out_idx] + + while in_idx < len(input_levels): + in_level = input_levels[in_idx] + in_count = input_counts[in_idx] + + if mode in ["greedy", "post-disturbance"]: + if current_in_count + in_count <= current_out_target: + modification_transform[in_level] = output_levels[out_idx] + current_in_count += in_count + in_idx += 1 + else: + out_idx += 1 + if out_idx < len(output_levels): + current_out_target = output_target_counts[out_idx] + current_in_count = 0 + else: + # No more output levels: assign the rest to the last output level + for remaining_idx in range(in_idx, len(input_levels)): + modification_transform[input_levels[remaining_idx]] = output_levels[-1] + break + + elif mode == "non-greedy": + deficiency = current_out_target - current_in_count + if deficiency >= in_count / 2: + modification_transform[in_level] = output_levels[out_idx] + current_in_count += in_count + in_idx += 1 + else: + out_idx += 1 + if out_idx < len(output_levels): + current_out_target = output_target_counts[out_idx] + current_in_count = 0 + else: + # No more output levels: assign the rest to the last output level + for remaining_idx in range(in_idx, len(input_levels)): + modification_transform[input_levels[remaining_idx]] = output_levels[-1] + break + + # Step 4: Apply the transform + modified_img = hist_utils.apply_hist_modification_transform(img_array, modification_transform) + + return modified_img + + +def perform_hist_eq( + img_array: np.ndarray, + mode: str, + L: int = 256 # number of levels to use for output +) -> np.ndarray: + """ + Perform histogram equalization on an input image according to the selected mode. + + Args: + img_array (np.ndarray): Input grayscale image with float values in [0, 1]. + mode (str): One of ["greedy", "non-greedy", "post-disturbance"]. + L (int): Number of levels to use for output + Returns: + np.ndarray: Equalized image array. + """ + # Step 1: define L uniformly spaced output levels + output_levels = np.linspace(0, 1, L) + hist_ref = {level: 1.0 / L for level in output_levels} + + # Step 2: call the generic modification function + equalized_img = perform_hist_modification(img_array, hist_ref, mode) + + return equalized_img + + +def perform_hist_matching( + img_array: np.ndarray, + img_array_ref: np.ndarray, + mode: str +) -> np.ndarray: + """ + Perform histogram matching of img_array to match img_array_ref according to the selected mode. + + Args: + img_array (np.ndarray): Input grayscale image. + img_array_ref (np.ndarray): Reference grayscale image. + mode (str): One of ["greedy", "non-greedy", "post-disturbance"]. + + Returns: + np.ndarray: Processed image with histogram matched to the reference. + """ + # Step 1: Calculate the normalized histogram of the reference image + ref_hist = hist_utils.calculate_hist_of_img(img_array_ref, return_normalized=True) + + # Step 2: Perform histogram modification + processed_img = perform_hist_modification(img_array, ref_hist, mode) + + return processed_img + + + +# +# =========================== Functional tests =========================== +# +# To execute the tests run: +# python hist_modif.py +# + +def test_perform_hist_modification_greedy(): + """ + Test perform_hist_modification with mode 'greedy' on a simple 3x3 image. + """ + img = np.array([ + [0.0, 0.0, 0.5], + [0.5, 1.0, 1.0], + [1.0, 0.5, 0.0] + ]) + + hist_ref = { + 0.25: 0.5, + 0.75: 0.5 + } + + modified_img = perform_hist_modification(img, hist_ref, mode="greedy") + + modified_hist = hist_utils.calculate_hist_of_img(modified_img, return_normalized=True) + + print("Mode: greedy") + print("Modified image:\n", modified_img) + print("Modified histogram:", modified_hist) + + output_levels = set(modified_hist.keys()) + expected_levels = {0.25, 0.75} + print("Test passed:", output_levels == expected_levels) + print() + + +def test_perform_hist_modification_non_greedy(): + """ + Test perform_hist_modification with mode 'non-greedy' on a simple 3x3 image. + """ + img = np.array([ + [0.0, 0.0, 0.5], + [0.5, 1.0, 1.0], + [1.0, 0.5, 0.0] + ]) + + hist_ref = { + 0.2: 0.4, + 0.8: 0.6 + } + + modified_img = perform_hist_modification(img, hist_ref, mode="non-greedy") + + modified_hist = hist_utils.calculate_hist_of_img(modified_img, return_normalized=True) + + print("Mode: non-greedy") + print("Modified image:\n", modified_img) + print("Modified histogram:", modified_hist) + + output_levels = set(modified_hist.keys()) + expected_levels = {0.2, 0.8} + print("Test passed:", output_levels == expected_levels) + print() + + +def test_perform_hist_modification_post_disturbance(): + """ + Test perform_hist_modification with mode 'post-disturbance' on a simple 3x3 image. + """ + img = np.array([ + [0.0, 0.0, 0.5], + [0.5, 1.0, 1.0], + [1.0, 0.5, 0.0] + ]) + + hist_ref = { + 0.3: 0.5, + 0.7: 0.5 + } + + modified_img = perform_hist_modification(img, hist_ref, mode="post-disturbance") + + modified_hist = hist_utils.calculate_hist_of_img(modified_img, return_normalized=True) + + print("Mode: post-disturbance") + print("Modified image:\n", modified_img) + print("Modified histogram:", modified_hist) + + output_levels = set(modified_hist.keys()) + expected_levels = {0.3, 0.7} + print("Test passed:", output_levels == expected_levels) + print() + + + +def test_perform_hist_eq_all_modes(): + """ + Test perform_hist_eq for all modes on a simple 3x3 image. + """ + img = np.array([ + [0.0, 0.0, 0.5], + [0.5, 1.0, 1.0], + [1.0, 0.5, 0.0] + ]) + + for mode in ["greedy", "non-greedy", "post-disturbance"]: + print(f"Mode: {mode} (histogram equalization)") + + equalized_img = perform_hist_eq(img, mode=mode) + equalized_hist = hist_utils.calculate_hist_of_img(equalized_img, return_normalized=True) + + print("Equalized image:\n", equalized_img) + print("Equalized histogram:", equalized_hist) + + levels = list(equalized_hist.keys()) + freqs = list(equalized_hist.values()) + expected_freq = 1.0 / len(levels) + + uniform_distribution = all(np.isclose(freq, expected_freq, atol=1e-2) for freq in freqs) + print("Test passed:", uniform_distribution) + print() + + + +def test_perform_hist_matching_all_modes(): + """ + Test perform_hist_matching for all modes on a simple 3x3 image. + """ + img = np.array([ + [0.0, 0.0, 0.5], + [0.5, 1.0, 1.0], + [1.0, 0.5, 0.0] + ]) + + # Reference image (different histogram) + ref_img = np.array([ + [0.2, 0.2, 0.2], + [0.8, 0.8, 0.8], + [0.8, 0.2, 0.2] + ]) + + for mode in ["greedy", "non-greedy", "post-disturbance"]: + print(f"Mode: {mode} (histogram matching)") + + matched_img = perform_hist_matching(img, ref_img, mode=mode) + matched_hist = hist_utils.calculate_hist_of_img(matched_img, return_normalized=True) + ref_hist = hist_utils.calculate_hist_of_img(ref_img, return_normalized=True) + + print("Matched image:\n", matched_img) + print("Matched histogram:", matched_hist) + print("Reference histogram:", ref_hist) + + matched_levels = set(matched_hist.keys()) + ref_levels = set(ref_hist.keys()) + + # Check if output levels are the same as the reference levels + print("Test passed:", matched_levels == ref_levels) + print() + + +def test_perform_hist_matching_simple(): + """ + Simple test for perform_hist_matching function. + """ + # Create a small input image + img = np.array([ + [0.0, 0.5], + [1.0, 0.5] + ]) + + # Create a reference image with known distribution + ref_img = np.array([ + [0.25, 0.25], + [0.75, 0.75] + ]) + + # Perform histogram matching using greedy mode + matched_img = perform_hist_matching(img, ref_img, mode="greedy") + + # Calculate histograms + matched_hist = hist_utils.calculate_hist_of_img(matched_img, return_normalized=True) + ref_hist = hist_utils.calculate_hist_of_img(ref_img, return_normalized=True) + + print("Simple test for perform_hist_matching") + print("Matched image:\n", matched_img) + print("Matched histogram:", matched_hist) + print("Reference histogram:", ref_hist) + + # Check if output levels are same as reference levels + matched_levels = set(matched_hist.keys()) + ref_levels = set(ref_hist.keys()) + + print("Test passed:", matched_levels == ref_levels) + print() + + +if __name__ == '__main__': + test_perform_hist_modification_greedy() + test_perform_hist_modification_non_greedy() + test_perform_hist_modification_post_disturbance() + test_perform_hist_eq_all_modes() + test_perform_hist_matching_all_modes() + test_perform_hist_matching_simple() \ No newline at end of file diff --git a/HW01/scripts/hist_utils.py b/HW01/scripts/hist_utils.py index 2b64c81..981361b 100644 --- a/HW01/scripts/hist_utils.py +++ b/HW01/scripts/hist_utils.py @@ -8,7 +8,6 @@ try: import numpy as np - from PIL import Image except ImportError as e: print("Missing package: ", e) print("Run: pip install -r requirements.txt to install.") diff --git a/HW01/scripts/input_img.jpg b/HW01/scripts/input_img.jpg new file mode 100644 index 0000000..4b25274 Binary files /dev/null and b/HW01/scripts/input_img.jpg differ diff --git a/HW01/scripts/ref_img.jpg b/HW01/scripts/ref_img.jpg new file mode 100644 index 0000000..a7a9dc9 Binary files /dev/null and b/HW01/scripts/ref_img.jpg differ diff --git a/HW01/scripts/requirements.txt b/HW01/scripts/requirements.txt index 0d59398..e9994ae 100644 --- a/HW01/scripts/requirements.txt +++ b/HW01/scripts/requirements.txt @@ -1,2 +1,4 @@ numpy Pillow +matplotlib +