[sane-devel] Plustek OpticPro A320E (07b3:1826): fifth version, after all — a defect the fourth one shipped

Tero Kankaanperä tero at terokankaanpera.fi
Mon Sep 21 08:21:32 BST 2026


Hello list,

Three weeks ago I wrote that the fourth version was the last one and 
that the
published driver was finished. It was not. This is the correction, and the
reason is worth telling properly, because it is the kind of defect that 
hides
from the AI who writes the driver and finds the person who uses it.

WHAT WENT WRONG, IN PLAIN TERMS

I had been using my own driver for actual work: scanning a stack of old 
comic
pages. Looking at the results as pictures rather than as measurements, I 
saw a
faint cyan cast along the left-hand edge of the page — the first few
millimetres, a little too little red, nothing dramatic. It was in seven 
of the
eight pages.

Two things about that were worse than they looked.

The first is that the driver caches its calibration. Calibrating takes a few
seconds, so it is done once and reused for the rest of the session. That is
normal and sensible. But it means that if a calibration comes out wrong, 
it is
not one bad scan — it is every scan you make for the next few hours, all of
them wrong in exactly the same way.

The second is that the calibration came out wrong roughly two times in 
three,
at random. Not on a particular mode, not after a cold start: the same 
command,
run twice in a row, would give a clean scan and then a tinted one. That
randomness is precisely why it survived my testing. I tested by 
measuring, and
when a measurement disagreed with the previous one I tended to look for what
I had changed rather than accept that the device had tossed a coin.

It turned out to be the same fault I had been chasing for weeks under 
another
name. I had a long-standing open item about the red channel reading two
distinct levels between otherwise identical runs, and I had written it 
down as
a small tonal wobble not worth blocking a release for. It was not a separate
curiosity. It was this, seen through a different instrument.

WHAT IT ACTUALLY WAS

The sensor clocks its odd and even pixels out through two separate taps. 
That
is ordinary. What is not ordinary is that during the shading calibration 
sweep
the two taps drift apart: one of them advances two pixels for every row 
of the
sweep, so by the end of a 48-row burst the two halves of the data are 
looking
at places 96 pixels apart.

The shading table is built by taking every second pixel — which is to 
say, one
of the two taps. Which tap the red channel ends up on is decided afresh at
every calibration. Green and blue always land on the same one; red is the
coin. When red lands on the drifting tap, its correction table is 
derived from
a strip of the sensor about 3 mm away from where it should be, red comes out
too dark at the origin edge and slightly too bright at the far one, and the
picture gets a cyan edge.

I can say all of that with numbers, and the technical section below 
does. What
I cannot say is *why* the drift happens. I know, by measurement, how to stop
it — but not what the device is doing. I decided to ship the fix anyway 
rather
than hold a user-visible defect hostage to more testing.

TWO THINGS THIS PACKAGE HAS BEEN SAYING THAT ARE NOT TRUE

While tracking this down I had to image the area of the glass that sits 
under
the scanner's frame, where the calibration actually happens. That settled a
question this package has been getting wrong since August, in the README, in
changes.md, in a comment in gl124.cpp, and — worst — in the comment field of
doc/descriptions/genesys.desc, which is where the SANE device list gets its
text:

   - I claimed this scanner has no internal white strip. It has one, 
under the
     frame at the home position, across the full width. My earlier 
measurement
     only covered the lid, which really is nearly black, and I 
generalised from
     it.
   - From that I told users they must lay a white target of at least 305 
mm on
     the glass before scanning, and that no standard paper size would 
do. That
     instruction was wrong and is withdrawn. Nothing needs to be on the 
glass.

Three measurements say so: gain calibration returns identical values 
with and
without anything on the glass; the strip has been imaged directly; and a 
white
sheet covering the whole bed moves the white calibration reading by 0.6 %
against a grey card that is four times darker in the image.

I am sorry for the bother this caused anyone who read the device list entry
and went looking for a 310 mm white card.

THE TECHNICAL PART

Two changes, both measured on the device, 20 x 20 mm at the origin, grey 
card:

   - The shading window now starts at the bed's origin and ends at its
     right-hand edge (shading pixels 292..5092 = 304.8 mm) instead of 
starting
     at the sensor's pixel 0 and running 5200. The starting point is what
     decides the drift: at pixel 0 one tap slips 2 px/row, 85 px over a 
48-row
     burst; from the bed's origin the same measurement reads +-17 px.
     Shortening the window alone does not do it — pixel 0 with a 4800-pixel
     width still drifts. The left 18.5 mm that this removes carried no 
usable
     white reference anyway (1.2 % of peak at its start).
   - The shading table is decimated as the mean of two neighbouring pixels,
     i.e. of both taps, instead of every second pixel. The mean is the same
     value whichever way the coin lands. At 300-800 dpi shading_factor 
is 1 and
     the existing parity filter already did this; those modes measured clean
     throughout, which is also why the defect only ever showed at 
100-200 dpi.

Result: the loaded gain R/B at the origin edge went from two populations
(0.852 and 1.012, no values between) to one (0.98..1.02), and the image's
red-minus-green step at the edge from -10.1 DN to +-0.5 DN. A full-width 
scan
shows no black tail at the right edge.

The calibration burst is also 32 rows now instead of 48, which is what the
vendor's driver uses. Five repeats at each of 32, 40 and 48 rows: 32 
gave the
smallest mean tilt (0.0042 against 0.0108 and 0.0114) and the smallest 
spread,
and the coefficient noise does not change (0.320 % against 0.321 %). 
Going the
other way is not free: above 48 rows the exposure per row falls during the
burst — at 79 rows the white level ends at 54 % of where it started — 
because
the row count selects the motor profile.

Patch: 21 files, +3671 / -37 as git apply --stat counts it, against master
fcaa30a7. It still applies cleanly, still builds warning-free under -Wall
-Wextra -pedantic, and still passes make check. Every change remains 
behind a
ModelId check except the genesys.desc comment above.

   https://terokankaanpera.fi/a320e/

If you already applied the fourth version, take this one: the defect is not
cosmetic on colour originals, and the cache makes it a whole session at a
time.



-- 
---
Tero Kankaanperä
https://terokankaanpera.fi




More information about the sane-devel mailing list