Skip to content

Handle inline RGBI frames from SANE coolscan3#471

Draft
rohanpandula wants to merge 1 commit into
marcinz606:mainfrom
rohanpandula:coolscan3-inline-rgbi
Draft

Handle inline RGBI frames from SANE coolscan3#471
rohanpandula wants to merge 1 commit into
marcinz606:mainfrom
rohanpandula:coolscan3-inline-rgbi

Conversation

@rohanpandula

@rohanpandula rohanpandula commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds NegPy support for inline 1x RGB+IR frames from SANE's coolscan3
backend. Hardware verification is limited to one LS-5000 ED. Other models are
not claimed as supported by this change.

With the separate SANE patch, coolscan3 exposes infrared as a boolean option.
When enabled, it reports an RGB frame but returns four pixel-interleaved
samples: R, G, B, and IR. NegPy detects this contract, enables the option,
reconstructs the RGBI rows from the array returned by python-sane, and passes
the fourth plane to NegPy's existing IR path.

The coolscan3 path returns the same ScanResult(rgb, ir, ...) object that the
rest of NegPy already uses, so dust repair and file output stay unchanged.

Why channel repair is needed

python-sane assumes every non-gray frame has three samples per pixel. A
four-sample SANE_FRAME_RGB stream is therefore grouped into three-channel
rows and returned with the wrong shape. If the final group is partial,
python-sane discards it.

NegPy gets the real channel count from SANE's reported pixels, lines, and bytes
per line, then reconstructs complete R, G, B, IR rows from the returned sample
buffer. In the known truncated-tail case, every sample through the penultimate
scanner row is still present. NegPy returns that complete prefix as a zero-copy
four-channel view and omits the one incomplete edge row. It then splits RGB
from IR.

If IR was requested and no fourth channel arrives, the scan fails instead of
silently saving an RGB-only result.

Before reading, NegPy also verifies that SANE reports a terminal color frame,
the requested bit depth, positive geometry, and exactly four samples per row.
An inactive or read-only infrared option is not advertised as usable and fails
before scanner configuration if a caller requests it directly.

Other compatibility fixes in this PR

  • Strip net:<host>: before backend detection so the same coolscan3 strategy
    works through saned.
  • Do not write a mode option on devices such as coolscan3 that do not expose
    one.
  • Recognize direct and saned coolscan3 IDs as dedicated film scanners when
    they have no generic source or RGBI mode option.
  • Keep coolscan2 out of the inline strategy: it uses a separate-frame IR
    contract despite exposing a similarly named option.

Scope

This PR covers 1x RGBI integration only.

It does not include:

  • autofocus or multisample UI controls;
  • RGB Nx plus RGBI 1x practical-parity capture;
  • frame selection or roll registration;
  • paired TIFF writer changes;
  • SANE backend source code.

The required SANE change is available for review in draft MR !937:
https://gitlab.com/sane-project/backends/-/merge_requests/937. Until a backend
change lands, users need a patched coolscan3 build in which the infrared
option is active.

That SANE draft asks maintainers whether four interleaved samples should use
the existing SANE_FRAME_RGB precedent or a separate grayscale IR frame. That design question remains open for maintainer review.

Verification

Physical scanner:

  • Nikon Super Coolscan LS-5000 ED, firmware 1.03
  • SA-21 fitted with a solderless SA-30 conversion
  • direct USB capture at 16-bit RGBI, 1000 dpi and 4000 dpi
  • 4000 dpi geometry: 3946 x 5959 pixels, four 16-bit samples per pixel
  • raw 4000 dpi raster: 188,113,712 bytes, the complete expected size
  • scanimage PNM file: 188,113,751 bytes including its 39-byte header
  • the fourth sample contains a real IR defect signal

The saned path was first checked with SANE's test backend. Local and network
reads were byte-identical, including 16-bit sample order. It has now also run
on the physical LS-5000 through NegPy at 1000 and 4000 dpi. In each live test,
the second capture used the same 1x inline RGBI recovery path covered by this
PR. Both returned a real IR plane and left the scanner responsive.

The exact focused branch passes 1,605 tests, with 4 skipped and 7 deselected.
make format, make lint, and make type also pass. The 42 new focused tests
cover option discovery, frame-contract validation, and channel recovery.

Automated coverage includes:

  • direct and net: coolscan3 device IDs;
  • backend-scoped option discovery and coolscan3 backend inference;
  • divisible and truncated four-channel frame shapes;
  • zero-copy reconstruction of complete rows when the final group is
    truncated;
  • fail-loud behavior when IR is requested but no fourth channel arrives;
  • no behavior change for RGB-only scans, pieusb, coolscan2, or ordinary
    flatbeds.

