DIY more expensive segmented capture film scanner (part 2)

In part 2 of the scanner saga we disregard the low-cost aspect of the original concept, seeing how far we can push this shit. This is the DIY segmented capture film scanner (Mk. II)

Table of Contents

A larger, more luxurious boi: CNC 3040 frame

The Mk. II variant uses a "CNC 3040" gantry type motion stage. This moves the camera head, leaving the film in place. This has a significant benefit in that we no longer have to toss the film and backlight assembly back and forth. It's also significantly stiffer and has X/Y room to spare.

Due to this, I can run ~20x higher accelerations and ~5x higher speeds vs. the old 3D printer frame.

Motors & Control

NEMA 17 standard size motors are used with 3D printed adapters and the usual flex-couplers. A 400 W 24 V power supply I had sitting around powers both the stepper drivers and the backlight panel. An earlier 120 W supply could sustain power but had issues on startup.

The same Klipper based control scheme from before is used even though a CNC-type frane is used. Crash-homing ("sensorless homing" in fancy talk) is used with the TMC2208 E-field sensing telling Klipper when the motor has crashed.

CobaltTL lens

The CobaltTL lens was hinted at in Part 1, and it is a big boi. At f⁄6 it offers basically diffraction-limited sharpness except for the far corners of the image circle.

Edmund Optics CobaltTL 0.55⨉ lens MTF chart with T-Max 100 response overlaid shows high sharpness though not quite matching the sharpest films. Appropriate sharpening filters can probably make up for the difference.

It has a resolving power of around 2.4 µm and 1.1" image circle, making it capable of around 20-25 Megapixels resolution while covering a 35 mm frame in 4 exposures.

A major improvement in stray light performance was attained by using a lens hood that extends down to a few mm above the object (film) plane. I made a composite hood using a previously bought lens hood and made a 3D printed extension with a square cutout a few mm above the focus point. The inside was filled with some blackened aluminium foil.

The computer

Shoutout to the HP ProDesk 600 G3 SFF computer. This was part of the later Mk. I setup as well. This type of 6th generation Intel based computer is very affordable, and the SFF form factor means it can take a dual 10 Gbit⁄s SFP+ network card. The internal NVME SSD is plenty fast to dump image data over 10 Gbit⁄s after each scan for processing. It also leaves open a 1oGigE-Vision upgrade in future (just need $2000 or so for the camera).

I kitted it out with 24 GB of DDR4 RAM (at the height of the RAM-crisis that was a lot), though in practice only a small amount was used so I might scavenge that later.

As usual I run Debian at whatever the latest version is (13, at time of writing) and it's good.

I modified the computer to output 12 V on a standard barrel jack. It then powers the 15" VGA monitor for preview (my specific computer has a VGA port for some reason), as well as the camera.

Learning to love bayer: the new camera

As part of the Mk. II variant I somewhat begrudgingly switched to a bayer pattern camera. I had a lot of issues throughout development with field sequential colour capture synchronisation, colour separation, and the speed of capture was pretty limited.

Another "camel to swallow" was going back to a MDVision camera. These cameras are simply far too affordable and available in whatever variant you want to ignore.

With the new lens in place I ordered a colour PoE powered camera with a 1" Sony IMX183 rolling shutter sensor. This is a relatively old (~2017) 4k+ style sensor with 2.4 µm resolution and 5544⨉3694 pixels format (~20 Megapixels)—perfectly matching the lens, and supporting 12 bit readout.

Fortunately I found the second go around much easier, probably due to me having more experience dealing with industrial cameras in general. There are some minor features I'd have liked and the documentation isn't incredibly detailed, but everything seems to work stably.

With the 0.55⨉ lens this yields a 35⨉24 mm scan size of 8000⨉5500 pixels or 44 Megapixels, an adequate amount, and comparable to the highest lab-quality scans typically offered. This is equivalent to around 5200 dpi.

It is noteworthy that each pixel output by this camera isn't quite as good as the ones coming off the Raspberry Pi HQ camera based one, since that previous camera had 1.55 µm pixels which I could decimate to approximate ~3 µm monochrome. In very fine details we can sometimes see debayering artefacting:

