Снимок сделан на пароме Сан-Франциско — Окленд в феврале 2026 года (изображение в оттенках серого, 56 894×2048 пикселей); прокрутите для увеличения, зажмите и тащите для перемещения.

Больше снимков представлено в галерее.

Проект был представлен в виде доклада на EMFcamp 2026.

Что тут вообще изображено?


Процесс съёмки изображения, подобного снимку контейнерного порта выше.

Камера направлена из окна движущегося транспорта и постоянно захватывает одну вертикальную линию — что-то вроде тех серых полос на диаграмме, только намного тоньше. Пока камера движется, то, что она видит, постоянно меняется. Если захватывать строки с камеры достаточно быстро и склеивать их между собой, получается цельное изображение. На практике всё немного сложнее, и добиться хорошего результата оказалось довольно непросто, но идея именно такая.

Предыстория и предшественники

В 1990-х годах сенсоры цифровых камер ещё не догнали по размеру и эффективному разрешению средний и крупный формат фотоплёнки, поэтому появились цифровые сканирующие задники. Они позволяют получить изображение высокого разрешения без гигантской матрицы пикселей, перемещая одну строку пикселей (или три строки для цвета) по всему кадру. За прошедшие годы сенсоры сильно выросли в размере (сейчас существует даже модель, покрывающая крупный формат 4x5"), но для больших форматов такой подход всё ещё дешевле, чем гигантская матрица.

Мысль собрать собственный сканирующий задник для крупноформатной камеры давно не давала покоя, но дело не двигалось — казалось слишком сложным сделать что-то, что нормально крепится к камере. (Покупка готового решения теоретически была вариантом, но экземпляры из 1990-х до сих пор продаются на eBay за тысячи долларов и требуют воссоздания вычислительной среды того же возраста.) В конце прошлого года, посмотрев видео о сборке среднеформатной сканирующей камеры Gigawipf, внезапно пришла идея: а что если двигать не задник, а всю камеру, оставляя объект неподвижным? Решение было принято попробовать.


Загрузка плёнки в крупноформатную камеру на вершине горы в Вермонте — потому что фотографировать «нормальным» способом слишком просто. (Снимки с той поездки можно посмотреть здесь.)

Похожие проекты уже существовали: Scannoramic, эксперимент Джона Хайкербайкера, переворот стационарной камеры Дэниела Лоуренса Лу и интересные плёночные снимки Мартина Либшера. Но результаты этих проектов казалось можно улучшить — учёт скорости движения и более чистая картинка не выглядели такой уж сложной задачей.

Слит-скан дивана

В ту же ночь, когда возникла идея «большого сканера», удержаться от попытки не удалось. Ехать куда-то на поезд было поздно, поэтому вместо этого сканированию подвергся собственный диван.

Телефон закрепили на офисном кресле и медленно катили его, снимая видео. Затем на скорую руку был написан код (который автор предпочитает не публиковать, дабы защитить читателей от него), извлекающий крайний левый столбец пикселей («щель») каждого кадра и склеивающий их в изображение.

В комментариях к коду оказались строчки из песни "Future Me Hates Me" группы The Beths, что стало почти пророчеством, когда позже пришлось писать постпроцессор для следующей версии камеры на основе того же кода и проклинать собственные решения.


Похоже на диван, но изображение сплюснуто, а картина на стене вообще нечитаема. Явно можно лучше.


Поэкспериментировав с постобработкой и продублировав каждый столбец, изображение выглядит менее сжатым, но всё равно каша — из-за того, что кресло катили без сколько-нибудь постоянной скорости.

С самого начала было понятно, что скорость нужно как-то измерять, но была наивная надежда обойтись без точных измерений и просто подгонять параметры на глаз. Это изображение показало, что даже небольшие изменения скорости сильно влияют на результат — первое знакомство с тем, насколько мучительной окажется борьба со скоростью.

Для следующего эксперимента была поездка на оранжевой линии MBTA. Старый телефон приклеили скотчем к сиденью, чтобы использовать его акселерометр, а текущий телефон держали у окна с частотой кадров, выкрученной на максимум — 60 fps.

Данные акселерометра оказались малополезны, а после интегрирования для получения скорости — тем более.
Если память не изменяет, ось y соответствовала направлению движения поезда, но данные настолько шумные, что к концу поездка выглядела так, будто поезд поехал назад.

Результат получился любопытным, хотя для по-настоящему разборчивого изображения нужно явно больше строк.

Готовясь к EMFcamp, в программе конференции нашёлся другой доклад — Тима Джейкобса, более известного как mitxela — также посвящённый слит-скан камерам, что вызвало опасение о случайном повторении чужого проекта. (После собственного доклада Тим подошёл сам и признался, что переживал о том же.) Его доклад начинался похожим образом — с извлечения щели из видео, — но затем он пошёл дальше и сделал по-настоящему крутые и психоделические анимации, перебирая все возможные позиции щели для заданного видео.

Промышленная линейная камера

Источником большего числа строк в секунду стала камера Basler ruL2048-19gm, изначально предназначенная для наблюдения за быстро движущимися конвейерными лентами. Странно написанное название отражает способность считывать сенсор 1×2048 пикселей почти 19 000 раз в секунду.

За такие возможности приходится платить: новые модели младшей линейки этого производителя стоят около $700. К счастью для бюджета, экземпляр нашёлся на eBay за десятую часть этой суммы.

Плата взимается и светом: из-за очень короткого времени захвата (минимальная выдержка — 1/100 с) камере требуется очень много света. Съёмка возможна только днём, а большинство станций и туннелей — кроме самых ярко освещённых — недоступны для съёмки.

К удивлению — после опыта работы с закрытым проприетарным софтом других вендоров — Basler позволил без каких-либо контрактов на поддержку или подтверждения покупки скачать SDK. Более того, самая новая версия SDK до сих пор поддерживает эту камеру 2013 года выпуска, что удивляет меньше, но всё же приятно.

Камера связывается с компьютером по гигабитному Ethernet-каналу, и программное обеспечение находит её автоматически, если соответствующий интерфейс настроен на адреса APIPA (169.254.0.0/16). Можно было задать статические адреса на обеих сторонах, но поскольку камера используется только одна за раз, менять что-то не было смысла.

С удивительно небольшим количеством проклятий в адрес SDK — если не считать претензий к использованию времени выдержки вместо скорости выдержки и путаницы с тем, что здесь считается «кадром» — была собрана программа, которая захватывает буферы пикселей и записывает их на диск.


Первое изображение, полученное с камеры с помощью собственного кода — выглядит неплохо для съёмки просто «с руки».

Конструкция и механика

Чтобы возить камеру в поездах, не имея трёх рук для её удержания, потребовался способ закрепить её на штативе. В итоге был спроектирован довольно утилитарный корпус с резьбовой вставкой в нижней части, который на 3D-принтере распечатала подруга Брук. Заказ деталей стал поводом наконец-то сделать заказ на McMaster-Carr и почувствовать себя настоящим инженером.

Первая попытка провалилась — оказалось, существуют такие вещи, как «производственные допуски», которые были полностью упущены из виду.


Ой, получилось слишком мелко.


Оглядываясь назад, стоило прикрепить датчики чем-то другим, а не синим малярным скотчем — но держится он неплохо.
По часовой стрелке расположены следующие платы:

Объектив спереди — Vivitar 28mm f/2.8, уже имевшийся для обычной камеры, с переходником с байонета Pentax K на резьбовое крепление C-mount камеры. Поскольку некоторые объекты съёмки довольно высокие, угол обзора этого объектива подошёл неплохо.

Всё устройство питается от USB-C батарейного блока, а к ноутбуку тянутся кабели Ethernet и USB, так что в работе это выглядит как настоящий клубок проводов.

С подключёнными датчиками наконец можно было их опробовать.

Оба изображения — один и тот же захват при покачивании камеры вперёд-назад за окном, но верхнее — исходное изображение, а нижнее учитывает данные акселерометра. Как видно, использование акселерометра делает картинку заметно ближе к нормальной и менее растянутой. (Подробнее о том, как это работает, — в разделе про постобработку ниже.)

Попытки в Бостоне

Когда всё было собрано, пришло время испытать камеру в поезде.


Первой стала оранжевая линия MBTA — самая близкая, но результат, как видно, оставлял желать лучшего. Просматривать поток с камеры было мучительно, поэтому выдержку пришлось выставлять «на глазок» — и угадать не удалось. Написанный код постобработки работал плохо, изображение выглядело странно растянутым и сжатым местами.


В другой раз, в более хорошую погоду, с выдержкой повезло больше, хотя фокус, кажется, немного не удался. В отличие от попытки со смартфоном, текст на станционных указателях теперь вполне читаем — значит, строк захватывается достаточно.


Особенно удачным получился снимок моста Лонгфелло из Бостона в Кембридж. Это изображение доступно в галерее для более детального просмотра.

Захват (в излишних подробностях)


Во время первых съёмок в Бостоне для предпросмотра использовалась утилита от производителя камеры под названием Pylon. Чёрная горизонтальная полоса — это всё, что можно было увидеть от изображения в любой момент времени, да ещё и повёрнутая на 90° от привычного вида. Настроить экспозицию в ней, отобрать управление камерой у утилиты и успеть запустить собственный код до того, как поезд снова тронется, — сущая мука, с которой пришлось разбираться.

Первая попытка собственного GUI использовала OpenCV highgui, что не сработало для этой задачи. Библиотека требует задержку 1 мс после каждого кадра, что нормально для медленных камер, но приводило бы к потере целых 4 строк (по 250 мкс каждая при обычно используемых скоростях затвора) на каждый кадр отображения (256 строк).

В итоге выбор пал на Dear ImGui, которая хорошо вписалась в уже существующий цикл захвата кадров. Из примерно двух десятков поддерживаемых бэкендов был выбран GLFW («girl love for workgroups», как пошутил в переписке один друг) в сочетании с OpenGL3 — возможно, именно из-за этой шутки, хотя точно сказать сложно.


Большая часть GUI была написана за одну бессонную ночь в Торонто, когда поворот изображения ощущался как самая сложная задача в информатике. (Наверняка есть что улучшить в выбранной реализации, но пока работает вполне сносно.) К сожалению, снимки, сделанные в Торонто, толком не получились, но хотя бы экспозиция была верной.


Добавление гистограммы изображения принесло несколько занятных багов.

Получение данных с акселерометра тоже оказалось непростой задачей. Первая версия отправляла показания как текст по последовательному порту, что оказалось очень тяжёлой нагрузкой для микроконтроллера. (Преобразование чисел с плавающей точкой в строки, а затем сборка строк — весьма дорогая операция, даже на относительно мощном SAMD21 с целыми тридцатью двумя битами.) Преобразования были перенесены на ноутбук, у которого хватает мощности справляться с этим без проблем, но это принесло свои сложности.


Очень утомительная сессия отладки.

Показания акселерометра передавались как «сырые» числа с плавающей точкой, а данные GPS оставались в формате NMEA-предложений, и переключение между ними требовало отправки фиксированных байтовых последовательностей с надеждой, что ничто внутри данных не будет неверно интерпретировано как эти последовательности. (В NMEA-предложении не должно встретиться 0x11 0x11 0x11 0x11 — маркер начала данных акселерометра, но данные акселерометра теоретически могут содержать 0x22 0x22 0x22 0x22 — маркер начала NMEA-строки.)

Также случались проблемы, когда несвоевременный сброс буфера последовательного порта портил результаты целого дня съёмок. К счастью, съёмка велась на линии Маттапан в Бостоне, куда легко вернуться и попробовать снова.
«Шов» на изображении — момент, когда все данные с последовательного порта пропали на пол секунды, что для линейной камеры — целая вечность. Программа продолжала ждать следующее показание акселерометра, которое так и не пришло, потому что буфер последовательного порта был переполнен.

See It, Say It, Sorted

В собранном виде камера выглядит как подозрительная конструкция, а человек, который её использует, выглядит не менее подозрительно.


Обычно камеру не удерживает столько скотча, но в ту поездку в Монреаль пластина крепления штатива была случайно забыта дома.
Доверили бы вы такому человеку принести странное оборудование в свой поезд?

Несмотря на историю переусердствования бостонской полиции с безобидными электронными проектами, меньше всего опасений вызывает именно MBTA. Местные жители обычно заняты своими делами и никогда не вызывали полицию. Сами полицейские в поездах бывают редко, предпочитая беспокоить людей на станциях.

С другими городами такого опыта меньше, поэтому камеру берут с собой только в компании друга, который может присмотреть за обстановкой (а иногда и послушать диспетчерское радио).


Пока дело ограничивалось только «увидели», не дошло до «сказали» и «разобрались». Остаётся надеяться, что это не изменится по мере расширения географии съёмок.

В поездке в Монреаль охрана Gare Centrale остановила съёмку, сообщив, что штативы запрещены, и спросив на «франгле», идёт ли запись видео или делается фотография. Вместо попытки ответить на этот философский вопрос на языке, которым не владеешь, было проще несколько раз сказать «désolé» и убрать штатив — этого оказалось достаточно.

Снимки из Монреаля доступны в галерее (изображения 2 и 3).

Ад постобработки

Захват изображения и данных акселерометра оказался лёгкой частью по сравнению с постобработкой и приведением снимков к приличному виду.

Камера захватывала около 4000 строк в секунду, так что в каждом захвате оказывалось намного больше строк, чем требовалось, и приходилось решать, какие из них действительно важны.


Что случается, если взять слишком мало строк (автомагистраль 10 в Броссаре, Квебек, из окна REM A)
Слишком резкие переходы между строками выглядят искусственно и неестественно, как видно у линии воды на этой обложке-переделке ранней версии снимка окландского парома.

Чтобы решить, какие строки использовать, в итоге использовалась скорость, измеренная акселерометром, но это принесло несколько проблем.

Во-первых, акселерометры измеряют не скорость, а ускорение — скорость изменения скорости. Взяв интеграл, можно получить скорость, но только относительно некоего начального значения. Обычно можно предположить, что начальная скорость на станции равна нулю, но это не всегда верно. Если она не нулевая, точного значения узнать неоткуда — приходится подбирать на глаз, пока результат не «пахнет» правильно.

Во-вторых, как показано на диаграмме, используемый акселерометр измеряет данные с относительно низкой частотой. Камера захватывает строки примерно в 4 раза быстрее, поэтому нескольким строкам подряд приходится делить одно значение скорости. Частота измерений тоже не очень стабильна, поскольку код микроконтроллера работает не так быстро, как хотелось бы, что может вызывать неровности в итоговом изображении. Количество измерений ускорения также влияет на точность интеграла, что добавляет новых проблем.

Ранее упоминалось о GPS-приёмнике на камере — он действительно был установлен, но пользы принёс мало. Сигнал не удавалось получить на большинстве поездов, а когда получалось — частота обновления составляла лишь 10 раз в секунду, что покрывает целых 400 строк с камеры. Если бы модуль работал более стабильно, его можно было бы использовать для коррекции ошибки интегрирования с помощью фильтра Калмана, но это задача на будущее, когда данные GPS станут лучше.

Даже при идеальном измерении скорости остаётся проблема параллакса: объекты ближе к камере кажутся движущимися быстрее, чем далёкие. Это не связано с оптической фокусировкой, которая обычно выставлена на бесконечность.

Эту проблему можно решить, изменяя, сколько дистанции представляет каждый пиксель. Меньшие значения подчёркивают объекты ближе к камере, большие — делают более заметным фон.

Все эти изображения одного размера (10 000 пикселей в ширину на 2048 в высоту, масштабированы под ширину браузера), и каждое включает всё, что видно на предыдущем, за счёт покрытия большей части захвата. Единицы измерения условны и не соответствуют реальному расстоянию (можно было бы выразить в метрах на пиксель, но смысла в этом не увиделось).

Камера и программа не знают, на чём именно нужно «сфокусироваться», поэтому это художественное решение принимается вручную для каждого сегмента изображения, а затем сегменты склеиваются, чтобы получить снимки в галерее. Для каждого сегмента подбирались разные значения дистанции на пиксель и начальной скорости, а затем всё собиралось в GNU IMP для получения финального изображения. Собранные изображения часто оказывались слишком велики для максимального размера JPEG 65 535×65 535, поэтому использовался старый добрый формат TIFF. (Спецификация PNG теоретически допускает изображения такого же размера, но имеющийся софт лучше справляется с крупными TIFF, чем с крупными PNG.)


Заметки о значениях дистанции на пиксель (u) и начальной скорости (v) для каждого сегмента нескольких изображений.

Программа, учитывающая данные акселерометра для каждой строки изображения, называется grindstone — она «перемалывает» многогигабайтные необработанные захваты в компактные готовые изображения. Первая версия была написана на основе того самого плохого слит-скан кода со сканирования дивана и работала крайне медленно, обрабатывая минутный захват часами. Часто после многочасовой работы сохранение проваливалось из-за того, что итоговое изображение превышало лимиты JPEG, а отлаживать это было настоящей мукой.

В итоге получилось увлечь подругу Мэдди идеей переписать grindstone в идиоматичном стиле NumPy, чтобы сделать происходящие математические операции более понятными (она настаивает, что все операции уже были в оригинале, а её изменения были скорее «превращением этого во что-то волшебное»). Позже Мэдди разбила эту версию на «возможно, немного переусложнённый» конвейер из нескольких стадий, что упростило эксперименты и подмену разных операций, стратегий вывода и так далее. Благодаря её помощи стало намного проще пробовать разные комбинации параметров и получать результаты, которыми можно быть довольным.

Цветовой ад

В апреле, во время поездки на линии Маттапан вместе с другом Ари, когда на деревьях появлялась листва, снимки не получились из-за бага в софте захвата (см. раздел «Захват»), и вернуться туда пока не удалось. Но эта поездка навела на мысль, что цветные снимки с линейной камерой могли бы смотреться эффектно, особенно осенью.

Просматривая eBay поздно ночью, удалось найти очень выгодное предложение на цветную линейную камеру того же поколения, что и уже имевшаяся монохромная (Basler ruL2098-10gc, 3×2098 пикселей на скорости около 10 000 строк в секунду). После небольшой разборки (камера пришла в корпусе, в котором использовалась на какой-то заводской линии) и замены крепления объектива камера была механически готова.

На корпус нанесены красная, зелёная и синяя полосы, чтобы различать камеры без снятия объектива и разглядывания мелкого текста на этикетке.

Софт захвата не потребовал слишком больших изменений, хотя пришлось поправить ряд предположений о размере строки и переделать поворот изображения в GUI.

Процесс настройки цветного захвата.

Благодаря очень модульной архитектуре, в которую Мэдди переписала grindstone, добавить поддержку цвета оказалось не слишком сложно, хотя пришлось разбираться с несколькими странными на вид багами.


Поезд ехал так медленно и неравномерно, что ошибка интегрирования накопилась настолько, что grindstone посчитал, будто камера едет назад, и начал скакать по разным предыдущим точкам захвата.

Когда захват и обработка изображений в целом заработали, стали видны новые проблемы. Самая заметная — листва получается заметно светлее, чем должна быть.
Это происходит из-за того, что цветная камера чувствительна к инфракрасному свету на всех трёх каналах. (Если бы чувствительность была только на красном канале, листва выглядела бы красноватой, но сочетание ИК на всех трёх каналах с сильным видимым зелёным даёт зеленовато-белый оттенок на этом снимке.) Монохромная камера тоже чувствительна к ИК, но там это неважно — там всего один канал, и видимый свет полностью его перекрывает.

(Диаграмма взята из руководства к камере)

Стоит признать, что при правильном освещении этот эффект смотрится довольно эффектно. Снимок, сделанный в Манчестере-у-Моря севернее Бостона, одновременно и монохромный, и цветной. (Про цветные полосы на фоне — чуть ниже.)


Проблема была решена с помощью УФ- и ИК-отсекающего фильтра, пропускающего только свет в диапазоне 400–700 нм — это достаточно близко к видимому человеческим глазом спектру, чтобы всё выглядело правильно. Это первый крупный снимок с этим фильтром, и цвета потребовалось поправить в постобработке лишь минимально.


Также был опробован фильтр, пропускающий только свет длиннее 720 нм (с ним и ИК-чувствительной плёнкой ранее получались весьма интересные результаты), и опыты с ним планируется продолжить.

Следующая проблема — на некоторых объектах появляются странные красные, зелёные и синие полосы, особенно у объектов, расположенных дальше от камеры или движущихся быстрее.
Оказалось, что это заложено в самой конструкции сенсора этой камеры. Красный, зелёный и синий — отдельные вертикальные строки (а не фильтр Байера), поэтому они не могут увидеть одно и то же в один момент времени. Полосы возникают, когда объект попадает в поле зрения лишь одной из строк, и особенно заметны на ярких объектах. Полосы диагональные, а не строго вертикальные, потому что сама камера не выставлена идеально вертикально. (Добиться идеальной вертикали в движущемся поезде можно только до определённого предела.)


Из руководства к камере: производитель приводит формулы для компенсации этого эффекта на основе коэффициента оптического увеличения объектива и точной скорости, но оценки (относительной) скорости объекта съёмки нет.

Коррекция для конкретного объекта делается сдвигом красного и синего каналов относительно зелёного. Поскольку строки расположены на равном расстоянии друг от друга, можно сдвигать на одну и ту же величину в противоположных направлениях, вместо того чтобы измерять отдельные смещения для каждого канала. Теоретически можно было бы вычислять величину сдвига по корреляции яркостных смещений между каналами, но пока это делается вручную.
Разделение каналов видно на мачтах парусника, скорректированное сдвигом красного канала на 10 пикселей вправо и синего — на 10 пикселей влево. Цветные полосы на фоне всё равно заметны, поскольку фон намного дальше от камеры и, соответственно, имеет более высокую угловую скорость; их тоже можно было бы скорректировать, но тогда сам парусник выглядел бы намного хуже.

Отображение

Показ и передача снятых изображений оставались проблемой на протяжении всего проекта. Большинство программ на компьютере плохо справляются с такими размерами файлов, а самым надёжным инструментом просмотра оказался GNU IMP, что кажется некоторым излишеством. Мессенджеры, которыми пользуются друзья, также плохо переносят широкие изображения и иногда сжимают их до мусора. Была надёжда, что в браузере будет не лучше, но проект OpenSeadragon уже проделал всю тяжёлую работу и предоставил удобный способ приближения и панорамирования изображения.

Для разбивки гигантских TIFF на мелкие JPEG-тайлы использовалась утилита vips, а для глубоких ссылок внутри галереи (в основном для удобства этой самой публикации) был написан небольшой JavaScript. Веб-разработка — не самая сильная сторона, так что за не самый аккуратный внешний вид приходится приносить извинения.

Планы на будущее

Есть немало идей для развития этой камеры. Главная — избавиться от зависимости от ноутбука при съёмке, что сделает камеру менее подозрительной внешне и удобнее для переноски. Для этого предстоит доработать некоторые проблемы со сбором данных акселерометра и интерфейсом захвата.

Также планируется улучшить инструменты постобработки. Хочется реализовать что-то, принимающее таблицу с номерами строк и автоматически склеивающее из них изображение. При достаточной амбициозности — интерфейс, позволяющий выделять сегменты и предпросматривать их при разных значениях дистанции на пиксель. Есть и соблазн наконец-то по-настоящему использовать GPS и реализовать фильтр Калмана, но, скорее всего, это будет отложено ещё дальше.

Ещё одна идея — больше странных инфракрасных снимков, возможно, с цветовой заменой в стиле æerochrome, как делает RYE.

Также интересно выяснить соответствие между настройкой усиления камеры и ISO, что позволило бы выбирать локации для съёмки, ориентируясь только на экспонометр. Попытки уже были, но условия освещения на улице менялись слишком быстро для получения надёжных результатов.

Код

Код для захвата доступен здесь, а постпроцессор (grindstone) — здесь.

Благодарности

Огромная благодарность:

  • Meadow (поездки на пароме/поезде, подготовка презентации, вычитка)
  • Брук (помощь с механической конструкцией, 3D-печать)
  • Ари (поездки на поезде, код, подготовка презентации, вычитка)
  • nyanotech (поездки на пароме/поезде)
  • cat (поездки на поезде)
  • Мэдди (код, поездки на пароме, вычитка)
  • kim (вычитка)

Без их помощи результат оказался бы куда хуже.

Спасибо и вам за то, что дочитали до конца!