HW01: First running version
3
HW01/.gitignore
vendored
@ -1,2 +1,5 @@
|
||||
# Python execution related
|
||||
__pycache__/
|
||||
|
||||
# IDE files
|
||||
.idea/
|
||||
|
90
HW01/scripts/demo.py
Normal file
@ -0,0 +1,90 @@
|
||||
#
|
||||
# Digital Image Processing HW01 assignment demo file
|
||||
#
|
||||
#
|
||||
# author: Christos Choutouridis <cchoutou@ece.auth.gr>
|
||||
# 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)
|
BIN
HW01/scripts/demo_outputs/equalization_greedy.png
Normal file
After Width: | Height: | Size: 152 KiB |
BIN
HW01/scripts/demo_outputs/equalization_non-greedy.png
Normal file
After Width: | Height: | Size: 158 KiB |
BIN
HW01/scripts/demo_outputs/equalization_post-disturbance.png
Normal file
After Width: | Height: | Size: 154 KiB |
BIN
HW01/scripts/demo_outputs/matching_greedy.png
Normal file
After Width: | Height: | Size: 199 KiB |
BIN
HW01/scripts/demo_outputs/matching_non-greedy.png
Normal file
After Width: | Height: | Size: 229 KiB |
BIN
HW01/scripts/demo_outputs/matching_post-disturbance.png
Normal file
After Width: | Height: | Size: 208 KiB |
@ -0,0 +1,373 @@
|
||||
#
|
||||
# histogram modification file
|
||||
#
|
||||
#
|
||||
# author: Christos Choutouridis <cchoutou@ece.auth.gr>
|
||||
# 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()
|
@ -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.")
|
||||
|
BIN
HW01/scripts/input_img.jpg
Normal file
After Width: | Height: | Size: 657 KiB |
BIN
HW01/scripts/ref_img.jpg
Normal file
After Width: | Height: | Size: 136 KiB |
@ -1,2 +1,4 @@
|
||||
numpy
|
||||
Pillow
|
||||
matplotlib
|
||||
|
||||
|