Glass test slide's 10 µm line pattern isn't cleanly resolved, instead disappearing into debayering artefacting. This also serves as a verification that the film plane pixel pitch is correct (measured 4.16 µm with pixel rounding, theorethical 4.36 µm).

Future work is to look into higher resolution debayering algorithms, as I expect this performance is sub-optimal.

Note that I've imposed a 100 megapixel limit on my processor outputs since e.g. a typical 6x9 frame is pretty unlikely to need more than that, while it's basically certain that your favourite image processing software will struggle to handle ~300 MP 48-bit TIFF's.

The achilles heel of a 20 MP Gig-E Vision camera is the 1 Gbit⁄s video output, this is simply too slow. While I like the sensor I do expect to replace it with a 10 Gbit⁄s model in future (doing this will triple the value of the scanner). I suppose USB 3 cameras could be used but this doesn't give the warm fuzzies of Ethernet.

A future article will address speed improvements I made after publication. Suffice to say that the MDVision library mainly does processing on the CPU and was a significant bottleneck.

Due to the very slow readout speed I can't reasonably do captures in optimal quality, e.g. number of averaged captures has to be lower than desired to keep scan times acceptable. I would consider a 5⨉ improvement in scan speed with the rest spent on improved quality to be optimal.

Colour management

At the time of publication colour management is done with a colour-correction matrix (CCM) applied at capture-time, with an ICC profile injected after processing. Presently, the matrices for the IMX477 were used since these were available, and Display P3 is used as the output colour space.

You might think using RGB LED's would make a correction matrix less necessary, and if I had free choice of LED wavelengths then this might be so. Below I've shown the IMX183 colour response with the WS2812B wavelength ranges overlaid:

IMX183 CFA response with WS2812B LED emission ranges overlaid. Note that most cameras ship with a sharp IR blocking filter that cuts in around 700-750 nm.

As seen above the wavelengths of the WS2812B (~465, 530, and 620 nm) are sub-optimal for colour separation with the IMX183 sensor (this sensor is fairly typical).  The blue-green area is the worst. If we could pick freely then 450, 550, and 650 nm would be preferable, but unfortunately it seems all common RGB LED chips use the same wavelengths.

Below are the spectral sensitivity and dye spectral density of Vision3 250D vs. the WS2812B wavelength intervals.

WS2812B spectral response vs. sensitivity of Vision3 230D. Note the much sharper response than the IMX183 above. Colour films can't rely on post processing with colour correction matrices so it has "masking dyes" to achieve the same effect.
WS2812B spectral response vs. dye density of Vision3 250D. Using shorter blue and longer red LED wavelengths would likely improve scanning performance. Significant overlap hints that the original field-sequential colour capture technique would likely give poor colour separation, which seems experimentally confirmed.

For scanning the dye response to the right is most important. A full analysis would also have to include the effects of the sensor filters, in principle these plots could be used to calculate correction matrices, this is left as an exercise for the reader.

Vision3 50D scan using WS2812B LED's, mono camera, no CCM
The same Vision3 50D frame with a CFA camera with CCM

For colour negative scanning a CCM tuned for the IMX477 at 8600 Kelvin is used, since this gave the best overall colour separation albeit with excessive red saturation. For black & white a flat matrix is used. For colour posititives TBD matrix is used.

This technique gives extremely saturated colours vs. many other techniques, to the point where I occasionally have to desaturate some colours slightly (red in particular) to avoid clipping. Compared to the mushy 70's looking mess I had in earlier iterations this isn't really a complaint. I also found that colours seemed better when the backlight white balance was set properly, the capture profile preset system allows this to be configured for specific film types.

I consider it future work to do the work of properly calibrating my specific sensor and backlight-combo. I do have iT.8 chart recordings, but the software to process these all want to make ICC profiles, and applying those in general causes a lot of issues. Since I refuse to use AI tools this will require actual learning, and in the summer heat this wasn't really practical.

Defect pixel management

I decided it was time to do my own defect pixel management as well. This is actually relatively straight forward: A dark-frame is captured at appropriate settings, pixels which exceed some threshold above the median pixel value are flagged (separately processed for R/G/B). I decided on 0.5/255 as a reasonable threshold, and this gave around 30k dead pixels for the 20 MP camera.

