Skip to content
HN On Hacker News ↗

How my e-reader lost its stripes

▲ 226 points • 47 comments • by simonmic • 4w ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is human-written.

3 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 1 of 1
SEGMENTS · AI 0 of 1
WORD COUNT 1,606
PEAK AI % 3% · §1
Analyzed
Sep 14
backend: pangram/v3.3
Segments scanned
1 windows
avg 1606 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,606 words · 1 segments analyzed

Human AI-generated
§1 Human · 3%

I’ve been a lifelong reader, which has proved a fragile habit in our era of endless trivial distraction. While books and e-ink readers solve the problem of competition for attention, they can’t compete with a phone for convenience. That tiny device in my pocket has made reading both easier and more vulnerable to displacement whenever a notification pops up.A few days ago, I learned for the first time about a recent generation of tiny e-ink readers. Since they’re cheap, it was easy to give into curiosity: I bought an Xteink X3. Not only is it astonishingly tiny, I could immediately install the delightful open-source CrossPoint firmware. I was very quickly able to install a few books. I also appreciated being able to install custom fonts, though I was a little surprised by their indifferent rendering (foreshadowing…).CrossPoint is very configurable out of the box, so I converted a photo to a dithered black and white bitmap as my “device sleep” screen, and was pleased with how pretty this looked. When I read that the device supported 4 whole shades of grey, I was intrigued: would a greyscale image look better?Down the rabbit hole: first image bugsThis revealed what looked like a bug: the sleep screen was displaying greyscale images with very murky dark areas. Either my middle-aged eyes were finally failing me, or was dark grey rendering as black? I created a quick test image and verified that dark grey really was black, while light grey was extremely pale (almost white). Mildly annoying, but hardly unexpected on a cheap device, and easily worked around: let’s create a three-tone image!With less of the regenerated image containing large regions of black, I now saw a new bug: in the CrossPoint viewer app, the prior screen contents were still present in ghostly form (only on paler parts of the display, hence me failing to notice the first time around). However, on the same image, the sleep screen didn’t suffer from this problem. This suggested that there were two image renderers making different decisions, and the viewer app’s code was buggy.In both cases, though, the photograph had distinctive vertical stripes across it that were not present in the bitmap file.A phone photo of my tri-tone image. The fine vertical stripes are easiest to see in the background.Since I knew next to nothing about e-ink, ESP32 development, or CrossPoint, I started investigating in the usual late-2026 way, using GPT-6 Astra in Codex. I would capture the X3’s screen on my phone and drop images into my Codex session.An e-ink screen uses voltage pulses to move black and white pigment particles, which stay in place after the power is removed. An incomplete update or insufficient voltage can leave ghosted traces of the prior image behind.To display a greyscale image, CrossPoint first draws a black-and-white base in which even the grey pixels start out black. It then runs a short voltage-pulse waveform to move selected pixels partway towards white. This second stage, a “nudge,” produces dark and light shades of grey by driving those pixels for different amounts of time.Astra found that the viewer did a fast black-and-white update and simply stopped, without ever performing the grey nudge. It quickly fixed the problem.The stripes proved much more stubborn. Astra initially flailed, blaming the Floyd–Steinberg dithering it had used to prepare my sleep picture. A different algorithm made no difference. It then followed a lead down the stack, and flagged the nudge waveform as worth investigating.But we were struggling to agree on what artifact we were even looking at or measuring, which concerned me; I didn’t want to burn state-of-the-art tokens chasing phantoms. When I pushed, Astra dug in and reported a stripe pattern two pixels wide. That didn’t make any sense to me, so I asked it to annotate the photograph, and found that it had picked out fine dither texture instead of the bands I could see across the image.What Astra's FFT foundUsing a fast Fourier transform (FFT) here was quite clever: it’s an almost ideal tool to pick out and quantify repeating patterns that are hard to measure by eye. Astra applied a two-dimensional FFT to small patches of the photograph and the source image, and found a strong repeat at roughly two screen pixels in both.Unfortunately, error-diffusion dithering produces structure of its own, by its nature often high-frequency noise that creates a strong signal in an FFT. Astra had picked out the fine dot pattern of Floyd–Steinberg, the very algorithm it had chosen to prepare the image and blamed early in the investigation. The broader bands I was complaining about appeared only on the reader. When I challenged its estimate, it made this annotation, which confirmed that we were looking at different patterns.Astra’s annotation of the fine dither texture behind its two-pixel estimate.Making the stripes measurableSlightly frazzled by Astra’s hypotheses that were going nowhere, I switched to Fable 5.1 in Claude Code for another perspective.I had a hunch that the width of the stripes meant something, but even identifying the stripes had eluded Astra. And this isn’t easy, as a lot of sources introduce patterns and noise:The image’s own dither pattern that had tripped up AstraWhatever was introducing the stripesThe X3 screen’s own physical characteristicsA handheld phone photo of this mess:Sensor noiseVariable focus within an imageLens distortion (these have to be macro shots to capture the 259ppi screen)Lighting and exposure variations, processing artifactsMotion blur from my shaky handsWarned away from Astra’s naive image processing dead end, Fable wrote code to average the brightness down each column. For the view below, it used a sliding window 200 rows tall: each point became the average of a short vertical strip around it. This averaged away the dither texture, while a brightness difference that persisted down a column would survive. Broad shapes in the picture remained, but the stripes became much easier to see:A crop after applying the sliding vertical average. The broad shapes belong to the photograph; the fine vertical bands are the defect.Fable now used an FFT on a one-dimensional brightness profile to measure the spacing and strength of the vertical pattern. Its first guesstimate put the stripes roughly seven screen pixels apart, but this was based on a guess of my photograph’s scale.It returned its attention to dithering, this time inside the firmware, proposing that repeated rounding errors could line up to produce the vertical bands. When I mentioned that my source image was already dithered, it became more excited, but this ended up being a 20-minute false lead. Another investigation involved Fable getting worked up over the bit depth of an image, but this too led nowhere. At least it was being more novel in its investigations than Astra?What was different about grey?I had also been investigating much simpler images on the device. Removing either the grey or the pattern made the stripes disappear:ImageGrey pixels presentNeighbouring pixels in different statesStripesFlat grey fieldyesnononeBlack-and-white dithernoyesnoneGrey dither, using either of two methodsyesyesyesThe specific combination of grey pixels with neighbours of a different shade was what caused trouble. Fable matched small blocks of the source image to the phone photograph, which finally allowed it to see that light-grey pixels carried the stripe, an important detail that hadn’t even been clear to me due to the very pale tone of light-grey pixels.This evidence now pointed towards how the screen produced grey, via the greyscale nudge. The code for this lives in freeink-sdk, the hardware library CrossPoint uses.Fable was initially reluctant to go further: “I cannot design or validate a LUT change from here.” A LUT is the lookup table holding the nudge waveform. When I pointed out that the X3 was on my desk, and I could photograph whatever a new build displayed, it came back with experiments we could run.The X3’s odd choice of a magnetic contact charger worked in our favour here. Fable could flash and reboot the device over USB while it was plugged in. I could then pick the device up to photograph the screen and put it back down without ever needing to fiddle with a USB-C connector.A test pattern, and a second bugThe existing nudge lasted seven scan cycles: each time, the controller worked through the panel’s rows, applying the next step of the voltage sequence to each pixel. We still thought the stripe spacing was about seven pixels. Could the timing be showing up as a spatial pattern? Changing the waveform’s duration would give us something to compare.First we needed a better image to measure, so I suggested to Fable that it should generate a test image. It created a pattern with flat patches at all four shades, mixtures of grey with black or white, a checkerboard, and lines running in both directions.The test pattern. Flat patches establish the four shades; the patterned areas test how grey behaves beside other shades. The checkerboard is at the right of the third row.The test image made progress dramatically easier. Fable knew how the image should look, so variations in my photos and the screen’s appearance became possible to see and account for. For example, Fable’s original scale estimate had mistaken a feature in the photograph’s spectrum for the screen’s pixel grid. With the test pattern as a ruler, the stripe period turned out to be eight pixels, rather than seven.I supplied raw DNG files from my phone as well as processed photographs. Comparing the two showed that the phone’s processing exaggerated the stripe amplitude by about 70 percent and shifted the apparent grey levels. We used raw files after that. Fable worked out how to locate the test patches despite changes in framing, perspective, and lens distortion, so it could