4
$\begingroup$

I am currently in the midst of processing a satellite image, and I've come to a point where I need to remove those faint stripes caused by the sensor, as you can see below:

enter image description here

The image is RGB. I converted it to grayscale and performed FFT to visualise it in the frequency domain. These are the results:

enter image description here

What I've come to realize, mainly from here, is that this cross created represents the stripes I want to remove. I don't know how to remove them though, any help would be appreciated.

Claude.AI provided me with:

import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
from scipy.fftpack import fft2, ifft2, fftshift, ifftshift

# Load the image (replace with your image path)
image_path = 'input.tiff'
image = np.array(Image.open(image_path).convert('L'))

# Apply FFT to transform to frequency domain
f_transform = fft2(image)
f_shift = fftshift(f_transform)

# Create the notch filter
rows, cols = image.shape
crow, ccol = rows // 2, cols // 2  # center of the frequency domain

# Create a mask for the filter - start with all ones (no filtering)
mask = np.ones((rows, cols), dtype=np.float32)

# Define the width of the vertical line to filter out
line_width = 3  # Adjust this based on your image

# Create a vertical band filter that removes the vertical line in the FFT
# (which corresponds to horizontal stripes in the image)
mask[:, ccol-line_width:ccol+line_width+1] = 0
mask[crow-line_width:crow+line_width+1, :] = 0

# Apply the filter
f_shift_filtered = f_shift * mask

# Inverse FFT to get back to the spatial domain
f_ishift = ifftshift(f_shift_filtered)
img_back = np.abs(ifft2(f_ishift))

# Save the destriped image
Image.fromarray(img_back.astype(np.uint8)).save('destriped_satellite_image.tiff')

But it provides me with a result I don't get: enter image description here

$\endgroup$
15
  • 4
    $\begingroup$ I think your filter is too strong and/or too abrupt. What happens if you apply the filter gradually (full filter at center, gradually reducing as you go off-center) or if you don't filter out the center frequency completely, but only for example 80%? $\endgroup$ Commented Apr 22 at 15:03
  • 15
    $\begingroup$ note that we'll probably not fix your AI-generated code for you. You need to understand exactly what it does yourself (it's more sensible to ask for expert help building your own understanding than to ask humans to correct machine-generated code. Machines can generate questionable code at a rate much higher than humans can correct, and it takes them no human time :) ) $\endgroup$ Commented Apr 22 at 15:54
  • 9
    $\begingroup$ The diagonal artifacts in your image are showing up as the faint diagonal components in your FFT. I'm not sure where the vertical/horizonal cross in your FFT comes from. $\endgroup$
    – TimWescott
    Commented Apr 22 at 16:57
  • 2
    $\begingroup$ crosspost: stackoverflow.com/questions/79586103/… $\endgroup$ Commented Apr 22 at 17:08
  • 3
    $\begingroup$ @TimWescott the vertical/horizontal cross is probably from the extremely hard edges you get when considering the image as a repeating tile, which is what the FFT analysis does. By removing that, they've kinda implemented a tileable-blur. $\endgroup$
    – cloudfeet
    Commented Apr 23 at 10:35

5 Answers 5

10
$\begingroup$

I feel that the other answers address only part of the issue and leave out the elephant in the room. Like TimWescott points out in one of the comments, the faint diagonal stripes in the frequency domain are the ones responsible for the diagonal stripes in the original image, not the cross in the middle. In general, any line pattern in an image is translated to perpendicular line patterns in the frequency domain, and you'll see that those diagonal patterns are perpendicular (or at least close to perpendicular)

Also, I'm not sure where you got the idea that zeroing the cross in the middle would get rid of the noise, I don't think the link you provided advocates for that nuclear choice, but by a more localized intervention in a specific region of the FT.

It's been a long time since I've worked with 2D FFTs and I wish I could give you more guidance, but I feel that it was worth it to give more visibility to some of the comments in the form of an answer.

[EDIT 2025-04-26] After seeing some of the comments, I've noticed that an essential property of the 2D continuous FT (which is great to get an intuition over the results in the discrete FFT case) is not so well-known (and I'm surprised that I remember it at all!): lines in the spatial domain result in perpendicular lines in the frequency domain. I wrote that in my original answer, but I think this didn't get enough attention.