100% crop of the dead-pixel-mask. The mask is monochrome but the preview is coloured based which channel caused the pixel to be flagged.

When applying the correction I calculate a gaussian blurred copy of the image, and use Python-type array masking to copy the blurred image pixels into the normal image only where the corresponding defect pixel mask is set.

Since there are some potential issues that could be encountered I decided to include the defect pixel image as part of the scanner output file set and apply the final correction in the merging software (allowing re-processing if issues are found). I also do the same processing for display purposes in real time, as well as when doing flat field correction.

In practice some dynamic dead pixels also show up from time to time, but the majority of the dead pixels are static so this is perfectly acceptable for my purposes.

A future improvement could be to do DPM before debayering, or to generate separate R/G/B defect pixel masks. Given the amount of defective pixels (~30k out of 20+ MP) it doesn't seem worthwhile to optimise this.

Revised multi-exposure HDR

In part 1 I demonstrated a simple HDR/composite using proportional scaling of two exposures (in linear light). This worked fine at the time, but when switching cameras I found that the technique was harder to apply consistently, sometimes giving noticeable steps in the tone curve which were quite ugly. I suspect this is a combination of the camera transfer function not being entirely linear (e.g. rounding off gains), and offset correction limitations.

32⨉ dual-HDR capture of Adox HR-50 slide shows poor shadow retention. The gull chest is close to fully transparent in the positive. Exceeding 32⨉ lead to artefacting halos around the blurred people in the background.
128⨉ multi-HDR capture of Adox HR-50 slide shows shadow retention similar to what a human eye can see looking at the actual positive.

The revised HDR merging is a simple extension of the previous approach: instead of two exposures at different gains, do a lot more of them. Doing this means camera calibration is far less important, I tentatively do √G steps in a geometric sequence (square root seems like a magic ratio). That is, for a HDR gain of 64, a total of 8 exposures are made in a sequence that goes approximately as 1, 1.8, 3.3, 6, 11, 20, 35, 64. Captures are done in rising gain order and summed proportionally at each step with the previous. For HDR gain 128, 11 steps are summed (floor() of the square root of N).

At this time I also fixed an off-by-one type error in summing, the images should be summed with weights 1⁄(1+gain) and 1– this. The error was present in the previous iteration but wasn't significant when using only high gains.

This technique makes it practical to use HDR gains up to and exceeding 256 where previously 64 was the practical limit, which is sufficient to digitise e.g. HR-50 slides as shown above (using a fairly low-end IDS UI537xCP-C camera). With the extended dynamic range it's also far less important to control exposure times. The technique comes at a cost of taking more shots, but as usual paring down the integration count makes up for that somewhat.

I apply different levels of HDR-gain depending on the scan type since excessive HDR gain takes longer and makes the output files more annoying to work with. Additionally there will eventually be some loss of resolution going to the 16-bit gamma encoded output when feeding >16 bit linear data in, at 256⨉ HDR and 12 bit capture we have ~17 bits of data in theory. For colour  and B&W negatives (e.g. Vision3, HP5+) 64-128⨉ is sufficient, for slides 256⨉ is optimal.

User experience

The scanner software now includes a set of presets for capture, including generic profiles like colour negative, colour positive, B&W etc.. Some additional profiles for certain films are also included like 500T, and I expect this list will grow.

The profiles specify the backlight R/G/B mix to compensate for the base colour of the film, the colour correction matrix to use, gamma coefficient, HDR gain/integration count, and a default integration time. The latter can be overridden if a particularly dense or light base is encountered.

Selection of scanner profile can be done manually, but it's also automatically selected when the film type is entered based on a simple word match. A similar matching is done when processing, mainly to apply specific sharpening profiles (e.g colour films benefit from additional sharpening vs. B&W).

Film holder presets are manually selected and include default scan sizes and X/Y offsets for the first frame. For medium format the frame spacing can be tweaked in discrete steps. The scanner can be configured to scan up to a given frame index, so for e.g. scanning 35 mm the film is loaded and it's told to scan up to frame 18 or 36.

After each image the scan computer beeps once to signal that a new image is coming, and after completing a batch scan several beeps signal that you probably need to load more film. I also added a parking mode where the camera head is moved out of the way to simplify loading, when the last frame in a given holder is loaded the camera is automatically parked.

