AI-Created Software

Last Updated: October 16, 2026

The following is AI-generated (aka vibe-coded) software. The software is specific to my needs and my system and may not work for you or your system. Support is limited, but if you report a bug I'll certainly look into it. All code is provided in a zip file for you to review should you choose.

Auto Crop Photo Borders

Batch-crop scanned photos by removing the solid-color border left by the scanner bed. The script can also straighten photos that were placed at a slight angle, split a scan containing several prints into separate files, and preserve the metadata that matters for archival scans: EXIF, color profile, and DPI.

Requirements

  • Python 3.8 or later
  • Pillow (9 or later recommended)
  • NumPy 1.16 or later
pip install Pillow numpy

No other dependencies (OpenCV, SciPy, etc.) are needed.

Quick start

python auto-crop-photo-borders.py --input-folder-path scans --output-folder-path cropped

This crops every supported image in scans that has a white border and writes the results to cropped with the same filenames. The originals are never modified unless you explicitly allow it (see Safety).

It’s a good idea to try new settings on a copy of a few scans first, or to use --dry-run to see what would happen without writing anything.

Common recipes

Photos scanned slightly crooked

python auto-crop-photo-borders.py --input-folder-path scans --output-folder-path cropped --deskew

Several prints on each scan

python auto-crop-photo-borders.py --input-folder-path scans --output-folder-path cropped --split --deskew

A scan named album-03.jpg with three prints produces album-03_1.jpg, album-03_2.jpg, and album-03_3.jpg, numbered in reading order (top to bottom, left to right).

Border isn’t white, or varies between scans

python auto-crop-photo-borders.py --input-folder-path scans --output-folder-path cropped --color-hex auto

A dark line along the edge of every scan (a shadow from the scanner lid)

python auto-crop-photo-borders.py --input-folder-path scans --output-folder-path cropped --ignore-edge 5

A large archive with subfolders, using all CPU cores, with a log

python auto-crop-photo-borders.py --input-folder-path archive --output-folder-path archive-cropped \
    --recursive --workers 0 --skip-existing --log-csv crop-log.csv

--skip-existing makes it safe to stop and restart a long run: files that already have output are skipped.

Remove a thin blended edge left after straightening

python auto-crop-photo-borders.py --input-folder-path scans --output-folder-path cropped --deskew --padding -2

How it works

  1. Load. The image is opened and any EXIF orientation is applied, so detection works on the picture the way it’s meant to be viewed.
  2. Classify pixels. A pixel counts as border if each of its red, green, and blue values is within the tolerance of the border color. With --color-hex auto, the border color is the median color of a thin strip around the scan’s edges.
  3. Remove dust. Isolated specks smaller than --denoise pixels are ignored.
  4. Find the photo. The script takes the bounding box of all non-border pixels, then repeatedly trims rows and columns in which at least --fill-threshold percent of pixels are border. The percentage is measured only across the part of the image that remains, so a small photo on a large scanner bed is found correctly, and stray marks near the edges don’t prevent a row or column from being trimmed.
  5. Straighten (with --deskew). The script traces the photo’s outline, finds the rotation that fits it into the smallest rectangle, rotates the photo, fills the exposed corners with the border color, and detects the crop again.
  6. Split (with --split). Separate groups of non-border pixels are treated as separate photos. Groups closer together than --split-merge-distance are joined, and groups smaller than --split-min-area (dust, scratches) are ignored. Each photo is then straightened and cropped on its own, with neighbouring photos masked out.
  7. Apply limits from --padding and --max-crop, crop, and save.

Options

Folders

Option Default Description
--input-folder-path PATH required Folder containing the scans.
--output-folder-path PATH required Folder for the cropped images. Created if it doesn’t exist.
--recursive off Also process subfolders. The folder structure is recreated in the output folder. If the output folder is inside the input folder, it is excluded from the scan.

Border detection

