Первые баги с изображениями
Владелец купил компактный e-reader Xteink X3 и сразу же установил открытую прошивку CrossPoint. Устройство поддерживало четыре оттенка серого, что заинтересовало его экспериментировать с изображениями. Однако при попытке загрузить фотографию в оттенках серого он обнаружил проблему: тёмный серый цвет отображался как чёрный, а светлый серый был едва заметен. Он создал тестовое изображение с тремя оттенками вместо четырёх — казалось бы, проблема решена.
Но это раскрыло новую ошибку: в приложении-просмотрщике CrossPoint остатки предыдущего изображения проявлялись призрачным образом на более светлых участках экрана. На экране сна такой проблемы не было. Это указывало на две разные реализации рендеринга изображений с различающейся логикой.
В обоих случаях на фотографиях появились отчётливые вертикальные полосы, которых не было в исходном файле.
Не имея опыта в e-ink технологии и разработке для ESP32, он начал расследование с помощью GPT-6 Astra в Codex, загружая скриншоты экрана e-reader'а прямо в сессию.
Понимание проблемы
E-ink экран использует импульсы напряжения для перемещения чёрных и белых частиц пигмента, которые остаются на месте после отключения питания. Неполное обновление или недостаточное напряжение может оставить призрачные следы предыдущего изображения.
Для отображения изображения в оттенках серого CrossPoint сначала рисует чёрно-белую основу (даже серые пиксели начинают как чёрные), а затем выполняет импульсы напряжения, чтобы переместить выбранные пиксели частично к белому. Эта вторая стадия, называемая «nudge», создаёт оттенки серого, воздействуя на пиксели в течение разного времени.
Astra обнаружил, что средство просмотра делает быстрое чёрно-белое обновление и просто останавливается, никогда не выполняя серый nudge. Проблема была исправлена быстро.
С полосами было сложнее. Сначала Astra обвинил алгоритм Floyd–Steinberg, использованный для подготовки изображения сна. Другой алгоритм дал тот же результат. Затем он предположил, что проблема в waveform для nudge.
Однако они не могли согласиться, что именно они измеряют, что беспокоило исследователя — он не хотел зря расходовать мощность на фантомные артефакты. Когда он настоял, Astra сообщил о паттерне полос шириной в два пикселя. Это не имело смысла, поэтому он попросил аннотировать фотографию и обнаружил, что Astra выделил не полосы, а тонкую текстуру дизеринга.
Что нашла Astra через FFT
Использование быстрого преобразования Фурье (FFT) было весьма умным подходом: это почти идеальный инструмент для выделения и количественной оценки повторяющихся паттернов, которые трудно измерить глазом. Astra применила двумерное FFT к небольшим участкам фотографии и исходного изображения, найдя сильный повтор примерно на двух пикселях экрана в обоих.
К сожалению, дизеринг с распространением ошибок производит структуру самостоятельно — часто высокочастотный шум, создающий сильный сигнал в FFT. Astra выделила тонкий точечный паттерн Floyd–Steinberg, тот самый алгоритм, который она выбрала для подготовки изображения и обвинила в начале расследования. Более широкие полосы, на которые жаловался исследователь, появлялись только на устройстве. Когда он оспорил её оценку, она создала эту аннотацию, подтвердившую, что они смотрят на разные паттерны.
Измеримые полосы
Слегка расстроенный бесплодными гипотезами Astra, исследователь переключился на Fable 5.1 в Claude Code для другой точки зрения.
У него была интуиция, что ширина полос что-то означает, но даже идентификация полос не удалась Astra. И это действительно сложно, так как много источников вносят паттерны и шум:
- Собственный паттерн дизеринга изображения, который запутал Astra
- Всё, что вводит полосы
- Собственные физические характеристики экрана X3
- Фото смартфона этого беспорядка:
- Шум сенсора
- Переменный фокус в изображении
- Искажение линзы (нужны макросъёмки, чтобы захватить экран с 259ppi)
- Вариации освещения и экспозиции, артефакты обработки
- Размытие движения от дрожащих рук
Избежав наивного подхода к обработке изображений Astra, Fable написал код для усреднения яркости вниз по каждому столбцу. Используя скользящее окно высотой 200 пиксель, каждая точка становилась средней вертикальной полосы вокруг неё. Это усреднило текстуру дизеринга, в то время как разница в яркости, сохранившаяся вниз по столбцу, выжила. Широкие формы на фотографии остались, но полосы стали намного яснее:
Fable применила FFT к одномерному профилю яркости для измерения интервала и силы вертикального паттерна. Её первая оценка поставила полосы примерно на семь пикселей экрана врозь, но это было основано на предположении о масштабе фотографии.
Она вернула внимание на дизеринг, на этот раз внутри прошивки, предлагая, что повторяющиеся ошибки округления могли выстроиться в ряд для создания вертикальных полос. Когда исследователь упомянул, что его исходное изображение уже было дизеринировано, она посходила по этому пути 20 минут, но это оказалось ложной тревогой. Другое расследование задействовало Fable, взволнованную битовой глубиной изображения, но и это никуда не привело. По крайней мере она была более творческой в своих расследованиях, чем Astra?
Что было особенного в сером?
Он также экспериментировал с более простыми изображениями на устройстве. Удаление либо серого, либо паттерна заставило полосы исчезнуть:
| Изображение | Серые пиксели присутствуют | Соседние пиксели в разных состояниях | Полосы |
|---|---|---|---|
| Плоское серое поле | да | нет | нет |
| Чёрно-белый дизеринг | нет | да | нет |
| Серый дизеринг, оба метода | да | да | да |
Конкретная комбинация серых пиксель с соседями разного оттенка была тем, что вызывало проблему. Fable сопоставила небольшие блоки исходного изображения с фотографией, что наконец позволило ей увидеть, что светло-серые пиксели несут полосу — важная деталь, которая даже не была ясна исследователю из-за очень бледного тона светло-серых пиксель.
Эти доказательства теперь указали на то, как экран производит серый, через серый nudge. Код для этого живёт в freeink-sdk, аппаратной библиотеке, которую использует CrossPoint.
Fable изначально не хотела идти дальше: «Я не могу разработать или проверить изменение LUT отсюда». LUT — это таблица поиска, содержащая waveform для nudge. Когда исследователь указал, что X3 лежит на его столе и он может сфотографировать всё, что отобразит новая сборка, она вернулась с экспериментами, которые они могли бы запустить.
Необычный выбор X3 магнитного контактного зарядного устройства сработал в их пользу. Fable могла загружать и перезагружать устройство по USB, пока оно было подключено. Затем исследователь мог взять устройство, сфотографировать экран и положить его обратно без необходимости возиться с разъёмом USB-C.
Тестовый паттерн и второй баг
Существующий nudge длился семь циклов сканирования: каждый раз контроллер проходил по строкам панели, применяя следующий шаг последовательности напряжения к каждому пикселю. Они всё ещё думали, что интервал полос составляет около семи пиксель. Может ли синхронизация проявляться как пространственный паттерн? Изменение длительности waveform дало бы им что-то для сравнения.
Сначала им нужно было лучшее изображение для измерения, поэтому он предложил Fable создать тестовое изображение. Она создала паттерн с плоскими участками всех четырёх оттенков, комбинациями серого с чёрным или белым, шахматной доской и линиями, идущими в обоих направлениях.
Тестовое изображение резко ускорило прогресс. Fable знала, как изображение должно выглядеть, поэтому вариации в фотографиях и появление экрана стали заметны. Например, первоначальная оценка масштаба Fable ошибочно приняла особенность в спектре фотографии за пиксельную сетку экрана. С тестовым паттерном как линейкой, период полос оказался восемь пиксель, а не семь.
Исследователь предоставил необработанные DNG файлы из смартфона, а также обработанные фотографии. Сравнение показало, что обработка телефона преувеличивала амплитуду полос примерно на 70 процентов и смещала видимые уровни серого. После этого они использовали необработанные файлы. Fable разработала способ находить тестовые участки несмотря на изменения в кадрировании, перспективе и искажении линзы, поэтому она могла измерять каждую сборку и фото одинаково.
Они попробовали растягивать nudge с семи кадров на десять, затем версию, которая перемежала приводные импульсы с пауз. Ни один не влиял на полосы. Это исключило предложенную связь между количеством кадров и периодом полос.
Три, потом четыре оттенка серого
Тестовый паттерн также вернул проблему, которую они обходили ранее.
Их предположительно четырёхоттеночный reader определённо отображал три оттенка; это был не только возраст глаз исследователя. Тёмный серый был чёрным. Ему повезло наблюдать, как Fable это обнаружила, потому что она сообщила об этом лишь как мимолётный комментарий. Поняв значение её находки, он вмешался, остановил её и заставил копать глубже.
Fable проследила это до расхождения между CrossPoint и драйвером в том, как запрашивать тёмный серый. CrossPoint отправляет два бита на пиксель для nudge, выбирая одну из четырёх таблиц waveform. Его код для тёмного серого выбрал таблицу, которая ничего не делала. Предполагаемый тёмный серый приводной сигнал находился в другой таблице. Fable исправила ошибку и выпустила PR против freeink-sdk.
После исправления тёмный серый появился, и участок, который должен был быть тёмным серым, дизеринированным на чёрном, стал видимой крапинкой вместо сплошного чёрного квадрата. Но светло-серый приводной сигнал не изменился, и полосы тоже не изменились. Они случайно исправили другой баг, находясь на строительстве теста для первого.
Отсутствующий оттенок также делал текст шероховатым (помните упоминание о неравнодушном рендеринге текста?). CrossPoint сглаживает свой текст, поэтому пиксель, предназначенные для смягчения краёв с тёмным серым, вышли чёрными, что дало жирный текст. Восстановление этого оттенка улучшило текст по всему reader'у.
Более медленный способ рисования картины
Драйвер содержал другую waveform, которую они не пробовали: режим четырёхсерого изображения производителя, называемый XTH4. Он использовал гораздо более длинную последовательность импульсов для создания четырёх оттенков, занимая примерно секунду на обновление. Это была бы раздражающая задержка при каждом повороте страницы, но казалась разумной для открытия изображения или рисования экрана сна.
Таблица уже была в freeink-sdk, и драйвер другого reader'а использовал версию для изображений. Они могли просто попробовать её на X3 без изобретения waveform с нуля. Fable совсем не была уверена, что это что-то достигнет, но исследователь подтолкнул её вперёд.
Сначала нужно было решить проблему памяти. Nudge нужен только различать тёмный серый, светлый серый и «оставить этот пиксель в покое». Чёрный и белый могут использовать последнюю инструкцию, потому что первый проход уже нарисовал их. Более длинная waveform нужны отдельные инструкции для всех четырёх оттенков.
Драйвер другого reader'а решил это, сохраняя копию чёрно-белого изображения в RAM и объединяя его с данными серого. X3 использует ESP32-C3 примерно с 380 килобайтами RAM, и его самый большой свободный блок был всего 53 килобайта. При 528 на 792 пиксель, даже однобитовая копия экрана потребила бы 52 килобайта. Не было бы места для небрежного добавления другого буфера.
Fable предложила, чтобы CrossPoint рисовала данные в требуемом формате изначально. Средство просмотра изображений уже декодировало файл один раз для каждого прохода рендеринга; эти проходы могли производить два бита на пиксель, которые идентифицировали бы все четыре оттенка. Драйвер получит то, что ему нужно, без другого экрана данных.
После внедрения и загрузки всего этого, исследователь открыл изображение, которое начало расследование, и сказал Fable: «Полосы БОЛЬШЕ НЕ ВИДНЫ в моём исходном почему есть полосы? изображении».
Измерения согласились. Вариация столбца упала примерно с четырёх процентов диапазона чёрного-к-белому до одного процента. Пик на восьми пиксель полностью исчез из спектра частот.
Более длинная waveform также имела побочный эффект улучшения самих оттенков. Восстановление тёмного серого дало нам четыре уровня; это также потянула светлый серый дальше от белого (помните мои комментарии о том, что он очень бледный?). Тёмный серый немного темнее, чем хотелось бы, поэтому есть ещё настройка.
Получаем ли мы закрытие?
Итак, это полусвязное расследование: замена кода исправила проблему, но действительно ли мы знаем, почему это произошло с самого начала? Не совсем.
Вот его лучшее предположение, основанное на недавно приобретённых и всё ещё очень ограниченных знаниях об электрофоретических дисплеях. Строки e-ink панели переключаются драйвером ворот, травленным в стекле. Несколько чередующихся часов управляют воротами, каждый открывает каждый n-й ряд на немного другой интервал времени. Частица, приводимая сильно к чёрному или белому, нечувствительна к изменениям синхронизации. Пиксель, толкаемый частично к серому в течение трёх кадров, получает только заряд, который позволяет его временное окно строки, поэтому каждый восьмой ряд выходит немного светлее или темнее.
Когда столбец заряжается, это происходит одна строка за раз, и его напряжение должно двигаться только когда соседние строки имеют разные оттенки. Если серый пиксель следует чёрному, этот размах напряжения должен завершиться в нашем кратком и переменном временном окне. Плоское серое поле никогда не колеблется, в то время как чёрно-белое колеблется, но оптический ответ насыщается, поэтому синхронизация не имеет большого значения. Только когда у нас есть серый дизеринг, мы видим размах и чувствительность к синхронизации, и именно тогда мы получаем наши полосы.
Знает ли он, что это правда? Нет, но это соответствует электрической архитектуре и имеющимся у него доказательствам. Он мог бы преследовать это дальше, но был рад получить изображения без полос.
Счастливый пиксельный конец
Изменение драйвера стало freeink-sdk#95. Оба PR в freeink-sdk были объединены в течение часов. Исследователь отправил пару PR в CrossPoint тоже. Забавно, что кто-то другой обогнал его там, объединив почти идентичный набор изменений прямо перед тем, как его изменения были рассмотрены.
Он находит масштабы всего этого немного смешными. Через несколько часов после распаковки своего первого e-ink устройства он тестировал исправления для его waveform дисплея. Astra и Fable обрабатывали незнакомый код, инструменты сборки и скрипты измерений, в то время как исследователь мог сосредоточиться на том, что должны делать эксперименты, предоставлять основные подсказки, такие как использование FFT и тестовые паттерны, спорить и направлять экспериментальные ложные следы, и рассказывать, что действительно показывал экран.
Этот опыт возможности прыгнуть в неизвестное и немедленно добиться прогресса был довольно захватывающим. Хотя он не знал много об e-ink, CrossPoint или ESP32 в начале, за часы он многому научился. Это определённо разожгло его аппетит к большему количеству такого рода низкоуровневого хакинга устройств.