私の電子書籍リーダーがストライプを失った方法
How my e-reader lost its stripes (serpentine.com)
要約
筆者は、最近購入した小型電子書籍リーダーXteink X3で、ファームウェアCrossPointを使い、グレースケール画像を表示しようとした際に、奇妙な縦縞模様のバグに遭遇しました。AIアシスタントの助けを借りながら調査した結果、この問題は、グレースケール表示のために使用される特定の電圧パルス波形と、画像レンダリングの組み合わせに起因することが判明しました。
全文翻訳
How my e-reader lost its stripes
私は生涯にわたる読書家ですが、それは終わりのない些細な気晴らしに満ちた現代において、脆い習慣であることが証明されています。書籍や電子ペーパーリーダーは注意を引く競合の問題を解決してくれますが、利便性においてはスマートフォンに太刀打ちできません。私のポケットにあるその小さなデバイスは、通知がポップアップするたびに読書をより容易にし、また置き換えられやすくもしました。
数日前、私は初めて、最近の世代の小さな電子ペーパーリーダーについて知りました。安価だったので、好奇心に負けて購入しました。Xteink X3です。驚くほど小さいだけでなく、すぐに素晴らしいオープンソースのCrossPointファームウェアをインストールできました。すぐに数冊の本をインストールできました。カスタムフォントをインストールできることも気に入りましたが、その表示(伏線…)には少し驚きました。
CrossPointは最初から非常に設定可能なので、写真をディザリングされた白黒ビットマップにして「デバイススリープ」画面にし、それがどれほど綺麗に見えるかに満足しました。デバイスが4段階のグレースケールをサポートしていると読んだとき、私は興味をそそられました。グレースケール画像の方が良く見えるだろうか?
迷宮入り:最初の画像バグ
これはバグのように見えるものを示しました。スリープ画面は、非常に暗い領域が濁ったグレースケール画像を表示していました。私の年齢による目がついに衰えたのか、それともダークグレーが黒として表示されていたのか? 私は簡単なテスト画像を作成し、ダークグレーが本当に黒であり、ライトグレーが非常に薄い(ほとんど白)であることを確認しました。多少迷惑でしたが、安価なデバイスではほとんど予想外のことで、簡単に回避できました。3段階の画像を作成しましょう!
再生成された画像に大きな黒い領域が少なくなったことで、新しいバグが見えました。CrossPointビューアアプリでは、前の画面の内容が幽霊のように残っていました(表示の薄い部分にのみ、そのため最初のうちは気づきませんでした)。しかし、同じ画像でも、スリープ画面ではこの問題は発生しませんでした。これは、2つの画像レンダラーが異なる決定を下しており、ビューアアプリのコードにバグがあったことを示唆していました。
しかし、どちらの場合も、写真にはビットマップファイルにはなかった特徴的な縦縞がありました。
[画像: 3段階の画像を示したスマートフォンの写真。細い縦縞は背景で最もよく見えます。]
A phone photo of my tri-tone image. The fine vertical stripes are easiest to see in the background.
電子ペーパー、ESP32開発、またはCrossPointについてほとんど何も知らなかったので、私は2026年後半の典型的な方法で、GPT-6 AstraをCodexで使用して調査を開始しました。X3の画面をスマートフォンでキャプチャし、画像をCodexセッションにドロップしました。
電子ペーパー画面は、電圧パルスを使用して黒と白の顔料粒子を移動させ、電源が切れてもそれらは所定の位置に留まります。不完全な更新または不十分な電圧は、前の画像のゴーストのような痕跡を残す可能性があります。
グレースケール画像を表示するために、CrossPointはまず白黒のベースを描画します。これでは、グレーのピクセルでさえ最初は黒になります。次に、選択されたピクセルを白に部分的に移動させる短い電圧パルス波形を実行します。この2番目のステージ、「ナッジ」は、それらのピクセルを異なる時間だけ駆動することにより、濃淡のグレーを生成します。
Astraは、ビューアが高速な白黒更新を行い、グレーのナッジを実行せずに停止したことを発見しました。それはすぐに問題を修正しました。
縞模様ははるかに頑固であることが判明しました。Astraは当初、スリープ画像を準備するために使用したFloyd–Steinbergディザリングを非難して、手間取っていました。異なるアルゴリズムは違いを生みませんでした。次に、スタックを下にたどって、ナッジ波形を調査する価値があるとしてフラグを立てました。
しかし、私たちは見ている、または測定しているアーティファクトについて合意するのに苦労していました。これは私を心配させました。最先端のトークンを幽霊を追いかけるために無駄にしたくありませんでした。私が押し付けると、Astraは掘り下げ、2ピクセル幅の縞模様パターンを報告しました。それは私には全く意味がなかったので、写真を注釈付けするように頼み、それが私が画像全体で見ることができた帯ではなく、細かいディザテクスチャを拾い上げたことを発見しました。
Astra's FFTが見つけたもの
ここで高速フーリエ変換(FFT)を使用したのは非常に賢明でした。それは、目では測定が難しい繰り返しパターンを拾い上げ、定量化するのにほぼ理想的なツールです。Astraは、写真とソース画像の小さなパッチに2次元FFTを適用し、両方で約2画面ピクセルの強い繰り返しを発見しました。
残念ながら、エラー拡散ディザリングは、それ自体が構造を生成します。本質的に高周波ノイズであることが多く、FFTで強い信号を作成します。Astraは、調査の早い段階で非難し、画像の準備に使用したアルゴリズムであるFloyd–Steinbergの細かいドットパターンを拾い上げていました。私が不満を言っていたより広い帯は、リーダーでのみ現れました。その推定値に異議を唱えたとき、それはこの注釈を作成し、私たちが異なるパターンを見ていることを確認しました。
[画像: Astraによる注釈付きの写真。拡大されたパッチと輝度プロファイルがあり、約20画面ピクセルにわたって10個の細かいディザ間隔がマークされています。]
Astra’s annotation of the fine dither texture behind its two-pixel estimate.
ストライプを測定可能にする
Astraの的外れな仮説に少し疲れて、別の視点を得るためにClaude CodeのFable 5.1に切り替えました。
縞模様の幅が何か意味を持つのではないかという予感がありましたが、縞模様を特定することさえAstraにはできませんでした。そして、これは簡単ではありません。多くのソースがパターンとノイズを導入するためです。
- 画像自体のディザパターンがAstraを混乱させた
- ストライプを導入していたもの
- X3画面自体の物理的特性
- 手持ちのスマートフォンの写真:
- センサーノイズ
- 画像内の焦点のばらつき
- レンズの歪み(259ppi画面をキャプチャするにはマクロ撮影が必要)
- 照明と露出のばらつき、処理アーティファクト
- 私の震える手によるモーションブラー
Astraのナイーブな画像処理の行き止まりから警告されたFableは、各列の輝度を平均化するコードを書きました。以下のビューでは、200行の高さのスライディングウィンドウを使用しました。各点は、その周りの短い垂直ストリップの平均になりました。これにより、ディザテクスチャは平均化されましたが、列に沿って持続する輝度の違いは残りました。画像内の広い形状は残りましたが、縞模様ははるかに見やすくなりました。
[画像: スライディング垂直平均を適用した後のクロップ。広い形状は写真に属し、細い垂直バンドは欠陥です。]
A crop after applying the sliding vertical average. The broad shapes belong to the photograph; the fine vertical bands are the defect.
Fableはその後、1次元輝度プロファイルにFFTを使用して、垂直パターンの間隔と強度を測定しました。最初の推定値では、縞模様は約7画面ピクセル間隔でしたが、これは写真のスケールに関する推測に基づいています。
ディザリングに再び焦点を当て、今回はファームウェア内で行い、繰り返し丸め誤差が積み重なって垂直バンドを生成する可能性があると提案しました。ソース画像はすでにディザリングされていると私が言及すると、それはより興奮しましたが、これは20分間の偽のリードに終わりました。別の調査では、Fableが画像のビット深度について興奮しましたが、これも行き詰まりました。少なくともAstraよりも独創的な調査でしたか?
グレーで何が違ったのか?
デバイスでさらに単純な画像を調査していました。グレーまたはパターンを削除すると、縞模様は消えました。
| 画像 | グレーピクセルあり | 隣接ピクセルが異なる状態 | ストライプ |
|---|---|---|---|
| フラットグレーフィールド | はい | いいえ | なし |
| 白黒ディザ | いいえ | はい | なし |
| グレーディザ(2つの方法のいずれかを使用) | はい | はい | はい |
グレーピクセルと異なるシェードの隣接ピクセルの特定の組み合わせが問題を引き起こしていました。Fableはソース画像の小さなブロックをスマートフォンの写真と照合し、ついにライトグレーピクセルがストライプを運んでいることを見つけました。これは、ライトグレーピクセルの非常に薄いトーンのために、私にとっても明らかではなかった重要な詳細でした。
この証拠は、画面がグレーをどのように生成するか、つまりグレースケールナッジを介して生成するかを示していました。これに対するコードは、CrossPointが使用するハードウェアライブラリであるfreeink-sdkにあります。
Fableは当初、さらに進むことに消極的でした。「私はここからLUTの変更を設計または検証することはできません。」LUTはナッジ波形を保持するルックアップテーブルです。X3が私の机の上にあり、新しいビルドが表示するものを写真に撮ることができると指摘すると、実行できる実験を持って戻ってきました。
X3の奇妙な磁気接点充電器の選択は、ここで私たちのために有利に働きました。Fableは、USBに接続されている間、USB経由でデバイスをフラッシュして再起動できました。その後、USB-Cコネクタをいじる必要なしに、デバイスを手に取って画面を写真に撮り、元に戻すことができました。
テストパターン、そして2番目のバグ
既存のナッジは7つのスキャンサイクル持続しました。各サイクルで、コントローラーはパネルの行を処理し、各ピクセルに電圧シーケンスの次のステップを適用しました。私たちはまだ縞模様の間隔が約7ピクセルだと思っていました。タイミングが空間パターンとして現れる可能性はありますか? 波形の持続時間を変更すると、比較対象が得られます。
まず、測定のためにより良い画像が必要だったので、Fableにテスト画像を生成するように提案しました。それは、4つのシェードすべてにフラットなパッチ、グレーと黒または白の混合物、市松模様、および両方向の線を持つパターンを作成しました。
[画像: テストパターン。フラットパッチは4つのシェードを確立し、パターン化された領域はグレーが他のシェードの隣でどのように振る舞うかをテストします。市松模様は3行目の右側にあります。]
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.
テスト画像により、進捗は劇的に容易になりました。Fableは画像がどのように見えるべきかを知っていたので、私の写真と画面の外観のバリエーションを認識し、説明できるようになりました。例えば、Fableの元のスケール推定値は、写真のスペクトル内の特徴を画面のピクセルグリッドと間違えていました。テストパターンを定規として使用すると、縞模様の周期は7ピクセルではなく、8ピクセルであることが判明しました。
私はスマートフォンから生のDNGファイルと処理済みの写真を提供しました。両方を比較すると、スマートフォンの処理が縞模様の振幅を約70パーセント誇張し、表示されるグレーレベルをシフトさせることが示されました。その後、生のファイルを使用しました。Fableは、フレーミング、パースペクティブ、レンズの歪みの変化にもかかわらずテストパッチを見つける方法を理解したので、各ビルドと写真を同じ方法で測定できました。
ナッジを7フレームから10フレームに引き伸ばし、次にドライブパルスと休止をインターリーブするバージョンを試しました。どちらも縞模様に影響しませんでした。これにより、フレーム数と縞模様の周期との関連性の仮説が除外されました。
3つ、そして4つのグレーシェード
テストパターンは、以前に回避していた問題も再浮上させました。
[画像: 元の波形でのテストパターン。一番上の行の最初の2つのパッチは黒とダークグレーであるはずです。両方とも黒です。3行目の左側にある黒地のダークグレーのパッチも消えています。]
The test pattern under the original waveform. The first two patches in the top row should be black and dark grey. Both are black. The dark-grey-on-black patch at the left of the third row has disappeared too.
私たちのいわゆる4段階のリーダーは、間違いなく3段階を表示していました。これは私の老眼だけの問題ではありませんでした。ダークグレーは黒でした。Fableがそれを見つけたとき、私は偶然見ていましたが、それは単なる通りすがりのコメントとして報告されただけでした。その発見の重要性を理解し、私は介入してそれを止め、さらに掘り下げるようにさせなければなりませんでした。
Fableはこれを、CrossPointとドライバーの間でダークグレーを要求する方法についての不一致にまでたどりました。CrossPointは、ナッジのためにピクセルあたり2ビットを送信し、4つの波形テーブルのいずれかを選択します。ダークグレーのコードは、何も行わないテーブルを選択していました。意図されたダークグレーのドライブは、別のテーブルにありました。Fableはその間違いを修正し、freeink-sdkに対してPRを発行しました。
それが修正されると、ダークグレーが表示され、黒地にディザリングされるはずだったダークグレーのパッチは、ソリッドブラックの四角形ではなく、目に見える斑点になりました。しかし、ライトグレーのドライブは変わらず、縞模様も変わりませんでした。最初のバグを修正するためのテストを作成しながら、別のバグを機会に修正しました。
失われたシェードは、テキストが粗く見える原因にもなっていました( indifferent text rendering の言及を覚えていますか?)。CrossPointはテキストをアンチエイリアスするため、ダークグレーでエッジを滑らかにするためのピクセルが黒になり、太いテキストになっていました。そのシェードを復元することで、リーダー全体のテキストが改善されました。
画像を描画するより遅い方法
ドライバーには、試していなかった別の波形がありました。メーカーの4階調画像モード、XTH4と呼ばれるものです。それは、4つのシェードを生成するために、はるかに長いパルスのシーケンスを使用し、更新に約1秒かかりました。それは各ページめくりで迷惑な遅延になるでしょうが、画像を開いたりスリープ画面を描画したりするには妥当に思えました。
テーブルはすでにfreeink-sdkにあり、別のリーダーのドライバーは画像のためにそのバージョンを使用していました。波形をゼロから発明することなく、X3でそれを試すことができました。Fableはこれが何も達成しないだろうと全く確信していませんでしたが、私はそれを前進させるように促しました。
まずメモリの問題を解決する必要がありました。ナッジは、ダークグレー、ライトグレー、および「このピクセルをそのままにする」を区別するだけで済みます。最初のパスがすでに黒と白を描画しているため、黒と白は最後の指示を共有できます。より長い波形は、4つのシェードすべてに個別の指示が必要です。
他のリーダーのドライバーは、黒と白の画像のコピーをRAMに保持し、それをグレーデータと組み合わせることでこれを解決しました。私のX3は約380キロバイトのRAMを持つESP32-C3を使用しており、その最大の空きブロックはわずか53キロバイトでした。528 x 792ピクセルでは、画面の1ビットコピーでさえ52キロバイトを消費します。別のバッファを気軽に簡単に追加する余地はありませんでした。
Fableは、CrossPointが最初から必要な形式でデータを描画するように提案しました。画像ビューアはすでにレンダリングパスごとにファイルをデコードしていました。それらのパスは、4つのシェードすべてを識別するピクセルあたり2ビットを生成できます。ドライバーは、画面いっぱいのデータをもう1つ追加することなく、必要なものを受け取ります。
これらすべてが実装されフラッシュされた後、私は調査を開始した画像を開き、Fableに言いました。「ストライプは、元のなぜストライプがあるのか?画像ではもう見えなくなりました。」
[画像: 私のX3上の写真、変更前と変更後。線の下は元のグレー波形を使用し、上は新しいものを使用しています。]
The photograph on my X3, before and after. Below the line it uses the original grey waveform; above it uses the new one.
測定値も一致しました。列の変動は、黒から白の範囲の約4パーセントから1パーセントに減少しました。8ピクセルでのピークは、周波数スペクトルから完全に消えました。
[画像: 黒から白の範囲に対する列の輝度。8画面ピクセルごとにグリッド線があります。上のプロットの規則的な振動は、下のプロットでは消えています]
Column brightness relative to the black-to-white range, with gridlines every eight screen pixels. The regular oscillation in the upper plot disappears in the lower one
より長い波形は、シェード自体を改善するという嬉しい副作用もありました。ダークグレーを復元したことで4つのレベルが得られました。これにより、ライトグレーも白からさらに遠ざかりました(それが非常に薄いという私のコメントを覚えていますか?)。ダークグレーは私が望むよりも少し暗いので、まだ調整が必要です。
[画像: 元のファームウェア下、ダークグレー復元後、およびより長い波形下での測定されたシェード。点線は比較のために均等に間隔を空けた輝度レベルを示しています。]
Measured shades under the original firmware, after restoring dark grey, and under the longer waveform. The dotted lines mark evenly spaced brightness levels for comparison.
解決しますか?
したがって、これは半ば満足のいく調査です。コードを置き換えることで問題は解決しましたが、なぜそれが最初に発生したのかを本当に知っていますか? まだ完全ではありません。
私の新しく獲得した、そしてまだ非常に限られた電子泳動ディスプレイの知識に基づいた、私の最良の推測は次のとおりです。電子ペーパーパネルの行は、ガラスにエッチングされたゲートドライバーによって切り替えられます。少数のインターリーブされたクロックがゲートを駆動し、各クロックはn番目の行をわずかに異なる時間間隔で開きます。黒または白に強く駆動される粒子は、タイミングの変動に鈍感です。3フレームにわたってグレーに部分的に駆動されるピクセルは、その行の時間ウィンドウが許可する電荷のみを取得するため、8行ごとに少し明るくなるか暗くなります。
列が充電されるとき、これは一度に1行ずつ行われ、その電圧は連続する行が異なるシェードである場合にのみ移動する必要があります。グレーピクセルが黒の後に続く場合、その電圧スイングは、私たちの短く変動する時間ウィンドウ内で完了する必要があります。フラットなグレーフィールドは決してスイングしませんが、白黒はスイングしますが、光学応答は飽和するため、タイミングはそれほど重要ではありません。グレーディザがある場合にのみスイングとタイミングへの感度が見られ、そこでストライプが発生します。
これが真実だと知っていますか? いいえ、しかしそれは電気的アーキテクチャと私が持っている証拠に適合します。さらに追求することもできましたが、ストライプのない画像に安心しました。
幸せなピクセル化された結末
ドライバーの変更はfreeink-sdk#95になりました。私のfreeink-sdkのプルリクエストは両方とも数時間以内にマージされました。クロスポイントにもいくつかのPRを提出しました。面白いことに、誰かが私の変更がレビューされる直前に、ほぼ同じ変更セットをマージして私を出し抜きました。
これらすべてにかかった時間の短さは少しばかげています。最初の電子ペーパーデバイスを開梱してから数時間後に、その表示波形の修正をテストしていました。AstraとFableは、見慣れないコード、ビルドツール、測定スクリプトを処理しましたが、私は実験が何をすべきかに集中し、FFTやテストパターンの使用のような主要なヒントを提供し、調査の行き詰まりを回避し、画面が実際に何を表示したかを共有できました。
未知の領域に飛び込み、すぐに進歩できたこの経験は非常にエキサイティングでした。電子ペーパー、CrossPoint、またはESP32について最初はあまり知らなかったにもかかわらず、数時間以内に多くを学びました。これは間違いなく、このような低レベルのデバイスハッキングへの私の食欲をそそりました。