From 49de39883b60b6c9e7993d720af868f333bd621c Mon Sep 17 00:00:00 2001 From: servostar Date: Wed, 26 Feb 2025 11:26:59 +0100 Subject: [PATCH] Add median filter --- src/HistogramManipulation.py | 148 ++++++++++++++++++++++++++++++++++- src/ImageFiltering.py | 79 ++++++++++++++++++- src/controllers.py | 12 +-- src/models.py | 1 - 4 files changed, 231 insertions(+), 9 deletions(-) diff --git a/src/HistogramManipulation.py b/src/HistogramManipulation.py index b39544c..0b96209 100755 --- a/src/HistogramManipulation.py +++ b/src/HistogramManipulation.py @@ -1,12 +1,23 @@ import cv2 import numpy as np import Utilities +import math +MAX_LUM_VALUES = 256 # Task 1 # function to stretch an image def stretchHistogram(img): result = img.copy() + + grayscale = getLuminance(img) + hist = calculateHistogram(grayscale, MAX_LUM_VALUES) + minPos, maxPos = findMinMaxPos(hist) + + for x in range(0, img.shape[0]): + for y in range(0, img.shape[1]): + result[x][y] = (grayscale[x][y] - minPos) / (maxPos - minPos) * 255.0 + return result @@ -14,6 +25,25 @@ def stretchHistogram(img): # function to equalize an image def equalizeHistogram(img): result = img.copy() + + grayscale = getLuminance(img) + hist = calculateHistogram(grayscale, MAX_LUM_VALUES) + minPos, maxPos = findMinMaxPos(hist) + + # Precompute integral of histogram from left to right. + sum = 0 + integral = [] + for i in range(0, hist.shape[0]): + sum += hist[i] + integral.append(sum) + + for x in range(0, img.shape[0]): + for y in range(0, img.shape[1]): + # Compute histogram value of pixel. + bin = int(img[x][y][0] / 255.0 * (MAX_LUM_VALUES - 1)) + # Equalize pixel. + result[x][y] = (255.0 - 1.0) / (img.shape[0] * img.shape[1]) * integral[bin] + return result @@ -21,6 +51,18 @@ def equalizeHistogram(img): # function to apply a look-up table onto an image def applyLUT(img, LUT): result = img.copy() + + grayscale = getLuminance(img) + + for x in range(0, img.shape[0]): + for y in range(0, img.shape[1]): + # Compute histogram value of pixel. + bin = int(grayscale[x][y][0] / 255.0 * (MAX_LUM_VALUES - 1)) + + result[x][y][0] = LUT[bin] + result[x][y][1] = LUT[bin] + result[x][y][2] = LUT[bin] + return result @@ -29,6 +71,17 @@ def applyLUT(img, LUT): def findMinMaxPos(histogram): minPos = 0 maxPos = 255 + + for x in range(0, histogram.shape[0]): + if histogram[x] > 0: + minPos = x + break + + for x in range(histogram.shape[0] - 1, 0): + if histogram[x] > 0: + maxPos = x + break + return minPos, maxPos @@ -37,24 +90,117 @@ def findMinMaxPos(histogram): def calculateHistogram(img, nrBins): # create histogram vector histogram = np.zeros([nrBins], dtype=int) + + for x in range(0, img.shape[0]): + for y in range(0, img.shape[1]): + bin = int(img[x][y][0] / 255.0 * (nrBins - 1)) + histogram[bin] = histogram[bin] + 1 + return histogram -def apply_log(img): +def luminanceD65(rgb: np.ndarray) -> np.float64: + """ + Compute the luminance value of the specified linear RGB values + according to the D65 white point. + + @param rgb(np.ndarray): sRGB image + + @returns The luminance value + """ + return np.float64(rgb @ [0.2126, 0.7152, 0.0722]) + + +def getLuminance(img): result = img.copy() + + for x in range(0, img.shape[0]): + for y in range(0, img.shape[1]): + lum = luminanceD65(img[x][y]) + result[x][y][0] = lum + result[x][y][1] = lum + result[x][y][2] = lum + + return result + + +def apply_log(img): + grayscale = getLuminance(img) + result = img.copy() + + # Logarithmic scale factor. + LOG_SCALE_FACTOR = 2.0 + + for x in range(0, img.shape[0]): + for y in range(0, img.shape[1]): + + # Compute logarithmically scaled D65 luminace + # and clamp result to be smaller 255. + lum = min(255, + math.log(grayscale[x][y][0]/255.0 + 1) + * 255.0 * LOG_SCALE_FACTOR) + + result[x][y][0] = lum + result[x][y][1] = lum + result[x][y][2] = lum + return result def apply_exp(img): + grayscale = getLuminance(img) result = img.copy() + + # Logarithmic scale factor. + EXP_SCALE_FACTOR = 0.5 + + for x in range(0, img.shape[0]): + for y in range(0, img.shape[1]): + + # Compute logarithmically scaled D65 luminace + # and clamp result to be smaller 255. + lum = min(255, + (math.exp(grayscale[x][y][0]/255.0) - 1) + * 255.0 * EXP_SCALE_FACTOR) + + result[x][y][0] = lum + result[x][y][1] = lum + result[x][y][2] = lum + return result def apply_inverse(img): + grayscale = getLuminance(img) result = img.copy() + + for x in range(0, img.shape[0]): + for y in range(0, img.shape[1]): + + # Compute logarithmically scaled D65 luminace + # and clamp result to be smaller 255. + lum = 255.0 - grayscale[x][y][0] + + result[x][y][0] = lum + result[x][y][1] = lum + result[x][y][2] = lum + return result def apply_threshold(img, threshold): + grayscale = getLuminance(img) result = img.copy() + + for x in range(0, img.shape[0]): + for y in range(0, img.shape[1]): + + lum = 0 + if grayscale[x][y][0] >= threshold: + lum = 255 + + result[x][y][0] = lum + result[x][y][1] = lum + result[x][y][2] = lum + return result diff --git a/src/ImageFiltering.py b/src/ImageFiltering.py index 0080547..ff051ee 100644 --- a/src/ImageFiltering.py +++ b/src/ImageFiltering.py @@ -2,12 +2,43 @@ import numpy as np import matplotlib.pyplot as plt import datetime as dt import cv2 +from numpy._core.numeric import ndarray import Utilities +import math +def _getChannelMedian(values: list[list[int]], channel: int) -> int: + channelValues = list(map(lambda rgb: rgb[channel], values)) + channelValues.sort() + return channelValues[int(len(channelValues)/2)] # apply median filter -def applyMedianFilter(img, kSize): +def applyMedianFilter(img: ndarray, kSize: int): filtered_img = img.copy() + + for x in range(0, img.shape[0]): + for y in range(0, img.shape[1]): + + values = [] + + for u in range(int(-kSize/2), int(kSize/2)+1): + s = x + u + if s < 0 or s >= img.shape[0]: + continue + + for v in range(int(-kSize/2), int(kSize/2)+1): + t = y + v + if t < 0 or t >= img.shape[1]: + continue + + values.append(img[s][t]) + + if len(values) > 0: + filtered_img[x][y] = [ + _getChannelMedian(values, 0), + _getChannelMedian(values, 1), + _getChannelMedian(values, 2) + ] + return filtered_img @@ -25,6 +56,30 @@ def gaussian(x, y, sigmaX, sigmaY, meanX, meanY): # create a gaussian kernel of arbitrary size def createGaussianKernel(kSize, sigma=None): kernel = np.zeros((kSize, kSize)) + + stdev = math.floor(kSize/2) + stdev2 = stdev * stdev + factor = 1.0/(stdev2*2*math.pi) + + sum = 0.0 + + for x in range(kSize): + xm = x - kSize/2 + xsum = xm * xm / stdev2 + + for y in range(kSize): + ym = y - kSize/2 + ysum = ym * ym / stdev2 + + kernel[x][y] = math.exp((xsum + ysum) * -0.5) * factor + sum += kernel[x][y] + + # Normalize gaussian kernel in order not minimize power loss: + # https://stackoverflow.com/a/61355383 + for x in range(kSize): + for y in range(kSize): + kernel[x][y] /= sum + return kernel @@ -42,6 +97,28 @@ def createSobelYKernel(): def applyKernelInSpatialDomain(img, kernel): filtered_img = img.copy() + + width, height = kernel.shape + + for x in range(0, img.shape[0]): + for y in range(0, img.shape[1]): + + filtered_img[x][y] = np.zeros([3]) + + for u in range(0, width): + s = x + u - int(width/2) + + for v in range(0, height): + t = y + v - int(height/2) + + color = np.zeros([3]) + if t >= 0 and t < img.shape[1] and s >= 0 and s < img.shape[0]: + color = img[s][t] + + filtered_img[x][y][0] += kernel[u][v] * color[0] + filtered_img[x][y][1] += kernel[u][v] * color[1] + filtered_img[x][y][2] += kernel[u][v] * color[2] + return filtered_img diff --git a/src/controllers.py b/src/controllers.py index c95fe75..969817d 100755 --- a/src/controllers.py +++ b/src/controllers.py @@ -117,32 +117,32 @@ class MainController: def apply_gaussian_filter(self, kernel_size): kernel = IF.createGaussianKernel(kernel_size) img = IF.applyKernelInSpatialDomain(self._model.input_image, kernel) - self._model.image = Utilities.ensure_three_channel_grayscale_image(img) + self._model.image = img def apply_moving_avg_filter(self, kernel_size): kernel = IF.createMovingAverageKernel(kernel_size) img = IF.applyKernelInSpatialDomain(self._model.input_image, kernel) - self._model.image = Utilities.ensure_three_channel_grayscale_image(img) + self._model.image = img def apply_moving_avg_filter_integral(self, kernel_size): img = IF.applyMovingAverageFilterWithIntegralImage( self._model.input_image, kernel_size ) - self._model.image = Utilities.ensure_three_channel_grayscale_image(img) + self._model.image = img def apply_median_filter(self, kernel_size): img = IF.applyMedianFilter(self._model.input_image, kernel_size) - self._model.image = Utilities.ensure_three_channel_grayscale_image(img) + self._model.image = img def apply_filter_sobelX(self): kernel = IF.createSobelXKernel() img = IF.applyKernelInSpatialDomain(self._model.input_image, kernel) - self._model.image = Utilities.ensure_three_channel_grayscale_image(img) + self._model.image = img def apply_filter_sobelY(self): kernel = IF.createSobelYKernel() img = IF.applyKernelInSpatialDomain(self._model.input_image, kernel) - self._model.image = Utilities.ensure_three_channel_grayscale_image(img) + self._model.image = img def run_runtime_evaluation(self): IF.run_runtime_evaluation(self._model.input_image) diff --git a/src/models.py b/src/models.py index 3b30574..b75ae7c 100755 --- a/src/models.py +++ b/src/models.py @@ -57,5 +57,4 @@ class ImageModel(QObject): def load_rgb_image(self, path): image = cv2.imread(path, 1) - # image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) self.image = image