Let's see first that a single line results in one single perpendicular line in the frequency domain (notice my non-compromising claim). A line can be represented with a delta such as $ \delta(Ax + By + C) $. For the sake of simplicity, I'll assume that the line goes through the origin, i.e. $ C = 0 $, in the following derivations. It's not important, $ C \neq 0 $ results only in a phase shift. Let's calculate the 2D FT:

$$ \mathcal{F}\left\{ \delta(Ax + By) \right\} = \int_{-\infty}^\infty \int_{-\infty}^\infty \delta(Ax + By) \exp\left\{ -2 \pi i (ux + vy) \right\} \mathrm{d}x \mathrm{d}y = $$ $$ \int_{-\infty}^\infty \exp\left\{ -2 \pi i (u - v \frac{A}{B}) x \right\} \mathrm{d}x $$

We know from the relation between the 1D delta and its transform (a complex exponential), that the FT of a complex exponential must be a delta (why? Because the FT and the IFT are virtually identical operations). Specifically, $ \delta(u - \frac{A}{B} v) $. And the line represented by this delta is perpendicular to the line in the spatial domain. One way to see this is to take the normal vectors (A,B) and (1,-A/B) and calculate the scalar product: $ 1 \cdot A - \frac{A}{B} \cdot B = 0 $.

Now, some empirical evidence for this fact. Here's the FFT of a straight line:

FFT of a 2D line

Naturally, the FFT is not a perfect line for two reasons: the effect of discretization and the effect of the rectangular window.

$\endgroup$
5
  • 1
    $\begingroup$ I upvote because I'm in the same boat $\endgroup$
    – pm101
    Commented Apr 23 at 9:42
  • 2
    $\begingroup$ fair point! yeah, I think that's basically owed to the LLM generation of this code: I didn't bother to even read the code in detail, and compare it to what the requirements were – as a human, it's kind of hard to analyze code in depth that no human even bothered to write :) But I should have noticed that these stripes aren't DC components. 'doh! +1 for you. $\endgroup$ Commented Apr 23 at 13:59
  • $\begingroup$ @MarcusMüller Yeah, I understand. Personally I haven't looked (much) at the AI code, but I think the code was generated based on a bad premise (that the cross in the middle was responsible for the noise). I think AI code based on more sensible guidelines would give a less bad result. In other words, I don't think the code itself is the culprit, but the assumptions behind that code. $\endgroup$
    – Asher
    Commented Apr 24 at 10:03
  • 2
    $\begingroup$ On another note, I really like leftaroundabout's answer. $\endgroup$
    – Asher
    Commented Apr 24 at 10:04
  • 2
    $\begingroup$ @Asher possibly! But the problem is that the sole medium of transport here for the intent of what the user wanted the language model to write and what we see is the output of the language model. And that in itself is a bit of a problem: had we hand-written code, we could look at what it tries to do; it would have suggestive names, maybe even comments, stemming from the user's original intent, and not from an LLM's interpretation of what the user told it was their intent. Basically, LLMs are intent-obfuscating machines for engineering problems :) $\endgroup$ Commented Apr 24 at 10:06
6
$\begingroup$

I think you're basically seeing what you'd expect: since the DFT is circular on the image borders, removing the cross in the center is like multiplying the spatial frequency domain with a very box window that's the image dimensions - 1px on each side (just shifted so that the center of the box is at the corners of the image).

Now something you've probably heard before:

Multiplication in one domain is equivalent to convolution of the Fourier-transformed factors in the time domain

(or in your case, spatial domain)

That's why Why is it a bad idea to filter by zeroing out FFT bins? is such a popula question :)

You're convolving your image with a sinc shaped kernel, and having removed the center bin, you do that on an image where the average has been removed, which probably perceptually amplifies noise in the formerly dark regions of the image.

Instead, you probably want to design a more sensible filter that you convolve with :)

$\endgroup$
5
$\begingroup$

First, you should for spectral analysis purposes generally apply a window before taking the FFT. This avoids artifacts from the image edges from obscuring the actually interesting properties (in this case, the square cross which the AI tried to remove).