Detect the active, writable coolscan3 infrared option over direct and saned device IDs. Validate the four-sample frame contract before reading, recover complete RGBI rows from python-sane, and fail instead of silently returning RGB-only output.
@activexray

Copy link
Copy Markdown

I found this PR as I'm a LS-9000 user and was looking at SANE support in NegPy. It seems the coolscan SANE backend is pretty much abandoned (and was missing quite a few features compared to NikonScan). I'm currently in the process of reverse engineering the entire interface layer by decompiling NikonScan. I'm not sure if my findings should be upstreamed or if we should have some alternative to SANE.

@rohanpandula

rohanpandula commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@activexray that is really awesome! I didnt even consider doing a decompile of nikon scan, I just used USBPCap on the wire to capture the info that way. I was working on a blogpost about it, heres my draft https://gist.github.com/rohanpandula/3185bc13fff8e3e6da44eb3b04108af1
and a preview of what it looks like with SANE/Coolscan5000 working
image
I am also trying to reverse engineer the color inversion technique that nikonscan uses, I think im 80% of the way there but I am going to shoot a calibration roll sometime this week to really get it nailed down, but decompiling might be another way to do it, if you see anything about that topic please let me know

@activexray

activexray commented Jul 15, 2026

Copy link
Copy Markdown

apparently packet capture from SCSI is weirdly difficult from Windows, so it was easier just to load the binaries in Ghidra haha. I wasn't going to focus on any of the post processing code, just getting every single scanner feature implemented. I do, however, have the binaries for ICE, which i've considered reverse engineering as well as I've found others' implementations (like Vuescan) worse.

@thetalkingdrum

thetalkingdrum commented Jul 18, 2026

Copy link
Copy Markdown

Very cool work going on here!

You might also want to check out this project: https://github.com/kevihiiin/Nikon-Coolscan-RE
Looks interesting, might be helpful.

@activexray

Copy link
Copy Markdown

I found that repo after spending quite a bit of time doing the same thing haha. Some of their conclusions seems to not be correct, at least for the coolscan 9000. Maybe I PR that repo, but I have yet to complete a scan from rust using only RE'd information. Once I can get a frame back I'll publish my findings.

@thetalkingdrum

Copy link
Copy Markdown

Yeah, it's a bit over my head, so I can't really evaluate how good the project is. But I'm really happy to see this community effort finally coming to the Coolscan family. I thought I'd share it in case there's anything useful there, and maybe it could lead to some cross-pollination of ideas and save you all from reinventing a couple of wheels ;)

@activexray

Copy link
Copy Markdown

I'm mad enough at the stupid proprietary Vuescan being the only option for scanning with the 9000 on linux (esp considering they've moved to a subscription model). The SANE backend is really quite bad and unmaintained. And all the clients really choke on full-res scans of medium format. So my plan is to write a SANE-less scan client for Nikon scanners that's all GPU-accelerated for the rendering, cross platform, and of course open source. I've honestly made some decent progress with the reverse engineer, i'm just going slow as I only have time on the weekend.

@thetalkingdrum

thetalkingdrum commented Jul 18, 2026

Copy link
Copy Markdown

That sounds wonderful! Yeah, I was under the impression that the SANE drivers left quite a lot to be desired.
I used to run Nikon Scan in a Windows XP VM for my LS-50, and that worked great. But once I got a 4000 and an 8000, I had to bite the bullet and buy a 2012 Mac mini just for the FireWire port so I could run Nikon Scan natively on win10.

Some good alternatives would be very welcome, though. The quality of ICE in Nikon Scan has always held me back from switching to anything else , but it looks like @rohanpandula might have that covered soon.

Exciting times! I'm looking forward to seeing your project :)

@activexray

Copy link
Copy Markdown

Yeah ICE has always seemed better than the alternatives, that's also why I preferred NikonScan. I was going to decompile their algorithm as the patent expired, which it looks like the other person made some progress on. I'll keep all of you posted on my progress!

@thetalkingdrum

Copy link
Copy Markdown

There are also some potentially low-hanging fruit that Kosma never managed to unlock in the firmware. But it seems like the whole reverse-engineering game has changed quite a bit over the last year or so.

For example, a firmware patch that makes physically modding the SA-21 unnecessary, or unlocking multi-sampling on the LS-50. Just throwing it out there :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants