[CT404]: Finish Assignment 2
This commit is contained in:
@ -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}%")
|
@ -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)
|
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
After Width: | Height: | Size: 803 KiB |
Binary file not shown.
After Width: | Height: | Size: 799 KiB |
@ -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)
|
||||
|
@ -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)
|
Binary file not shown.
@ -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}
|
||||
|
Reference in New Issue
Block a user