Remove Speckle Noise from PGM Images with pgmmedian
pgmmedian scrubs salt-and-pepper speckle out of a PGM image by swapping each pixel for its neighbourhood median. Budget about ten minutes for a first pass, and have a readable PGM plus room for a second copy ready before you start.
The route
Jump straight to the step you need, or tick off Done means at the end.
This guide describes Netpbm 11.5.2, installed here as Debian package netpbm 2:11.05.02-1.1build1. The manual page is dated 20 January 2022, so check your own version if you are scripting across distributions.
1. Check the command and protect the source
- Work unprivileged. Filtering a file in your own directory needs no elevated rights, and
pgmmediannever modifies the input. - Confirm the binary. Run the two checks below before you trust anything else in this guide.
$ command -v pgmmedian
/usr/bin/pgmmedian
$ pgmmedian --version
pgmmedian: Using libnetpbm from Netpbm Version: Netpbm 11.5.2
...
The version command also prints build details and a pointer to the manual page. If command -v comes back empty, install Netpbm through your normal package manager. Do not reach for sudo just to run a filter.
Checkpoint
Note the input path, something like /path/to/input.pgm, and leave it alone until you have inspected the filtered result.
2. Run the default 3 by 3 filter
Leave the mask dimensions unset and pgmmedian defaults to a width of 3 and a height of 3. Redirect the output somewhere new:
$ pgmmedian /path/to/input.pgm > /path/to/filtered.pgm
$ printf 'exit status: %s\n' "$?"
exit status: 0
What lands on standard output is a PGM image, not a status report, so do not expect it to read like a log. In Netpbm 11.5.2 a successful run writes a raw, binary PGM starting with the P5 magic number. Printed straight into a terminal it looks empty or garbled, so inspect it properly instead:
$ file /path/to/filtered.pgm
/path/to/filtered.pgm: Netpbm image data, size = 5 x 5, rawbits, greymap
$ head -c 2 /path/to/filtered.pgm
P5
The exact wording from file varies by system. What matters is that the output exists, is non-empty and has the dimensions you expected. The source file has not moved.
3. Choose the mask size on purpose
A bigger mask pulls in more neighbouring pixels, which clears broader noise but also reshapes more of the image. Set width and height explicitly whenever the 3 by 3 default is not the right tool:
$ pgmmedian -width 5 -height 5 /path/to/input.pgm > /path/to/filtered-5x5.pgm
$ file /path/to/filtered-5x5.pgm
/path/to/filtered-5x5.pgm: Netpbm image data, size = 5 x 5, rawbits, greymap
These numbers describe the median mask, not a resize. Each one has to fit inside the corresponding input dimension, and the manual gives no automatic sizing based on file name, so never assume a small image wants a small mask.
At the edges, where a full mask would spill outside the frame, pgmmedian just copies the input pixels across. That means the border of a result can look completely unfiltered even while the centre has changed a lot. Check the whole image, not just the first or last row.
4. Pick the median algorithm when it matters
By default the program picks between histogram sorting and a select-kth-value method, based on the image's maxval, the mask area and a cutoff that defaults to 250. For a pipeline that has to behave the same way every time, name the method yourself:
$ pgmmedian -width 3 -height 3 -type histogram_sort \
/path/to/input.pgm > /path/to/filtered-histogram.pgm
$ pgmmedian -width 3 -height 3 -type select \
/path/to/input.pgm > /path/to/filtered-select.pgm
The only accepted values are histogram_sort and select. Supplying -type makes -cutoff a no-op. Leave the method unset and the default is usually the sensible choice: reach for -cutoff only after you have actually measured a reason to shift that trade-off for your images.
5. Do not let a redirect eat a good result
Shell redirection with > truncates its target the instant it opens the file, before pgmmedian has produced a single byte. Write to a temporary name first, check it, then swap it in:
$ pgmmedian /path/to/input.pgm > /path/to/filtered.pgm.new
$ test -s /path/to/filtered.pgm.new && file /path/to/filtered.pgm.new
/path/to/filtered.pgm.new: Netpbm image data, size = 5 x 5, rawbits, greymap
$ mv /path/to/filtered.pgm.new /path/to/filtered.pgm
Warning
That final mv overwrites whatever destination already exists. Skip it if the old file is still your recovery copy. If filtering fails, read the diagnostic, leave the source alone, and only delete the leftover .new file once you have confirmed its path. There is never a need to touch the original PGM to recover from a bad run.
6. Work through the common failures
An input error almost always comes down to a wrong path, missing permissions or bad PGM data. Check read access without changing anything:
$ ls -l /path/to/input.pgm
$ test -r /path/to/input.pgm && echo readable
readable
- Binary text on screen? That is a redirection slip, not necessarily a filter bug. Interrupt the display and rerun with
>pointing somewhere sane. - Wrong dimensions in the result? Check the source with
fileand review any explicit mask values you passed. - Border looks untouched? That is the documented edge-copy behaviour from step 3, not a bug.
Exit status 0 only means the command finished, not that the image looks right. Confirm the dimensions and magic number, then look at the result in an image viewer or a further Netpbm conversion, keeping the original around until that check is done.
Done means
- Command confirmed.
pgmmedianis installed and you know its version. - Source untouched. The original PGM has not been overwritten at any point.
- Output verified. The filtered file is non-empty and has the dimensions you expected.
- Mask sized deliberately. Width and height were chosen for the image, not mistaken for a resize.
- Algorithm explicit. Any
-typechoice uses one of the two accepted values. - Replacement checked first. No existing output was overwritten before its replacement had been inspected.