The feature we should actually get rid of are the tilted stripes in the frequency domain. But you should not brutally erase them from the spectrum, in particular not delete a whole line through the center – that removes also low-frequency components that are important part of the clean signal.
Instead, you should only reduce the amplitudes enough to make the spectrum isotropic.

One way to accomplish that is to transform the spectrum into polar coordinates and compute the median of each row (i.e. of each circle of direction-independent frequency). Broadcast this to the original shape and transform it back to cartesian to get your target spectrum.

Then you can compute the quotient of the target spectrum and the actually observed one, to figure out how the image needs to be tweaked. This you can then apply as a multiplier to the (complex) spectrum of the image without window function. iFFT back and the stripes should be mostly removed, without too much change to the signal.



A problem with the polar transform is that it's only defined in a disc-shaped region. It is probably ok to just zero the spectrum outside of the disc, but you can also pad the spectrum before going to polar coordinates (taking the median makes the processing somewhat stable to this).

Alternatively, do the entire processing with windowed chunks. This is the preferred strategy if the image is too big for a single FFT. Hann- or Tukey windows have the property that if you add together two overlapping neighbouring windowed chunks, the amplitudes add to unity. This avoids getting visible mismatches along the edges of the processed tiles.

$\endgroup$
1
  • 1
    $\begingroup$ +1, I really like the idea of manipulating the spectrum in polar coordinates! $\endgroup$
    – Asher
    Commented Apr 24 at 10:22
5
$\begingroup$

As @Asher mentioned, do not remove the cross in the middle, since it contains the image information you want to preserve. Instead, remove the lines perpendicular to your artifacts.

I drew over the diagonal stripes in black with GIMP and got this mask:

mask

I then filtered the image by multiplying its FFT with the mask.

import numpy as np
from PIL import Image

image = np.array(Image.open("ONkM3J18.png").convert("RGB")) / 255.0
mask = np.array(Image.open("mask.png").convert("L")) / 255.0

z = [np.fft.fftshift(np.fft.fft2(c)) for c in image.transpose(2, 0, 1)]

if 0:
    # Save FFT for painting over mask
    z = np.log(np.abs(np.dstack(z)) + 1)
    z = (z - z.min()) / (z.max() - z.min())
    Image.fromarray((z * 255).astype(np.uint8)).save("fft.png")
else:
    # Save FFT-filtered image
    image = np.dstack([np.fft.ifft2(np.fft.ifftshift(c * mask)) for c in z])
    image = np.clip(np.abs(image) * 255, 0, 255).astype(np.uint8)
    Image.fromarray(image).save("filtered.png")

The filtered image has much fewer stripe artifacts. For comparison, open both the original and the filtered image in a new browser tab and then flip back and forth between the two tabs to see the difference.

filtered image

New contributor
John Doe is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
$\endgroup$
3
$\begingroup$

You erased the DC component, which is the center of the cross.

That causes the picture to become "black", or rather, its mean will be 0, and about half the image content will be below 0, i.e. darker than black.

Do not erase it. Then it'll look right. Or better anyway.

$\endgroup$
3
  • $\begingroup$ This isn't an answer - it only says what not to do. There's not even a hint how to remove the diagonal lines. $\endgroup$
    – MSalters
    Commented Apr 24 at 14:06
  • 1
    $\begingroup$ the one answer before mine did not clearly point out the issue of the DC component and what to do about it, instead hid that point after some prose on the shifting/rolling of the spectrum. nor did it discuss how to suppress the diagonals (comment please?), only "design a more sensible filter". my answer is an answer, to the problem OP asked about, which was the picture turning almost black. do you deny that I explained this? what do you intend to effect with your "not an answer" framing? would you like me to expand my answer? would that change your mind? $\endgroup$ Commented Apr 24 at 19:49
  • $\begingroup$ @ChristophRackwitz To be fair, I don't think any of us has provided a full answer, in the sense that we haven't given an out-of-the-box solution to the problem. Mainly because it requires quite a bit of experimentation on OP's behalf. However, I think all of us combined have given very good guidelines and ideas on how to proceed. So don't be mad with each other, we're all trying to help :) $\endgroup$
    – Asher
    Commented Apr 26 at 1:41

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.