[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