I have been struggling to remove wave like patterns from my image. I tried FFT (Fourier Transform) and it wasn't good. I came across with inpainting and it looked promising but I don't know how to use it.

Can someone point me to the right direction or assist me how to remove those patterns? It'll be helpful. Image I'm working with


3 Answers 3


If it is just «amplitude modulation» can you identify the vertical periodic pattern and multiply with the inverse? Doing inpainting seems excessive as there seems to be real detail in the dark wave halves?

Edit: Here is my take on your problem, sketched in MATLAB. Basically: find the horizontal means of each row. Model the true image vertical means as a low-order polynoma, so any variation on top of this is an (unwanted) amplitude modulation, sort of a high-pass error to be corrected.

im = imread('65TwShqB.png');
%% all channels are equal. Do a crude BW conversion
im_bw = mean(im, 3);
%% threshold to NaN to avoid background pollution our estimate
im_bw(im_bw >= 50 & im_bw < 60) = NaN;
%% find the average vertical modulation
vert_wave_pattern = mean(im_bw, 2, "omitnan");
%% fit a low-order polynoma to be able to cancel large-scale (true) image variation.
p = polyfit(1:length(vert_wave_pattern), vert_wave_pattern, 2);
%% Find the multiplicative factor that makes the averages fit with the polynomial fit
corr = polyval(p, 1:length(vert_wave_pattern))' ./ vert_wave_pattern;
%% multiply each image row accordingly
im_bw_corrected = im_bw .* corr;
%% bring back the (unprocessed) background pixels
im_bw_corrected(isnan(im_bw_corrected)) = 52;

steps for vertical correction factor processed output

It seems to suppress much of the fluctuations. There are some drawbacks at the top and bottom of the image, I am guessing that some fiddling with what pixels gets to contribute to the correction vector would improve things further (make sure to exclude the black corners for a start - either using more level masking to NaN, or limiting the spatial range of pixels that goes into the average).

  • $\begingroup$ I agree with you! also inpainting is an excessive method. Honestly, I tried other methods and the results weren't good that's why, I resorted to inpainting. Do you know how I can use amplitude modulation to get rid of those patterns? $\endgroup$
    – Ammar
    Commented May 12 at 9:28
  • $\begingroup$ I'm unsure whether it adds more information but I received this image after resampled and registering multiple tiles. Maybe this moire pattern is being caused due to lack of flat field correction on those individual tile images. Do you have any thoughts? $\endgroup$
    – Ammar
    Commented May 12 at 14:34
  • $\begingroup$ I've added my answer with Python code, let me know your thought. $\endgroup$
    – Ammar
    Commented May 12 at 18:22

As Knut says, as a first cut I'd just use simple filtering.

Looking at the frequency domain of your original image:

Frequency domain of original image

it's a bit hard to see, but there are two peaks just above and below the central DC peak.

So, using the code stolen from here:

import cv2
import numpy as np
import matplotlib.pyplot as plt

def notch_reject_filter(shape, d0=9, u_k=0, v_k=0):
    P, Q = shape
    # Initialize filter with zeros
    H = np.zeros((P, Q))

    # Traverse through filter
    for u in range(0, P):
        for v in range(0, Q):
            # Get euclidean distance from point D(u,v) to the center
            D_uv = np.sqrt((u - P / 2 + u_k) ** 2 + (v - Q / 2 + v_k) ** 2)
            D_muv = np.sqrt((u - P / 2 - u_k) ** 2 + (v - Q / 2 - v_k) ** 2)

            if D_uv <= d0 or D_muv <= d0:
                H[u, v] = 0.0
                H[u, v] = 1.0

    return H

This creates a notch reject response in the 2D frequency domain:

Notch filter response

where the white is at 1 and the black is at 0.

Applying this filter:

img = cv2.imread('image.png', 0)

f = np.fft.fft2(img)
fshift = np.fft.fftshift(f)
phase_spectrumR = np.angle(fshift)
magnitude_spectrum = 20*np.log(np.abs(fshift))

img_shape = img.shape

NotchFilter = notch_reject_filter(img_shape, 4, 10, 0)

NotchRejectCenter = fshift * NotchFilter 
NotchReject = np.fft.ifftshift(NotchRejectCenter)
# Compute the inverse DFT of the result
inverse_NotchReject = np.fft.ifft2(NotchReject)  

Result = np.abs(inverse_NotchReject)

plt.imshow(Result, cmap='gray')

cv2.imwrite('image_filtered.png', Result)
cv2.imwrite('notch_filter.png', NotchFilter*255)
cv2.imwrite('frequency_domain.png', magnitude_spectrum)

yields something that seems to be significantly better (though not perfect):

Filtered picture.

  • $\begingroup$ thank you the code and help. But, after a careful study, I discovered I was working with moire effect. these patterns resulted for the different refresh rate/lens stuff. You approach looks good but like @knut Inge says there are real details in those patterns. That's why, the inpainting method I used resulted in bad outcomes. which I couldn't use. $\endgroup$
    – Ammar
    Commented May 12 at 9:23

expanding on knut's idea and advise, I have written a script that achieves what knut did in Python also trying to fix the black squares in the corners of the image.

import numpy as np
import matplotlib.pyplot as plt
import cv2

im = cv2.imread('TawsifExampleDataApr2024\SubsetSmallTiffFiles\img_t1_z1_c1_small_800.tiff', cv2.IMREAD_GRAYSCALE)

im_bw = im.astype(float)
im_bw[(im_bw >= 50) & (im_bw < 60)] = np.nan

vert_wave_pattern = np.nanmean(im_bw, axis=1)

p = np.polyfit(np.arange(len(vert_wave_pattern)), vert_wave_pattern, 2)

corr = np.polyval(p, np.arange(len(vert_wave_pattern))) / vert_wave_pattern

excluded_regions = [
    (0, 0, int(im.shape[0] * 0.1), int(im.shape[1] * 0.1)),
    (int(im.shape[0] * 0.9), 0, im.shape[0], int(im.shape[1] * 0.1)),
    (int(im.shape[0] * 0.9), int(im.shape[1] * 0.9), im.shape[0], im.shape[1])

mask = np.ones_like(im_bw)
for region in excluded_regions:
    mask[region[0]:region[2], region[1]:region[3]] = 0

im_bw_corrected = im_bw * corr[:, np.newaxis]
im_bw_corrected[mask == 0] = np.nan
im_bw_corrected[np.isnan(im_bw_corrected)] = 52

clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
im_bw_corrected_clahe = clahe.apply(im_bw_corrected.astype(np.uint8))

plt.imshow(im_bw_corrected_clahe, cmap='gray')

Example image

I personally think since this image was achieved through resampling multiple different images, the moire effect is probably happening due to lack of flat fielding. and Vignetting effect.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.