-1

I once again come to you with a question regarding filters, in this case: Bilateral Filters. I do understand the general concept of it and I did read several sources on the topic (including some questions on stackoverflow). Most sources use this equation to describe the process (I think originally it is from this source, but at this point I am not sure):
Bilateral Filtering
With the "general" gaussian filter (that only consideres the space) being represented by this Gaussian Filtering

The Gaussian Filter I did already implement successfully. It is only for gray-scale images and so will the bilateral filter (hopefully) be. It's in no way having a good performance, I am aware, but it does the job. In general I'd think I just need to add the multiplication with G(Ip - Iq) to make it work as a bilateral filter. My issue now is: what exactly does (Ip - Iq) represents? Or asked differently, where do I retrieve those values?
I'm not sure if my code is needed but here's my implementation anyways. The kernel-algorithm I took from this stackoverflow discussion.

def gaussfiltering (img = [], kernelsize=3, sigma=1.9):
    rows = len(img)
    columns = len(img[0])
    kernel = gaussian_kernel(kernelsize, sigma)
    horizontallyfiltered = iterate_horizontally(kernel, img, rows, columns, len(kernel))
    filtered = iterate_vertically(kernel, horizontallyfiltered, rows, columns, len(kernel))
    return filtered

def gaussian_kernel(kernel_size=3, sigma=1.9):
    x = numpy.linspace(-sigma, sigma, kernel_size+1)
    y = st.norm.cdf(x) 
    kernel = numpy.diff(y)
    return((kernel/kernel.sum()))

def iterate_horizontally (kernel=[], img=[], rows=0, columns=0, kernelsize=0):
    horizontallyfiltered = img.copy()
    for r in range(rows):
        for c in range(columns):
            halved = int(kernelsize/2)
            if ((c - halved) >= 0 and (c + halved) < columns): #is in range for filter
                indexOfImage = c - halved
                summed = 0.0
                for i in range(kernelsize):
                    summed += img[r][indexOfImage] * kernel[i]
                    indexOfImage+=1
                horizontallyfiltered[r][c]=summed
    return horizontallyfiltered

def iterate_vertically (kernel=[], img=[], rows=0, columns=0, kernelsize=0):
    verticallyfiltered = img.copy()
    for r in range(rows):
        for c in range(columns):
            halved = int(kernelsize/2)
            if ((r - halved) >= 0 and (r + halved) < rows): #is in range for filter
                indexOfImage = r - halved
                summed = 0.0
                for i in range(kernelsize):
                    summed += img[indexOfImage][c] * kernel[i]
                    indexOfImage+=1
                verticallyfiltered[r][c]=summed
    return verticallyfiltered

EDIT:
It was recommended to run the whole Kernel through the image at once for this, so I rewrote that function to do so (and on its own it still does a fine gaussian filtering). I additionally added a function to calculate the gaussian (without mu since mu=0) of a given X so I could multiply my results with the gaussian of Ip-Iq. However, this causes the entire image to turn incredibly dark - which makes sense the gaussian function returns a value around 0 for pixels that are far away now and thus many of the pixels are dark. So I clearly still got something wrong here and was hoping one of you could point me to it. The gaussian function I tried to replicate: enter image description here

and the new code:

def iterate_entirely (kernel=[], img=[], rows=0, columns=0, kernelsize=0, sigma=1.9):
    filtered = img.copy()
    for r in range(rows):
        for c in range(columns):
            halved = int(kernelsize/2)
            if ((c - halved) >= 0 and (c + halved) < columns and (r - halved) >= 0 and (r + halved) < rows): #is in range for filter
                indexOfImageRows = r - halved
                summed = 0.0
                for kr in range(kernelsize):
                    indexOfImageColumns = c - halved
                    for kc in range (kernelsize):
                        ipq = int(img[r][c]) - int(img[indexOfImageRows][indexOfImageColumns])
                        summed += img[indexOfImageRows][indexOfImageColumns] * kernel[kr][kc] * gaussian(ipq, sigma)
                        indexOfImageColumns+=1
                    indexOfImageRows+=1
                filtered[r][c]=summed
    return filtered

def gaussian(x, sigma):
    return 1.0/(numpy.sqrt(2.0*numpy.pi)*sigma)*numpy.exp(-numpy.power((x)/sigma, 2.0)/2)

def gaussian_kernel(kernel_size=3, sigma=1.9):
    x = numpy.linspace(-sigma, sigma, kernel_size+1)
    y = st.norm.cdf(x)
    kernel = numpy.diff(y)
    kernel2d = numpy.outer(kernel, kernel)
    return (kernel2d/kernel2d.sum())

EDIT 2:
more recommended changes, new code:

def iterate_entirely (kernel=[], img=[], rows=0, columns=0, kernelsize=0, sigma=1.9):
    filtered = img.copy()
    for r in range(rows):
        for c in range(columns):
            halved = int(kernelsize/2)
            if ((c - halved) >= 0 and (c + halved) < columns and (r - halved) >= 0 and (r + halved) < rows): #is in range for filter
                indexOfImageRows = r - halved
                summed = 0.0
                norm = 0.0
                for kr in range(kernelsize):
                    indexOfImageColumns = c - halved
                    for kc in range (kernelsize):
                        ipq = int(img[r][c]) - int(img[indexOfImageRows][indexOfImageColumns])
                        multiplicator = kernel[kr][kc] * gaussian(ipq, sigma)
                        summed += img[indexOfImageRows][indexOfImageColumns] * multiplicator
                        norm += multiplicator
                        indexOfImageColumns+=1
                    indexOfImageRows+=1
                print (" summed: ", summed, " norm: ", norm, " normed : ", summed/norm)
                filtered[r][c]=summed/norm
    return filtered

def gaussian(x, sigma): 
    return numpy.exp(-numpy.power((x)/sigma, 2.0)/2)
  • Consider asking on https://dsp.stackexchange.com – Mad Physicist Aug 19 '21 at 19:39
  • "It's in no way having a good performance": what do you mean ? This filter is quite effective at edge-preserving noise reduction. If you mean in terms of processing time, compare to the non-local means filter ! –  Aug 20 '21 at 07:43
  • Interestingly, you did implement the Gaussian filter, where `Iq` appears, but could not figure out `Ip-Iq`. –  Aug 20 '21 at 07:54
  • @YvesDaoust Thanks for the answers/ comments! I reacted to the clarification in your actual answer so I won't do that here again :D As for the performance: Yes, I meant from a time-perspective as I feel like iterating twice over each pixel (using two for-loops each run) feels kind of ineffective. I will look into the non-local means though! – AnnemarieWittig Aug 20 '21 at 16:27
  • @CrisLuengo As for that, I am of course aware I don't just mutliplicate (Ip-Iq). I simply forgot to add the G, sorry. – AnnemarieWittig Aug 20 '21 at 16:29
  • You're missing the normalization. Get the sum of `kernel[kr][kc] * gaussian(ipq, sigma)` over your kernel, say in variable `norm`, then write out `filtered[r][c]=summed/norm`. This will ensure you're still computing a weighted average, and therefore do not change the average intensity of your image. If you do this, you can leave out the normalization in the Gaussian functions, which should save some computational cost. – Cris Luengo Aug 20 '21 at 19:17
  • Also, instead of `for c in range(columns):` and then testing if `c` is in range, loop over the valid range: `for c in range(halved, columns-halved):`. Should again save a lot of time. Still, this is a slow filter, and an implementation in Python is adding a 100x overhead... I would recommend you try Numba to speed it up. – Cris Luengo Aug 20 '21 at 19:18
  • @CrisLuengo I tried the change you recommended, and I am sorry to bother again, but this just returns the same image as inserted (I added another Edit to the question). As for the time saving, I will definetely add that as I am thankful for any speeding up here, sadly I was tasked to implement this in python only. – AnnemarieWittig Aug 20 '21 at 19:56
  • Make sure you separate the sigma of the spatial Gaussian (`kernel`) and of the tonal Gaussian (`gaussian(ipq, sigma)`). Your default `sigma` for the tonal Gaussian is 1.9, which is very small if pixel intensities are in the range 0-255. This sigma controls what intensities you consider similar. With a very small sigma, none of the neighboring pixels will be similar, and so your output will depend almost for 100% on the one pixel in the middle of the kernel. Try 30 to start with. – Cris Luengo Aug 20 '21 at 20:12
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/236247/discussion-between-annemariewittig-and-cris-luengo). – AnnemarieWittig Aug 20 '21 at 20:23

1 Answers1

1

Ip and Iq are simply the gray-levels at p and q. The filtered value at p is mostly influenced by the pixels spatially close to it (like in a Gaussian), but also with similar gray-levels (close in radiometric space). The rationale is that pixels with a very different gray-level are probably from another region and should not be used.

If you imagine the image as a landscape where height is the gray-level, the filter output is the convolution with a 3D gaussian, i.e. points inside a sphere* centered at the target pixel get more weight than those outside.

[*You can scale the gray-levels so that the decay is isotropic in XYI space.]

  • thank you for your answer and comments! So if I understood correctly, I'll need to multiplicate my result for the pixel (for each horizontal and vertical run) with the result of running (Ip-Iq) through a Gaussian Function? From my understanding I'll need to compute that value for each Pixel the Kernel looks at on the run and can't actually adjust the Kernel I have for it, do I? – AnnemarieWittig Aug 20 '21 at 16:25
  • @AnnemarieWittig A partial answer to this question: you cannot use separate horizontal and vertical passes, you have to do the full kernel in one go. And you must update the weights of the kernel for every pixel you visit. – Cris Luengo Aug 20 '21 at 16:42
  • @CrisLuengo I added an Edit to my question, since I updated the method to what I understood from the given input. However, I can't really make it work so I assume, I didn't understand your input properly. Could you once again point me to the issue I'm having? I am very thankful for what I picked up already (at least I generally understands the function now) – AnnemarieWittig Aug 20 '21 at 19:11
  • @AnnemarieWittig: I cannot say better than the formula. –  Aug 22 '21 at 20:54