Auto-cropping scanned negatives with OpenCV

A project log for 35mm Flim Negative Scanning

Rig and technique for digitizing 35mm film negatives.

Stephen HoldawayStephen Holdaway 02/16/2019 at 08:122 Comments

With the number of negatives I've scanned, manually cropping each and every one image down to the exposed part of the film is pretty tedious. Over the last couple of days I've been hacking together a solution for taking a scanned negative from Adobe Lightroom, detecting the bounds of the exposure using OpenCV, and pushing that crop information back into Lightroom.

This is a fairly straight forward image processing problem, as most images have an obvious edge between the unexposed film and the exposed part. I chucked together a script in Python that uses a simple brightness threshold to look for this:

Oh hey, it's me in 1996
Exposure area detection using a brightness threshold

Once the bounds of the frame are known, there's a few small adjustments to improve it. The edge is inset slightly to make sure the crop doesn't contain any (partly) unexposed film, and the crop rectangle is adjusted to have an exact 3:2 aspect ratio. Once that's done, there's a bit of fiddling to turn it into four 0.0 - 1.0 range edge offsets that Lightroom understands.

Crop detected using OpenCV. The blue line is the detected frame, and the green line is the final crop. The green dot is a sniper.
Crop applied in Lightroom and developed to a positive.

Writing a Lightroom plugin to glue this together was a gigantic pain as the Lua API for Lightroom is poorly documented, and its plugin system is designed only for export plugins (eg. posting to a new social network), but the result was worth it:

Demo of integration with Lightroom 6 (and a slightly more challenging image). To trigger this, I use File -> Plug-in Extras -> Negative Auto Crop

The detection process I have at the moment works on most images, but as it looks for the biggest single blob/contour, it fails on images with over or under exposed parts that cross the full width or height of the frame. I'm looking at running a few different passes to resolve this. Underexposed images don't process very well as they have low (or no) contrast between the exposure and the rest of the film.

Currently the resulting crop is the median of all rectangles over a minimum size. These are the green rectangles in the gifs above (the red rectangles are too small and are ignored).

To prevent light shining through sprocket holes from affecting the frame, the brightest pixels in the image are masked out during thresholding. The darkest pixels probably also need to be ignored to stop dark marks on the film being included as an edge.

Here's the code at the time of writing.


Jan wrote 02/16/2019 at 11:02 point

I would love to see that Python script as it's super interesting to me with how little/much effort things like this can be done with the proper software!

Nice progress on your dia scanning!

  Are you sure? yes | no

Stephen Holdaway wrote 02/16/2019 at 11:44 point

The Python script is hiding at the bottom of the Gist linked at the end of the post:

The image is prepped and iterated in findExposureBounds(), with getRect() turning a threshold value into a rectangle.

The OpenCV stuff is actually pretty straight forward once it's installed. I hadn't used it before, so this is put together from 90% Googling and 10% reading the docs. The hardest part was the slight differences between the 2.x and 3.x OpenCV  API and the sometimes ambiguous error messages it gives when a parameter type is wrong.

  Are you sure? yes | no