diff --git a/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task1/5-7.py b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task1/5-7.py new file mode 100644 index 00000000..732e4959 --- /dev/null +++ b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task1/5-7.py @@ -0,0 +1,40 @@ +import cv2 +import numpy as np + +# Load and pre-process binary image +binary_image = cv2.imread("./output/kernel_size_17.jpg", cv2.IMREAD_GRAYSCALE) +binary_image = cv2.medianBlur(binary_image, 3) + +# Step 1.5: Extraction of Binary Regions of Interest / connected components +num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(binary_image, connectivity=8) + +# Initialize an empty mask for filtered regions +filtered_mask = np.zeros(binary_image.shape, dtype=np.uint8) + +total_globules = 0 + +# Task 1.6: Filtering of Fat Globules +for i in range(1, num_labels): + area = stats[i, cv2.CC_STAT_AREA] + + # Calculate compactness + perimeter = cv2.arcLength(cv2.findContours((labels == i).astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0][0], True) + compactness = (perimeter ** 2) / area if area > 0 else 0 + + if (300 < area) and (compactness < 27): + total_globules += 1 + filtered_mask[labels == i] = 255 + +cv2.imwrite("./output/filtered_fat_globules.jpg", filtered_mask) +print("Total globules: " + str(total_globules)) + +# Task 1.7: Calculation of the Fat Area +# Total area of the image in pixels (excluding the background) +total_image_area = binary_image.shape[0] * binary_image.shape[1] + +# Total fat area (in pixels) +fat_area = np.sum(filtered_mask == 255) + +# Calculate fat percentage +fat_percentage = (fat_area / total_image_area) * 100 +print(f"Fat Area Percentage: {fat_percentage:.2f}%") diff --git a/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task1/5_extraction_of_binary_region_of_interest.py b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task1/5_extraction_of_binary_region_of_interest.py deleted file mode 100644 index 8bbaaac8..00000000 --- a/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task1/5_extraction_of_binary_region_of_interest.py +++ /dev/null @@ -1,23 +0,0 @@ -# Task 1.5: Extraction of Binary Regions of Interest / Connected Components -import cv2 -import numpy as np - -# read in noise-reduced image -image = cv2.imread("./output/kernel_size_25.jpg", cv2.IMREAD_GRAYSCALE) - -# Find connected components -num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(image, connectivity=4) - -# Create an output image (color) to label components -output_img = np.zeros((image.shape[0], image.shape[1], 3), dtype=np.uint8) - -# Apply a single color (e.g., gray) to each component in the output image -for label in range(1, num_labels): # Skip background (label 0) - output_img[labels == label] = (200, 200, 200) # Light gray color for each component - -# Overlay red text labels at component centroids -for i in range(1, num_labels): # Skip background (label 0) - x, y = int(centroids[i][0]), int(centroids[i][1]) - cv2.putText(output_img, str(i), (x, y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) # Red color (BGR: (0, 0, 255)) - -cv2.imwrite("./output/region_of_interest.jpg", output_img) diff --git a/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task1/output/filtered_fat_globules.jpg b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task1/output/filtered_fat_globules.jpg new file mode 100644 index 00000000..8d58da9b Binary files /dev/null and b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task1/output/filtered_fat_globules.jpg differ diff --git a/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task1/output/filtered_fat_globules.png b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task1/output/filtered_fat_globules.png new file mode 100644 index 00000000..5d68c5ac Binary files /dev/null and b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task1/output/filtered_fat_globules.png differ diff --git a/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/jennifer.jpg b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/jennifer.jpg new file mode 100644 index 00000000..f5dbca6c Binary files /dev/null and b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/jennifer.jpg differ diff --git a/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/output/3_filtered_color_image.jpg b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/output/3_filtered_color_image.jpg new file mode 100644 index 00000000..9d0faf1f Binary files /dev/null and b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/output/3_filtered_color_image.jpg differ diff --git a/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/output/jennifer_freq.jpg b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/output/jennifer_freq.jpg new file mode 100644 index 00000000..bc981504 Binary files /dev/null and b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/output/jennifer_freq.jpg differ diff --git a/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/output/jennifer_spatial.jpg b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/output/jennifer_spatial.jpg new file mode 100644 index 00000000..fc52edff Binary files /dev/null and b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/output/jennifer_spatial.jpg differ diff --git a/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/task2.py b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/task2.py index 3c31e322..db946b39 100644 --- a/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/task2.py +++ b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/task2.py @@ -28,4 +28,42 @@ plt.imshow(magnitude_spectrum, cmap='gray') plt.axis('off') plt.savefig("./output/2_frequency_domain_low-pass_filter.jpg", bbox_inches='tight', pad_inches=0) -# Task 2.3: Frequency Domain Filtering +# Task 2.3: Frequency Domain Filtering for +channels = cv2.split(image) +filtered_channels = [] + +for channel in channels: + fft_channel = np.fft.fft2(channel) + + # shift the zero frequency component to the center + fft_channel_shifted = np.fft.fftshift(fft_channel) + + # create a Gaussian filter the same size as the channel + gaussian_kernel = cv2.getGaussianKernel(kernel_size[0], variance) + gaussian_kernel_2d = gaussian_kernel @ gaussian_kernel.T + + # pad the Gaussian filter to match the size of the image channel + gaussian_kernel_padded = np.pad(gaussian_kernel_2d, + ((0, fft_channel_shifted.shape[0] - gaussian_kernel_2d.shape[0]), + (0, fft_channel_shifted.shape[1] - gaussian_kernel_2d.shape[1])), + mode='constant', constant_values=0) + + # shift the padded filter in the frequency domain + fft_gaussian_padded_shifted = np.fft.fftshift(np.fft.fft2(gaussian_kernel_padded)) + + # apply the low-pass filter to the channel + low_pass_filtered = fft_channel_shifted * fft_gaussian_padded_shifted + + # perform the inverse FFT to get the filtered channel in the spatial domain + ifft_filtered = np.fft.ifft2(np.fft.ifftshift(low_pass_filtered)) + + # take the real part and normalize it + filtered_channel = np.real(ifft_filtered) + filtered_channel = np.clip(filtered_channel, 0, 255).astype(np.uint8) + + # append the filtered channel to the list + filtered_channels.append(filtered_channel) + +filtered_image_color = cv2.merge(filtered_channels) + +cv2.imwrite("./output/3_filtered_color_image.jpg", filtered_image_color) diff --git a/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/test.py b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/test.py new file mode 100644 index 00000000..880e07de --- /dev/null +++ b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/code/task2/test.py @@ -0,0 +1,53 @@ +import cv2 +import numpy as np +import matplotlib.pyplot as plt + +# Task 2.1: Spatial Domain +image = cv2.imread("jennifer.jpg") + +kernel_size = (15, 15) +variance = 20 + +smoothed_image = cv2.GaussianBlur(image, kernel_size, variance) + +cv2.imwrite("./output/jennifer_spatial.jpg", smoothed_image) + +# Task 2.3: Frequency Domain Filtering for +channels = cv2.split(image) +filtered_channels = [] + +for channel in channels: + fft_channel = np.fft.fft2(channel) + + # shift the zero frequency component to the center + fft_channel_shifted = np.fft.fftshift(fft_channel) + + # create a Gaussian filter the same size as the channel + gaussian_kernel = cv2.getGaussianKernel(kernel_size[0], variance) + gaussian_kernel_2d = gaussian_kernel @ gaussian_kernel.T + + # pad the Gaussian filter to match the size of the image channel + gaussian_kernel_padded = np.pad(gaussian_kernel_2d, + ((0, fft_channel_shifted.shape[0] - gaussian_kernel_2d.shape[0]), + (0, fft_channel_shifted.shape[1] - gaussian_kernel_2d.shape[1])), + mode='constant', constant_values=0) + + # shift the padded filter in the frequency domain + fft_gaussian_padded_shifted = np.fft.fftshift(np.fft.fft2(gaussian_kernel_padded)) + + # apply the low-pass filter to the channel + low_pass_filtered = fft_channel_shifted * fft_gaussian_padded_shifted + + # perform the inverse FFT to get the filtered channel in the spatial domain + ifft_filtered = np.fft.ifft2(np.fft.ifftshift(low_pass_filtered)) + + # take the real part and normalize it + filtered_channel = np.real(ifft_filtered) + filtered_channel = np.clip(filtered_channel, 0, 255).astype(np.uint8) + + # append the filtered channel to the list + filtered_channels.append(filtered_channel) + +filtered_image_color = cv2.merge(filtered_channels) + +cv2.imwrite("./output/jennifer_freq.jpg", filtered_image_color) diff --git a/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/latex/CT404-Assignment-2.pdf b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/latex/CT404-Assignment-2.pdf index 028030db..af9b503a 100644 Binary files a/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/latex/CT404-Assignment-2.pdf and b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/latex/CT404-Assignment-2.pdf differ diff --git a/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/latex/CT404-Assignment-2.tex b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/latex/CT404-Assignment-2.tex index 2b77e20c..e47eac01 100644 --- a/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/latex/CT404-Assignment-2.tex +++ b/year4/semester1/CT404: Graphics & Image Processing/assignments/assignment2/latex/CT404-Assignment-2.tex @@ -285,18 +285,50 @@ As can be seen from the above output, the optimal value chosen was 129. \captionof{figure}{\mintinline{python}{kernel_size = 31}} \end{minipage} -I chose to go with \mintinline{python}{kernel_size = 25} as it seemed to give the optimal balance between removing noise without significantly reducing the size of the remaining fat globules . +I chose to open the image with a structuring element that had \mintinline{python}{kernel_size = 17} as it seemed to give the optimal balance between removing noise without significantly reducing the size of the remaining fat globules. \begin{figure}[H] \centering - \includegraphics[width=0.5\textwidth]{../code/task1/output/kernel_size_25.jpg} - \caption{Chosen noise threshold: \mintinline{python}{kernel_size = 25}} + \includegraphics[width=0.5\textwidth]{../code/task1/output/kernel_size_17.jpg} + \caption{Chosen noise threshold: \mintinline{python}{kernel_size = 17}} \end{figure} \subsection{Extraction of Binary Regions of Interest / Connected Components} +\begin{code} +\inputminted[firstline=1, lastline=9, linenos, breaklines, frame=single]{python}{../code/task1/5-7.py} +\caption{Task 1.5 section of \mintinline{python}{5-7.py}} +\end{code} + +I'm not sure why, but no matter what level of noise removal I tried the connected components extraction with, the connected components always came out quite jagged. +To correct for this, I did some additional noise reduction by using a blur on the image to remove some of the white noise that was appearing in. +I also used a higher value of connectivity with \mintinline{python}{connectivity=8}, keeping components that were touching at all rather than components that just shared an edge. + +\subsection{Filtering of Fat Globules} +\begin{code} +\inputminted[firstline=11, lastline=29, linenos, breaklines, frame=single]{python}{../code/task1/5-7.py} +\caption{Task 1.6 section of \mintinline{python}{5-7.py}} +\end{code} + +I filtered the fat globules based off size \& compactness, using the compactness measure to remove globules that were not globule-shaped and the area measure to remove globules that were too small to be a globule. +I used a maximum compactness of 27 and a minimum area of 300 to filter the globules, resulting in a total of 35 fat globules. + +\begin{figure}[H] + \centering + \includegraphics[width=0.5\textwidth]{../code/task1/output/filtered_fat_globules.jpg} + \caption{Result of fat globule filtering} +\end{figure} + +\subsection{Calculation of the Fat Area} +\begin{code} +\inputminted[firstline=31, lastline=40, linenos, breaklines, frame=single]{python}{../code/task1/5-7.py} +\caption{Task 1.7 section of \mintinline{python}{5-7.py}} +\end{code} + +The percentage of the image covered by fat globules was 15.33\%. \newpage + \section{Filtering of Images in Spatial \& Frequency Domains} \begin{figure}[H] \centering @@ -318,7 +350,7 @@ After some experimentation, I chose parameter values of \mintinline{python}{kern \caption{Output of \mintinline{python}{1_spatial_domain.jpg}} \end{figure} -\subsection{Frequency Domain Filtering} +\subsection{Frequency Domain Low-Pass Filter} \begin{code} \inputminted[firstline=15, lastline=29, linenos, breaklines, frame=single]{python}{../code/task2/task2.py} \caption{Task 2.2 section of \mintinline{python}{task2.py}} @@ -330,4 +362,73 @@ After some experimentation, I chose parameter values of \mintinline{python}{kern \caption{Zero-centered low-pass filter of Gaussian Kernel} \end{figure} +\subsection{Frequency Domain Filtering} +\begin{code} +\inputminted[firstline=31, lastline=70, linenos, breaklines, frame=single]{python}{../code/task2/task2.py} +\caption{Task 2.3 section of \mintinline{python}{task2.py}} +\end{code} + +\begin{figure}[H] + \centering + \includegraphics[width=0.5\textwidth]{../code/task2/output/3_filtered_color_image.jpg} + \caption{Frequency domain filtered image} +\end{figure} + +The low pass filter type used was the same as in Section 2.2: a Gaussian filter with \mintinline{python}{(15,15)} and \mintinline{python}{2}. + +\subsection{Comparison} +\noindent +\begin{minipage}{0.49\textwidth} +\begin{figure}[H] + \centering + \includegraphics[width=\textwidth]{../code/task2/output/1_spatial_domain.jpg} + \caption{Spatial domain filtered image} +\end{figure} +\end{minipage} +\hfill +\begin{minipage}{0.49\textwidth} +\begin{figure}[H] + \centering + \includegraphics[width=\textwidth]{../code/task2/output/3_filtered_color_image.jpg} + \caption{Frequency domain filtered image} +\end{figure} +\end{minipage} + +The two images are very similar, having shared the same type of low pass filter. +However, the frequency domain filtered image has retained more colour range, and has an overall less blurred appearance. +The spatial domain filtering has applied a general blur across the entire image, making the filtering more obvious. +On the other hand, the frequency domain filtering is more subtle and reduces the visibility of wrinkles while retaining some definition in the eyes, lips, and stubble. +Overall, I would prefer the frequency domain filtering for this task; despite its increased complexity, it creates a more subtle and convincing effect that would be easily mistakable for an unfiltered image. + +\subsection{Unseen Image Testing} +\noindent +\begin{minipage}{0.33\textwidth} +\begin{figure}[H] + \centering + \includegraphics[width=\textwidth]{../code/task2/jennifer.jpg} + \caption{Original Image} +\end{figure} +\end{minipage} +\hfill +\begin{minipage}{0.33\textwidth} +\begin{figure}[H] + \centering + \includegraphics[width=\textwidth]{../code/task2/output/jennifer_spatial.jpg} + \caption{Spatial domain filtered image} +\end{figure} +\end{minipage} +\hfill +\begin{minipage}{0.33\textwidth} +\begin{figure}[H] + \centering + \includegraphics[width=\textwidth]{../code/task2/output/jennifer_freq.jpg} + \caption{Frequency domain filtered} +\end{figure} +\end{minipage} + +Again, the two filtered images are very similar. +In my opinion, the frequency domain filtered image performed better here again, as it has a more dynamic colour range and more subtle blurring. +The skin looks very artificially smoothed in the spatial domain filtered image, but more natural in the frequency domain filtered image. +The hair looks more blurred and out-of-focus in the spatial domain filtered image, but looks sharper and more natural in the frequency domain filtered image. + \end{document}