Option Default Description
--color-hex COLOR FFFFFF Border color as hex (#FFFFFF or FFFFFF), or auto to detect it for each scan.
--tolerance 0-100 10 How far each color channel may differ from the border color. 0 is an exact match; 10 allows ±26 on the 0–255 scale. Raise it for noisy or unevenly lit scans.
--fill-threshold 1-100 80 Percentage of a row or column that must be border for it to be trimmed. Lower values crop more aggressively.
--denoise N 3 Ignore dust specks up to about N pixels across. 0 turns this off.
--ignore-edge N 0 Ignore N pixels along every edge of the scan. Useful for dark shadow lines or scanner artefacts at the very edge.

Cropping

Option Default Description
--padding N 0 Keep N pixels of border around the photo. A negative value trims N pixels into the photo instead, which removes any soft edge.
--max-crop PERCENT 100 Never remove more than this percentage of the width or height from any one side. 100 means no limit.

Straightening

Option Default Description
--deskew off Straighten photos that were scanned at an angle.
--max-skew DEGREES 10 Largest tilt to look for (up to 45).
--min-skew DEGREES 0.1 Photos tilted less than this are left unrotated, avoiding an unnecessary resample.

Multiple photos per scan

Option Default Description
--split off Detect and save each photo on a scan separately. If only one photo is found, the output uses the original filename.
--split-min-area PERCENT 2 Ignore objects smaller than this percentage of the scan’s area.
--split-merge-distance PERCENT 1 Objects closer together than this percentage of the scan’s shorter side are treated as one photo. Increase it if one photo gets split into pieces; decrease it if photos placed close together are kept as one.

Output

Option Default Description
--quality 1-100 or keep 95 JPEG and WEBP quality. keep reuses the source JPEG’s compression settings. For WEBP output, keep saves losslessly.
--skip-existing off Skip scans whose output file already exists. In split mode, the check is for name.ext or name_1.ext.
--allow-overwrite off Allow the output folder to be the input folder. Originals will be replaced.

Batch

Option Default Description
--workers N 1 Number of scans to process at the same time. 0 uses one per CPU core.
--dry-run off Analyse every scan and print what would happen, without writing any images.
--log-csv PATH none Write a CSV file with the details of every image processed.

Reading the output

Each line shows the file, a status, and what was done:

[1/9] brightsky.png      OK    border #FAFAFA; cropped top=250px, bottom=152px, left=152px, right=152px
        ! crop limited by --max-crop 25%
[2/9] broken.jpg         SKIP  cannot open: Truncated File Read
[8/9] sub/multi.jpg      OK    border #FAFAFA; split into 3 photos
        -> multi_1.jpg  1098x748, rotated -1.50°
        -> multi_2.jpg  1098x748, rotated +3.00°
        -> multi_3.jpg  1098x748, rotated -0.80°
[9/9] tilted.tif         OK    rotated -2.50°; 1800x1400 -> 1198x798
  • OK: the image was processed (or would be, in a dry run).
  • SKIP: the file couldn’t be opened, its output already exists with --skip-existing, or saving would overwrite the input.
  • ERROR: something failed during processing. The batch continues with the next file.
  • Lines starting with ! are warnings worth checking.

The script exits with code 1 if any file had an error, and 0 otherwise, so it can be used in scripts and scheduled tasks.

CSV log columns

Column Meaning
input, output Source file (relative to the input folder) and output path. Split scans get one row per photo.
status, message Same as the console output.
region_left … region_bottom In split mode, the area of the scan this photo was taken from.
rotation_deg Rotation applied, in degrees counter-clockwise. 0.0 means not rotated.
crop_left … crop_bottom The crop box. If the photo was rotated or split, the coordinates refer to the rotated or split-out image, not the original scan. Empty means nothing was cropped.
output_width, output_height Final size in pixels.
notes Warnings for that image.

Formats and metadata

Supported formats: JPEG, PNG, TIFF, BMP, and WEBP. Each output uses the same format and filename as its source.

What How it’s handled
JPEG quality Saved at quality 95 with full color resolution (no chroma subsampling) by default, instead of Pillow’s default of 75.
EXIF Kept for JPEG, PNG, and WEBP sources. The orientation tag is reset to “normal”, because the rotation has already been applied to the pixels.
ICC color profile Kept, except when the image has to change color type for the output format (for example, CMYK saved as PNG), where the old profile would no longer be valid.
DPI Kept, so prints come out at the correct physical size.
TIFF compression LZW, Deflate, and PackBits compression are kept.
16-bit grayscale Kept as 16-bit in PNG and TIFF. Converted to 8-bit for JPEG, WEBP, and BMP, which can’t store it.
16-bit color (48-bit) Pillow reads these as 8 bits per channel, so the output is 8-bit.
The script prints a warning when it detects this.
Transparency Kept where the format supports it. For JPEG output, transparent areas are filled with the border color.

If saving with full metadata fails (for example, an EXIF block the output format rejects), the script retries without it and prints a note.

Safety

  • The script refuses to run when the output folder is the same as the input folder, unless you pass --allow-overwrite.
  • Files that can’t be opened or fail during processing are reported, and the rest of the batch continues.
  • --dry-run writes nothing, not even the output folder.

Limitations and tips

  • Bright areas at a photo’s edge can be cropped. If the sky, snow, or a white wall runs right up to the edge of a print, those rows look like border. Use --max-crop to cap how much can be removed per side, or lower --tolerance. Be aware that --max-crop also limits legitimate crops, so it works best on batches where the photos fill a similar share of each scan.
  • Straightening relies on the photo’s outline. It works best when the photo’s edges contrast clearly with the border. If much of an edge is close to the border color, the estimated angle can be off; check the rotation in the output or log.
  • Rotation causes a slight softening. A rotated photo is resampled, and its edges can have a pixel or two of blended color.
    --padding -2 removes this.
  • Photos placed very close together may be treated as one when splitting. Leave a gap between prints on the scanner bed, or lower --split-merge-distance.
  • Memory. Each worker holds one full-size scan in memory. A 7000 × 5000 pixel scan with straightening and splitting uses about 650 MB, so reduce --workers if you run out of memory.
  • Very large scans. Pillow refuses images over roughly 178 megapixels as a safety measure against malicious files. Such scans are reported as errors.

Troubleshooting

Symptom Try
“no border found” on scans that clearly have one Check --color-hex, or use auto. Raise --tolerance.
One side isn’t cropped There may be a dark line or artefact at that edge. Use --ignore-edge.
Too much is cropped Lower --tolerance or --fill-threshold, or set --max-crop.
Too little is cropped Raise --tolerance, lower --fill-threshold, or increase --denoise for dusty scans.
A photo is split into several pieces Increase --split-merge-distance.
Dust is saved as a separate photo Increase --split-min-area or --denoise.
“no consistent border color along the edges” The photo probably touches the edge of the scan, so auto can’t find a border. Specify --color-hex.
(1 votes, average: 5.00 out of 5)

Leave a Reply

Your email address will not be published. Required fields are marked *

Notify me of followup comments via e-mail.