diff --git a/HW03/scripts/demo3a.py b/HW03/scripts/demo3a.py new file mode 100644 index 0000000..828512b --- /dev/null +++ b/HW03/scripts/demo3a.py @@ -0,0 +1,59 @@ +# +# Demo 3a: Non-recursive normalized cuts +# +# author: Christos Choutouridis +# date: 06/07/2025 +# +try: + # Testing requirements + from scipy.io import loadmat + import matplotlib.pyplot as plt + import numpy as np + + # Project imports + from normalized_cuts import n_cuts + from spectral_clustering import spectral_clustering + from image_to_graph import image_to_graph +except ImportError as e: + print("Missing package:", e) + print("Run: pip install -r requirements.txt") + exit(1) + + + + +def plot_segmentation(image, labels, k, title, fname): + M, N, _ = image.shape + segmented = labels.reshape(M, N) + plt.imshow(segmented, cmap='tab10', vmin=0, vmax=k-1) + plt.title(title) + plt.axis('off') + plt.tight_layout() + plt.savefig(fname) + print(f"Saved: {fname}") + plt.close() + + +def run_demo3a(): + data = loadmat("dip_hw_3.mat") + + for name in ["d2a", "d2b"]: + img = data[name] + print(f"\n=== Image {name} ===") + + affinity = image_to_graph(img) + + for k in [2, 3, 4]: + print(f" k = {k}") + + labels_nc = n_cuts(affinity, k=k) + labels_sc = spectral_clustering(affinity, k=k, normalized=False) + labels_sc_nrm = spectral_clustering(affinity, k=k, normalized=True) + + plot_segmentation(img, labels_nc, k, f"{name} - n_cuts (k={k})", f"plots/demo3a_{name}_ncuts_k{k}.png") + plot_segmentation(img, labels_sc, k, f"{name} - spectral (k={k})", f"plots/demo3a_{name}_spectral_k{k}.png") + plot_segmentation(img, labels_sc_nrm, k, f"{name} - spectral-Lnorm (k={k})", f"plots/demo3a_{name}_spectral_k{k}_norm.png") + + +if __name__ == '__main__': + run_demo3a() diff --git a/HW03/scripts/demo3b.py b/HW03/scripts/demo3b.py new file mode 100644 index 0000000..bde243b --- /dev/null +++ b/HW03/scripts/demo3b.py @@ -0,0 +1,49 @@ +# +# Demo 3b: One-step recursive normalized cuts with Ncut metric +# +# author: Christos Choutouridis +# date: 06/07/2025 +# +try: + from scipy.io import loadmat + import matplotlib.pyplot as plt + import numpy as np + #Project requirements + from image_to_graph import image_to_graph + from normalized_cuts import n_cuts, calculate_n_cut_value +except ImportError as e: + print("Missing package:", e) + print("Run: pip install -r requirements.txt") + exit(1) + + +def plot_split(image, labels, title, fname): + M, N, _ = image.shape + segmented = labels.reshape(M, N) + plt.imshow(segmented, cmap='tab10', vmin=0, vmax=1) + plt.title(title) + plt.axis('off') + plt.tight_layout() + plt.savefig(fname) + print(f"Saved: {fname}") + plt.close() + + +def run_demo3b(): + data = loadmat("dip_hw_3.mat") + + for name in ["d2a", "d2b"]: + img = data[name] + print(f"\n=== Image {name} ===") + + affinity = image_to_graph(img) + + labels = n_cuts(affinity, k=2) + ncut_val = calculate_n_cut_value(affinity, labels) + + print(f" Ncut value: {ncut_val:.4f}") + plot_split(img, labels, f"{name} - one step n_cuts (Ncut={ncut_val:.4f})", f"plots/demo3b_{name}_ncut.png") + + +if __name__ == '__main__': + run_demo3b() diff --git a/HW03/scripts/demo3c.py b/HW03/scripts/demo3c.py new file mode 100644 index 0000000..3a5c715 --- /dev/null +++ b/HW03/scripts/demo3c.py @@ -0,0 +1,57 @@ +# +# Demo 3c: Recursive normalized cuts (full version) +# +# author: Christos Choutouridis +# date: 06/07/2025 +# + +try: + from scipy.io import loadmat + import matplotlib.pyplot as plt + import numpy as np + #Project requirements + from image_to_graph import image_to_graph + from normalized_cuts import n_cuts_recursive +except ImportError as e: + print("Missing package:", e) + print("Run: pip install -r requirements.txt") + exit(1) + +def plot_recursive_clusters(image, labels, title, fname): + M, N, _ = image.shape + segmented = labels.reshape(M, N) + plt.imshow(segmented, cmap='tab20') # tab20 supports up to 20 unique colors + plt.title(title) + plt.axis('off') + plt.tight_layout() + plt.savefig(fname) + print(f"Saved: {fname}") + plt.close() + + +def run_demo3c(T1: float, T2: float): + data = loadmat("dip_hw_3.mat") + + for name in ["d2a", "d2b"]: + img = data[name] + print(f"\n=== Recursive n_cuts on {name} ===") + + affinity = image_to_graph(img) + labels = n_cuts_recursive(affinity, T1=T1, T2=T2) + + num_clusters = len(np.unique(labels)) + print(f" Clusters found: {num_clusters}") + print(f" Labels: {np.unique(labels)}") + + plot_recursive_clusters( + img, + labels, + title=f"{name} - recursive n_cuts (T1={T1}, T2={T2})", + fname=f"plots/demo3c_{name}_recursive_T1-{T1}_T2-{T2}.png" + ) + + +if __name__ == '__main__': + run_demo3c(5, 0.2) + run_demo3c(5, 0.95) + run_demo3c(5, 0.975) diff --git a/HW03/scripts/normalized_cuts.py b/HW03/scripts/normalized_cuts.py new file mode 100644 index 0000000..19ecdca --- /dev/null +++ b/HW03/scripts/normalized_cuts.py @@ -0,0 +1,209 @@ +# +# Normalized Cuts +# +# author: Christos Choutouridis +# date: 05/07/2025 +# + +try: + import numpy as np + from numpy.typing import NDArray + from sklearn.cluster import KMeans + from scipy.sparse.linalg import eigs + + # Testing requirements + from scipy.io import loadmat + from image_to_graph import image_to_graph + import matplotlib.pyplot +except ImportError as e: + print("Missing package:", e) + exit(1) + + +def n_cuts( + affinity_mat: NDArray[np.floating], + k: int +) -> NDArray[np.int32]: + """ + Non-recursive normalized cuts implementation using spectral embedding. + + Parameters: + ----------- + affinity_mat : np.ndarray of shape (n, n), dtype=float + Symmetric affinity matrix representing the graph. + k : int + Number of clusters. + + Returns: + -------- + cluster_idx : np.ndarray of shape (n,), dtype=int + Cluster label for each node. + """ + # Degree matrix + D = np.diag(affinity_mat.sum(axis=1)) + + # Unnormalized Laplacian + L = D - affinity_mat + + # Solve the generalized eigenvalue problem: Lx = λDx + eigvals, eigvecs = eigs(A=L, M=D, k=k, which='SR') # SR = Smallest Real part + + # Each row of U is a node's representation in spectral space + # Convert complex -> real (imaginary parts should be negligible) + U = np.real(eigvecs) + + # Each row is a vector to be clustered + # random_state parameter to 1, to ensure reproducibility across experiments. + kmeans = KMeans(n_clusters=k, random_state=1) + kmeans.fit(U) + + return kmeans.labels_.astype(np.int32) + + +def calculate_n_cut_value( + affinity_mat: NDArray[np.floating], + cluster_idx: NDArray[np.int32] +) -> float: + """ + Calculates the Ncut(A, B) metric for a binary clustering. + + Parameters: + ----------- + affinity_mat : np.ndarray of shape (n, n) + Symmetric affinity matrix. + cluster_idx : np.ndarray of shape (n,) + Cluster labels with values in {0, 1}. + + Returns: + -------- + n_cut_value : float + The value of the Ncut metric. + """ + A = np.where(cluster_idx == 0)[0] + B = np.where(cluster_idx == 1)[0] + + assoc_AA = np.sum(affinity_mat[np.ix_(A, A)]) + assoc_BB = np.sum(affinity_mat[np.ix_(B, B)]) + assoc_AV = np.sum(affinity_mat[A, :]) + assoc_BV = np.sum(affinity_mat[B, :]) + + nassoc_AB = (assoc_AA / assoc_AV) + (assoc_BB / assoc_BV) + ncut = 2 - nassoc_AB + + return ncut + + +def n_cuts_recursive( + affinity_mat: NDArray[np.floating], + T1: int, + T2: float +) -> NDArray[np.int32]: + """ + Recursive normalized cuts clustering. + + Parameters: + ----------- + affinity_mat : np.ndarray of shape (n, n) + Symmetric affinity matrix. + T1 : int + Minimum size for splitting a group. + T2 : float + Maximum acceptable Ncut value to allow further splitting. + + Returns: + -------- + cluster_idx : np.ndarray of shape (n,), dtype=int + Final cluster labels after recursive partitioning. + """ + + n = affinity_mat.shape[0] + cluster_idx = np.zeros(n, dtype=np.int32) + next_label = [1] # Mutable counter to assign new cluster labels + + def recursive_partition(indices: NDArray[np.int32], current_label: int): + if len(indices) <= T1: + cluster_idx[indices] = current_label + return + + sub_affinity = affinity_mat[np.ix_(indices, indices)] + labels = n_cuts(sub_affinity, k=2) + + ncut_value = calculate_n_cut_value(sub_affinity, labels) + A = indices[labels == 0] + B = indices[labels == 1] + + if len(A) <= T1 or len(B) <= T1 or ncut_value > T2: + cluster_idx[indices] = current_label + return + + # Assign recursively new labels + recursive_partition(A, current_label) + recursive_partition(B, next_label[0]) + next_label[0] += 1 + + # Start with all indices + recursive_partition(np.arange(n), 0) + return cluster_idx + + + +def _test_n_cuts(k: int, plot: bool = False): + data = loadmat("dip_hw_3.mat") + img = data["d2a"] + M, N, _ = img.shape + + print("Running non-recursive n_cuts on d2a...") + + A = image_to_graph(img) + labels = n_cuts(A, k=k) + + print("Unique cluster labels:", np.unique(labels)) + + # Visualize + if plot: + clustered = labels.reshape(M, N) + matplotlib.use("TkAgg") + matplotlib.pyplot.imshow(clustered, cmap='tab10', vmin=0, vmax=2) + matplotlib.pyplot.title("n_cuts clustering (k={k}) on d2a") + matplotlib.pyplot.axis('off') + matplotlib.pyplot.tight_layout() + matplotlib.pyplot.show() + #matplotlib.pyplot.savefig("ncuts_d2a_k3.png") + #print("Saved result to: ncuts_d2a_k3.png") + + +def _test_n_cuts_req(plot: bool = False): + data = loadmat("dip_hw_3.mat") + img = data["d2b"] + M, N, _ = img.shape + + print("Running recursive n_cuts on d2b...") + + affinity = image_to_graph(img) + + # Thresholds from the demo instructions + T1 = 5 + T2 = 0.95 + + labels = n_cuts_recursive(affinity, T1=T1, T2=T2) + + print("Number of unique clusters:", len(np.unique(labels))) + print("Labels:", np.unique(labels)) + + if plot: + segmented = labels.reshape(M, N) + matplotlib.pyplot.imshow(segmented, cmap='tab20') + matplotlib.pyplot.title(f"Recursive n_cuts on d2b (T1={T1}, T2={T2})") + matplotlib.pyplot.axis('off') + matplotlib.pyplot.tight_layout() + matplotlib.pyplot.show() + #matplotlib.pyplot.savefig("ncuts_recursive_d2b.png") + #print("Saved result to: ncuts_recursive_d2b.png") + +if __name__ == '__main__': + for k in [2, 3, 4]: + _test_n_cuts(k, False) + + _test_n_cuts_req(False) + + diff --git a/HW03/scripts/plots.zip b/HW03/scripts/plots.zip new file mode 100644 index 0000000..7218237 Binary files /dev/null and b/HW03/scripts/plots.zip differ diff --git a/HW03/scripts/plots/demo3a_d2a_ncuts_k2.png b/HW03/scripts/plots/demo3a_d2a_ncuts_k2.png new file mode 100644 index 0000000..47d1571 Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2a_ncuts_k2.png differ diff --git a/HW03/scripts/plots/demo3a_d2a_ncuts_k3.png b/HW03/scripts/plots/demo3a_d2a_ncuts_k3.png new file mode 100644 index 0000000..e2c6404 Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2a_ncuts_k3.png differ diff --git a/HW03/scripts/plots/demo3a_d2a_ncuts_k4.png b/HW03/scripts/plots/demo3a_d2a_ncuts_k4.png new file mode 100644 index 0000000..8300275 Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2a_ncuts_k4.png differ diff --git a/HW03/scripts/plots/demo3a_d2a_spectral_k2.png b/HW03/scripts/plots/demo3a_d2a_spectral_k2.png new file mode 100644 index 0000000..f2ee062 Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2a_spectral_k2.png differ diff --git a/HW03/scripts/plots/demo3a_d2a_spectral_k2_norm.png b/HW03/scripts/plots/demo3a_d2a_spectral_k2_norm.png new file mode 100644 index 0000000..5b9fa66 Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2a_spectral_k2_norm.png differ diff --git a/HW03/scripts/plots/demo3a_d2a_spectral_k3.png b/HW03/scripts/plots/demo3a_d2a_spectral_k3.png new file mode 100644 index 0000000..fb4bf79 Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2a_spectral_k3.png differ diff --git a/HW03/scripts/plots/demo3a_d2a_spectral_k3_norm.png b/HW03/scripts/plots/demo3a_d2a_spectral_k3_norm.png new file mode 100644 index 0000000..6b88ba7 Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2a_spectral_k3_norm.png differ diff --git a/HW03/scripts/plots/demo3a_d2a_spectral_k4.png b/HW03/scripts/plots/demo3a_d2a_spectral_k4.png new file mode 100644 index 0000000..830f262 Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2a_spectral_k4.png differ diff --git a/HW03/scripts/plots/demo3a_d2a_spectral_k4_norm.png b/HW03/scripts/plots/demo3a_d2a_spectral_k4_norm.png new file mode 100644 index 0000000..e53e4b8 Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2a_spectral_k4_norm.png differ diff --git a/HW03/scripts/plots/demo3a_d2b_ncuts_k2.png b/HW03/scripts/plots/demo3a_d2b_ncuts_k2.png new file mode 100644 index 0000000..7e94540 Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2b_ncuts_k2.png differ diff --git a/HW03/scripts/plots/demo3a_d2b_ncuts_k3.png b/HW03/scripts/plots/demo3a_d2b_ncuts_k3.png new file mode 100644 index 0000000..f873138 Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2b_ncuts_k3.png differ diff --git a/HW03/scripts/plots/demo3a_d2b_ncuts_k4.png b/HW03/scripts/plots/demo3a_d2b_ncuts_k4.png new file mode 100644 index 0000000..bad0d45 Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2b_ncuts_k4.png differ diff --git a/HW03/scripts/plots/demo3a_d2b_spectral_k2.png b/HW03/scripts/plots/demo3a_d2b_spectral_k2.png new file mode 100644 index 0000000..5439667 Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2b_spectral_k2.png differ diff --git a/HW03/scripts/plots/demo3a_d2b_spectral_k2_norm.png b/HW03/scripts/plots/demo3a_d2b_spectral_k2_norm.png new file mode 100644 index 0000000..e3d3c48 Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2b_spectral_k2_norm.png differ diff --git a/HW03/scripts/plots/demo3a_d2b_spectral_k3.png b/HW03/scripts/plots/demo3a_d2b_spectral_k3.png new file mode 100644 index 0000000..065561b Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2b_spectral_k3.png differ diff --git a/HW03/scripts/plots/demo3a_d2b_spectral_k3_norm.png b/HW03/scripts/plots/demo3a_d2b_spectral_k3_norm.png new file mode 100644 index 0000000..8bcb055 Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2b_spectral_k3_norm.png differ diff --git a/HW03/scripts/plots/demo3a_d2b_spectral_k4.png b/HW03/scripts/plots/demo3a_d2b_spectral_k4.png new file mode 100644 index 0000000..20fd0ab Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2b_spectral_k4.png differ diff --git a/HW03/scripts/plots/demo3a_d2b_spectral_k4_norm.png b/HW03/scripts/plots/demo3a_d2b_spectral_k4_norm.png new file mode 100644 index 0000000..8e24067 Binary files /dev/null and b/HW03/scripts/plots/demo3a_d2b_spectral_k4_norm.png differ diff --git a/HW03/scripts/plots/demo3b_d2a_ncut.png b/HW03/scripts/plots/demo3b_d2a_ncut.png new file mode 100644 index 0000000..c5a67f5 Binary files /dev/null and b/HW03/scripts/plots/demo3b_d2a_ncut.png differ diff --git a/HW03/scripts/plots/demo3b_d2b_ncut.png b/HW03/scripts/plots/demo3b_d2b_ncut.png new file mode 100644 index 0000000..b78a61d Binary files /dev/null and b/HW03/scripts/plots/demo3b_d2b_ncut.png differ diff --git a/HW03/scripts/plots/demo3c_d2a_recursive_T1-5_T2-0.2.png b/HW03/scripts/plots/demo3c_d2a_recursive_T1-5_T2-0.2.png new file mode 100644 index 0000000..ad6dd51 Binary files /dev/null and b/HW03/scripts/plots/demo3c_d2a_recursive_T1-5_T2-0.2.png differ diff --git a/HW03/scripts/plots/demo3c_d2a_recursive_T1-5_T2-0.95.png b/HW03/scripts/plots/demo3c_d2a_recursive_T1-5_T2-0.95.png new file mode 100644 index 0000000..6ecfd51 Binary files /dev/null and b/HW03/scripts/plots/demo3c_d2a_recursive_T1-5_T2-0.95.png differ diff --git a/HW03/scripts/plots/demo3c_d2a_recursive_T1-5_T2-0.975.png b/HW03/scripts/plots/demo3c_d2a_recursive_T1-5_T2-0.975.png new file mode 100644 index 0000000..6b890e1 Binary files /dev/null and b/HW03/scripts/plots/demo3c_d2a_recursive_T1-5_T2-0.975.png differ diff --git a/HW03/scripts/plots/demo3c_d2b_recursive_T1-5_T2-0.2.png b/HW03/scripts/plots/demo3c_d2b_recursive_T1-5_T2-0.2.png new file mode 100644 index 0000000..41391f0 Binary files /dev/null and b/HW03/scripts/plots/demo3c_d2b_recursive_T1-5_T2-0.2.png differ diff --git a/HW03/scripts/plots/demo3c_d2b_recursive_T1-5_T2-0.95.png b/HW03/scripts/plots/demo3c_d2b_recursive_T1-5_T2-0.95.png new file mode 100644 index 0000000..4280e78 Binary files /dev/null and b/HW03/scripts/plots/demo3c_d2b_recursive_T1-5_T2-0.95.png differ diff --git a/HW03/scripts/plots/demo3c_d2b_recursive_T1-5_T2-0.975.png b/HW03/scripts/plots/demo3c_d2b_recursive_T1-5_T2-0.975.png new file mode 100644 index 0000000..544dd18 Binary files /dev/null and b/HW03/scripts/plots/demo3c_d2b_recursive_T1-5_T2-0.975.png differ