Conclusion

The system as built now is very high performance, achieving basically the maximum sensible resolution and dynamic range. The software aspect isn't exactly finished but is now sufficiently robust that scanning a roll mostly involves scanning and not debugging new problems. Whenever a new type of film is encountered I usually find some tweaks to apply (most obvious being exposure time/RGB balance and sharpening presets) but this is to be expected.

The biggest issue in building the system has been sub-frame uniformity, though mostly dealt with it remains the biggest downside of the system. I'm hoping part of the issue is thermal since a lot of the testing was done at above 30 °C ambient owing to global warming. More advanced techniques could likely be used to deal with this in the image processing, for this project more basic approaches were used.

The biggest downside other than cost and size is the poor scanning speed and lack of IR-capabilities (not as important as you might expect if you have decent dust mitigation).

The speed issue will most effectively be dealt with by simply buying a faster 10 Gbit⁄s camera. This will effectively triple the cost of the scanner, but it's a problem with an obvious solution.

A future article will cover a major bottleneck discovered after publication which allowed a ~4-5x speed increase with the same camera.

The original project spec also included better film holders, these are still considered future work at the time of writing. The V850 stock holders with a minor diffusor-mod work acceptably but aren't amazingly good at holding negatives flat.

Build your own?

While not wanting to be flippant I am not interested in publishing plans or kits for this project, as this would be a major effort on my part.

If someone is interested in doing a project of this type as an open source hardware kind of thing I would be willing to contribute know-how and software code.

Timeline

Proper Mk. II start (middle of May-late June)

For a lot of the Mk. II development I used a borrowed IDS uEye industrial monochrome camera. This camera works ok but the sensor seems to have a lot of fixed pattern noise, and has a pretty limited exposure time and analogue gain range.

I switched to the CobaltTL lens, which really strained the 3D printer frame I used with its' ~150 mm working distance (measured off the front element).

Added a object-plane mask as part of the lens hood, basically a hood that extends to a few mm above the focus plane with an opening sized to match the object area.

Reprinted the holders a few times trying to get acceptable performance. Not really possible with this amount of weight moving around.

I ended up doing what I alluded to in part 1: buying a gantry style CNC router frame. You can probably build something suitable from loose parts fairly cheaply, I decided to just buy a "3040" CNC kit. This is a kit containing all the moving bits and a simple bed with T-slots. Price-wise it's comparable to buying a new Ender 3 type 3D printer.

Since the kit doesn't come with any control electronics I printed a bunch of stepper motor adapters and initially wanted to scavenge part off the old printer, but it ended up being less work to buy new parts. I picked up a slightly generic Creality E3 Free-runs board and got it flashed with Klipper, and hooked up some new stepper motors.

This gantry style CNC router still thinks it's a 3D printer, and that's fine. The only real difference is that the Z-axis range is quite limited on a router (~50 mm).

I decided to use crash-homing initially since this is perfectly adequate and saved a bunch of time wiring home switches or potentiometers.

Preliminary IDS bayer work

By the time I actually took the first shots with the new setup I had switched to a bayer-pattern variant of the same IDS camera, over all it seemed to work about as well.

One restriction of the new frame that I hadn't fully considered is the fairly low maximum build height, so I only had around 5 cm to go from backlight to film plane. This seemed to work out ok though. I ended up using the Epson holders directly (no glass/diffusor), I taped some diffusor sheets under the ANR glass instead of mounting a separate diffusor sheet.

In late June I also implemented the new HDR merging mode, as well as defect pixel management and did some actual operational scans. Initial operating capabilitiy achieved at last!

Display output showing a colour slide being scanned in HDR256 mode with 0.9 ms base exposure time. The lower right overlay shows that the system is currently capturing the lower right segment of the scan. Note that the overlay doesn't use gamma correction so looks quite dark.

With the addition of extended HDR I was able to lock the exposure time to one where the direct backlight was just under saturation, saving one step in scanning. An unfortunate side-effect was that the autofocus now had to use HDR-capture to properly focus on very dark slides, which slows it down a fair bit. This issue was later rectified with a new AF search algorithm and double precision AF processing.

