Normalise PGM Images Safely with pgmnorm and pnmnorm
pgmnorm stretches the contrast of a washed-out PGM greyscale image, but on a modern install it is only a symlink to pnmnorm. Allow about ten minutes. You need the Netpbm package, a PGM file, and space for a second copy of the image.
The route
Jump straight to the step you need, or tick off Done means at the end.
This guide uses Netpbm 11.5.2 from package version 2:11.05.02-1.1build1. The old name remains useful in scripts and old runbooks, but new commands should reach for pnmnorm directly, since it is the maintained name and also handles PPM input.
1. Confirm the replacement on this machine
These checks are read-only and need no elevated privileges:
$ command -v pgmnorm
/usr/bin/pgmnorm
$ ls -l /usr/bin/pgmnorm
lrwxrwxrwx ... /usr/bin/pgmnorm -> pnmnorm
$ pnmnorm -version
pnmnorm: Using libnetpbm from Netpbm Version: Netpbm 11.5.2
Exact permissions, build details and the package revision will vary by machine. What matters is that the command exists and reports the Netpbm version you expect. Missing pgmnorm but present pnmnorm? Use pnmnorm directly. Neither present? Install Netpbm through your system's normal package process before going further.
Checkpoint
Executable name and version are confirmed. Nothing has changed on disk yet.
2. Inspect the source before touching anything
Pick a source file and look at its metadata first. This is still unprivileged and read-only:
$ INPUT='/path/to/input.pgm'
$ pnmfile "$INPUT"
/path/to/input.pgm: PGM raw, 240 by 160 maxval 255
Dimensions and maxval will differ for your file. PGM comes in plain text or raw binary, and pnmnorm reads both, writing out the same family it read in. Never point an output redirection at a path you do not fully control: a typo can wipe an unrelated file before the program has even started.
Without a dedicated metadata tool, the first line of a PGM is normally P2 or P5, but prefer pnmfile when it is available, since it reports dimensions and maxval in one go.
3. Create a normalised copy
Send standard output to a new filename; the source is read, never edited in place:
$ OUTPUT='/path/to/input-normalised.pgm'
$ pnmnorm "$INPUT" > "$OUTPUT"
pnmnorm: remapping 0..255 to 0..255
$ printf 'exit status: %s\n' "$?"
exit status: 0
That diagnostic line reports the brightness range chosen for the stretch, and its numbers are entirely image-dependent. A message like remapping 0..255 to 0..255 just means the selected range already spans the full scale. Exit status zero only means the command finished; verify the output separately:
$ pnmfile "$OUTPUT"
/path/to/input-normalised.pgm: PGM raw, 240 by 160 maxval 255
$ test -s "$OUTPUT" && printf '%s\n' 'output exists and is non-empty'
output exists and is non-empty
There is nothing to undo here, since this workflow only ever creates a new file. Remove the copy with your normal process once you no longer need it. The original input remains your recovery copy throughout.
4. Understand the default stretch
Left alone, pnmnorm maps the darkest 2 percent of pixels to black and the brightest 1 percent to white, then spreads everything in between across the full range. Ignoring those small extreme groups usually gives a more useful result than letting one stray dark or bright pixel set an endpoint.
Those percentages are not exact, since a histogram only contains whole pixels: pnmnorm chooses thresholds that remap at least the requested proportion. On a small or near-uniform image the black and white thresholds can meet, in which case Netpbm nudges one threshold by a single sample value to keep the mapping legal, so a small image will not necessarily behave like a large photograph.
Tip
Do not read the default stretch as an exposure fix or a quality improvement. It changes pixel values, full stop. Check the result for clipped shadow and highlight detail before it replaces an original asset in your workflow.
5. Set conservative endpoints when the default overshoots
Use -maxexpand to cap how far the selected range can stretch. This example allows at most 50 percent extra expansion:
$ pnmnorm -maxexpand=50 "$INPUT" > "$OUTPUT"
pnmnorm: limiting expansion of 150% to 50%
pnmnorm: remapping 35..85 to 0..100
Those diagnostic values are examples, not fixed output. A source with a narrow middle range can turn a full stretch into exaggerated noise and banding, so a limit gives you a bounded first attempt without promising a good-looking image.
For exact control, set -bvalue and -wvalue directly. This example maps input values 20 and 230 to black and white:
$ pnmnorm -bvalue=20 -wvalue=230 "$INPUT" > "$OUTPUT"
pnmnorm: remapping 20..230 to 0..255
Keep values inside the image's maxval. A histogram from ppmhist helps you pick sensible elbows, but look at the actual image too. Give both a value and a percentage for the same endpoint and current Netpbm picks whichever produces the smaller change; older releases behaved differently, so do not mix the two across scripts that must behave consistently on old hosts.
6. Use the legacy name only when compatibility demands it
The installed manual for pgmnorm says it was replaced in Netpbm 9.25, back in March 2002, and records the key compatibility fact: pnmnorm is backward compatible with pgmnorm and additionally handles PPM images. The safest migration is a small, tested rename:
$ pgmnorm "$INPUT" > "$OUTPUT.old-name-test"
$ pnmnorm "$INPUT" > "$OUTPUT.new-name-test"
$ cmp "$OUTPUT.old-name-test" "$OUTPUT.new-name-test" && printf '%s\n' 'outputs match'
outputs match
Only run that comparison when both output files are disposable and you have the space. cmp checks byte-for-byte equality, nothing more, so never treat it as a visual quality test. Once a script is updated and tested, prefer pnmnorm and note the Netpbm version if reproducibility matters to you.
Done means
- Version confirmed. Netpbm is installed and its version is recorded.
- Input identified.
pnmfileor an equivalent check named the source image. - Output redirected safely. A deliberate new path was used and the source stayed unchanged.
- Default stretch understood. The 2 percent black and 1 percent white thresholds may clip detail.
- Endpoints tuned when needed.
-maxexpandor explicit values replaced an overly aggressive default. - Modern name preferred. New scripts call
pnmnorm, keepingpgmnormonly for compatibility.