A positive improvement seems to be that this approach reduces the impact of stray light from looking directly at the backlight (near film-edges), which previously caused frame-wide colour offsets.

State of the scanner just prior to the high resolution camera upgrade.

During this phase of development contrasty Adox HR-50 and underexposed E100 slides were used as test targets. These are both challenging to capture properly, for comparison the (IT.8 colour managed) Epson V850 capture is shown below.

Due to the downtime between tearing down Mk. I of the scanner and getting back to initial operating capabilities with Mk. II I resorted to scanning several rolls with the V850. While the results were over all acceptable I ended up re-scanning this sad roll of E100 (+2) since the V850 lost so much shadow details as to render most of the roll unusable.

Since the new HDR mode is kind of slow I modified the display to show the accumulation in real time, here's a video of 35 mm frames being recorded:

Refining the uniformity

A fair amount of effort was put into resolving uniformity issues in uniform areas, typically landscapes with a lot of sky.

I found that this CobaltTL lens performs pretty good, I had some issues with B&W gaps causing some bloom that meant the left/right segments of 6x9 negatives had some patchiness. A big improvement here was achieved by cleaning the front element, which presumably had accumulated a light coating of gunk from sitting around near e.g. 3D printed plastics.

I reimplemented a simpler flat field correction to match the improved HDR mode, moving the actual processing out to the server side and only doing a single set at full HDR-gain.

Switching to full-frame backlighting instead of per-subframe backlight patterns ensured spatial quantisation didn't affect uniformity without excessive backlight power. I also moved the backlight closer to the diffusor since the diffusor sheet mentioned in Part 1 actually works really well.

Ambient light reflecting off the top surface of the negative was a residual issue. I did a two-part solve of this: 1) I reversed the Z-axis to home at the top of travel. This let me use a larger combined object-plane mask and stray light shield which helped. 2) I re-enabled and improved my previous dynamic darkframe mode. In this mode I simply capture and subtract an image with the backlight off before each capture.

The combination of these techniques dramatically improved performance, though perfection wasn't quite achieved yet on my worst-case negatives.

New camera (who dis?)

Taking a step back, the new MDVision camera showed up. This lead to some rejigging of the camera interface of the capture software, but based on the previous monochrome experience I had already worked out a lot of the potential issues.

Setup of the camera took maybe 5-6 hours of fiddling. My previous work with eliminating uniformity issues paid off and I didn't have any particular issues with this.

Part of the time was spent simply going through all the API functions and implementing readbacks to ensure I was actually in the capture mode I wanted to be in. A common issue with API's like this is e.g. you ask for 16 bit image data and get it, but the camera itself may still be 8 bit. A careful stretch of the histogram usually reveals this particular issue though.

Obviously the slow 1 Gbit⁄s Ethernet link was a pain, so I turned down the frame averaging until it was good again, as usual whenever the scanner gets too slow.

After those initial hours I was able to scan two rolls of Adox HR-50 positives I'd been saving for just the occasion. HR-50 is a remarkably sharp film with very high DMax in reversal process, and those rolls were all shot with the Zeiss ZM Biogon 2.8/25, a lens known for sharpness. I was rewarded with some wonderful ~40 MP scans! (And some longer than previous waits for processing to complete! this will be rectified later.)

Scanning 6x9 format I found that the first 4-5 subframes captured tend to have a reddish offset (after inversion). This was to a large extent resolved by adding dummy-captures before starting a scan to "warm up" the camera. Another hack is to just start in the bottom right instead of upper left: the top part of the image is often the sky where such artifacts are noticeable. The bottom of the frame tends to be cluttered and darker, hiding it better.

Improving AF again

I decided to make a more reliable AF function. The major improvement can be summed up as:

  • Do the laplacian operation in double precision instead of 8 bit int (hugely important for dark captures, hugely slower to process)
  • Do a linear search with a step small enough to always find a reliable peak
  • Use a cubic spline interpolator to refine the result then repeat with a smaller step until satisfied. The interpolator probably isn't doing much for accuracy but at least there's a lot of decimals after the result.

Trying some colour management

I scanned a transparent iT.8 chart and tried processing it through the chain. My first promising attempt was feeding the chart to LProf, a tool that shows its' age. This produced a fairly linear PCS curve set for converting to Lab format, which seems like the right sort of thing to do. It seems more modern profiles use XYZ as the intermediate colour space though. I also tried the X-Rite software but it gave me a profile that didn't look very good.

I experimented with linearising the images (reversing the processor gamma correction) and this gave curves that resembled a gamma curve. The green & blue curves were really ugly though, so I decided on the gamma-corrected mostly flat curve as the appropriate one. The results seemed ok, better than the uncorrected workflow anyway (most visible as orange reds). Unfortunately applying this profile to real images immediately led a pile of issues.

As a simple hack (discarding the previous work for now) I grabbed a CCM I'd previously computed for a broadly similar vintage Sony CMOS sensor (in the Hayer HY-6110). I then added the Apple Display P3 profile to the output. This CCM is from a DNG profile, so probably calculated for conversion to XYZ D50.

Random camera CCM R G B
R 1.5445 -0.493 -0.1174
G 0.0191 0.8682 0.1374
B -0.0653 0.1245 0.5655
Gain 1 0.45 1

While probably not optimal this seemed to give a decent result for colour negatives, giving a result that didn't require extensive colour work for e.g. Ektar.

With a minor set of R/G/B offset and gain adjustments (and a slight saturation bump) here's a comparison to a V850 "colour managed" (through iT8 profiling) scan where a similar level of effort was expended:

The reds are far less orange, the greens tend towards cyan, and the yellows tend towards orange. Here's another comparison, the greens are more cyan in the P3 example. Note that the pinkish clouds are a typical artifact, the V850 doesn't seem able to properly capture RGB data in the dense cloud area of the negative.

Perfection has not been achieved, personally I really dislike orange reds and care less about the other colours, but having green in the picture would be nice.

Throwing CCM's at the wall

I decided to re-scan a batch of negatives to try to improve on the colours. Looking at the CCM shown above the green channel is basically ~20% blue so it's not shocking the greens turn to cyan. From my previous experience I knew the IMX477 scientific JSON's had some goodies and it's from approximately the same era. Surely there aren't that many variation on colour filter array responses?

IMX477, 5600 K R G B
R 2.06 -1.065 0.012
G -0.163 1.598 -0.454
B 0.035 -0.0554 1.527
Gain 1 1 1

This looks a lot more promising at a glance, a strong cross-rejection of red/green channels, green rejecting some blue, and blue basically just blue. Future note: looking at the main article dye-spectral responses & backlight wavelengths suggests we probably want per-film CCM's for optimal performance.

Using the IMX477 scientific 5600 K CCM's and Display P3 interpretation I still had  excessive reds but a lot more green. Here I'm using some Vision3 250D for comparison.

I'd say the red-orange saturation is on the high side, very candy land but a lot less sad looking.

Another comparison with a lot of green and red-orange:

It'd be nice to retain the nice greens from the old scan. For the new scan I had to selectively dial back the red intensity to keep them somewhat in-gamut.

More testing suggests the 8600 Kelvin CCM for the IMX477 is a decent choice for colour negative scanning. The greens above can comfortably be dragged to mostly match the V850 scan with a HSL-tool, and is shown to the right.

With permission. here's a comparison showing a real life dog and associated person:

Left side is D56, right is D86. The coat isn't entirely accurate in either frame (it's insanely yellow in person), but the background foliage is much improved with the D86 CCM. Tweaking some more gives the image shown below (this is the next frame).

One conclusion we can draw is that I'm not very consistent when it comes to setting the white-point in a colour scan.

Making presets

At this time I finally introduced the preset concept to the scanner software. This is simply a set of JSON/dicts that contain the settings for backlight, exposure time, HDR gain, and colour mode for the camera. I defined presets for all expected types (Colour negative orange & clear base, colour positive, B&W positive and negative) and adding more is fairly easy.

As I went along certain film types were deemed special enough to get their own presets (e.g. 500T, which had a much more orange base than most other colour films). The preset is automatically selected based on word matching the name of the film as entered, though can of course be overridden.

Also added was an extra post-process sharpening filter, I found that some film types really benefit from a bit of extra sharpening (Vision3 50D in particular, since it's not actually that sharp). There's no one-size-fits-all approach there so it's selected based on the film type.

Additionally I finally got around to making the film holder type runtime selectable, a hotkey switches what holder is used and includes X/Y offsets tuned to each holder, and a default scan size. For medium format scans where the frame spacing is often irregular I made the frame spacing adjustable.

Another demo

Here's a demo from mid July showing the scanner at work in 4-8x speedup scanning a single 35 mm frame. Note a minor graphical bug where the scan-overlap rectangle overlaps the histogram. The top bar shows the preset (Colour Negative, orange base), film type, roll ID (prefix for files), and frame-number 10. The upper right shows a clock-like alive-indicator, the blue line rotates every screen update and the yellow line rotates every time a camera frame is received.

The histogram is fairly self-explanatory, other than being shown non-inverted. The bottom line shows the exposure time, progress for HDR-capture, and real-time X/Y/Z coordinates.  Lower right shows the sub-frame layout of the scan and is filled in as the scan progresses.

The sequence starts with a 3-level AF search, 8 pre-exposures (to stabilise the sensor and backlight), followed by a dark-frame + capture of all 4 segments.

A recent addition was network streaming of the UI. I originally used 'feh' with bitmap files written to a SHM file, I decided to replace this with a slightly less wobbly setup. I generate bitmap data and dump into the stdin of ffplay, which runs in fullscreen. It seems like it uses fewer resources.

With that worked out it was a fairly simple job to add a second ffmpeg-based output stream. Using MJPEG at 10 FPS and ~30 Mbit⁄s gives a pretty nice low latency view. This isn't particularly needed since the monitor output is next to my workstation, but is useful for recordings like this.

Processing performance improvements

I spent a few days re-scanning the last month's or so worth of colour negatives since the V850 pipeline clearly had something wrong with the colour management. Results were much improved.

One annoyance was that the batch merging software had grown to be quite slow, taking around 50 seconds per 35 mm frame and close to 5 minutes for a 6x9 frame. 35 mm frames comprise 4 subframes while 6x9 has 16 suggesting approximately O(n²) scaling vs. number of subframes. N is unlikely to exceed 16 so we don't need to care too much until I get into 8x10".

I made the download vs. processing tasks run asynchronously just in case processing were to take longer than scanning a frame, and to ensure that if some merging script bug prevented processing I wouldn't be rescanning which happened a few times during development.

The biggest improvement came from rewriting the overlap-area blending routines to no longer work pixel-by-pixel (a quick hack while trying to make it work the first time) and rather to build a mask that could be multiplied by the left/right overlap pictures. This chopped the runtime down to around 30 seconds, making the template-matching & merging one of the faster operations in the chain.

Another improvement came from reducing some filter kernel sizes to more reasonable levels in the sharpening and defect-pixel handling (~20-25 seconds runtime).

Finally, switching from a pure float gamma correction to a 24-bit lookup table shaved off around 10 seconds. The tables are saved to disk for reuse in future runs, since calculating them takes around 1-2 seconds. The use of integer gamma is a bit painful but the 24-bit resolution should be about as accurate as the 32-bit floats. Our final processing time is down to ~15 seconds for a 35 mm colour frame, and ~6 for a black and white one! Still probably O(n²) but whatever, we also seem to be O(n) for number of pixel channels but the only likely candidates are 1 and 3.

I experimented with detecting frame spacing in 35 mm scans to auto-crop the scanned images (since I have to do a 2x2 scan to cover the entire frame, a lot of extra pixels are included). This worked for a couple of images and generally failed spectacularly, I expect it's possible but it has to be reliable to be worth it.

Since only one type of 35 mm holder is used and the frame position is pretty well known I just found a set of coordinates to crop to that always has the entire frame + some waste area included. This is worth doing since the output file sizes shrink by ~40% without losing anything of value.

Finished for now

With the post-processing performance improvements in hand I decided to call it for this articles' purposes. I may write a future article after some time actually using the system, and/or when I finally pull the plug on a higher performance camera.

Future work will include software improvements to make scanning various film types easier and more optimal, in particular colour performance could benefit from actually putting in the work to calculate proper corrections.

Another aspect of the system will be proper integration of the hardware, currently the control board and power supply are just hanging out near the scanner instead of being properly mounted. Additionally, a better backlight is still something I'd like to do.