
Моя страсть к накопительству картинок зародилась еще в эпоху диалапа, когда каждый JPEG проявлялся построчно под писк и скрежет модема, а бэкап стянутых с BBS цифровых сокровищ на дискету напоминал ритуал. С тех пор куча скарба разрослась до масштабов домашнего дата‑центра: здесь доисторические смишные мемы, тонны диснеевского клипарта, сканы журналов, галереи фанарта от известных в узких кругах артоделов, масса неотсортированного фототреша из собственных поездок, картинки природы и красоток, порция клубнички, шедевры CGI, нейроарт и фотографии Элизабет Уинстон. Проблема в том, что весь этот терабайтный зоопарк из разных разрешений, битности и форматов — абсолютно неструктурированная свалка, и попытка найти нужное превращается в квест «убей свои выходные». По мере роста коллекции я пробовал подряд все костыли, которые лучшие умы изобретали для сравнения изображений. В этой статье я пройдусь по эволюционной цепочке: от одноклеточных хэш‑сумм до венца творения — свежесобранного монстра DINOv3. Объять необъятное не выйдет — по каждому методу сравнения можно катать лонгрид иллюстрациями и с примерами кода (что, возможно, и сделаю, если меня не закидают жжёными тряпками). Но сейчас попробую изложить суть: как метод устроен, когда он тащит, а когда выдает откровенный бред.
В огромной коллекции наверняка есть повторы — файлы и папки, скопированные в другое место и там забытые, да и скачанные в разное время одинаковые картинки тоже попадаются. Для поиска клонов подойдет любой любой хеш, чем короче тем лучше, даже классический скомпрометированный MD5 — мы не криптографией занимаемся. А если скорость не так важна как точность — более надежный и медленный SHA256. Хеш файла одинаков — изображения одинаковы. Да, теоретически возможны коллизии. Если у нас приступ паранойи, можем у файлов с одинаковым хешем еще с длину сравнить, чтобы уже совсем было на 100% точно. MD5 — это классика и занимает всего 16 байт, не разоримся. А если хотим реальной скорости — берем современный XXH3: на камне с поддержкой AVX-512 за счет векторизации и параллелизации он выжимает 60Гб/с. Для сравнения — на том же железе MD5 в 50–100 раз медленней.
В качестве грубого фильтра хеши сойдут, но они реагируют даже один изменённый бит. Изображения в файлах имеют и голову — заголовок, и, возможно, хвост — мусор после изображения, неизвестно как туда попавший. На само изображение мусор в конце файла не влияет, но убивает идею быстрого сравнения на корню. Да и достаточно открыть картинку в редакторе и сохранить её заново — энкодер добавит свои теги типа даты или копирайт в EXIF заголовка, и вуаля — для хешей это два абсолютно разных изображения, хотя пикселы и не шелохнулись.
Так что придется сначала вытащить декодером из файла само изображение, привести его в какой‑то обговоренный RAW формат (а в файле может быть и 32-битный цвет, и серый 8-битный и даже индексированный, разный порядок цветовых каналов — RGBA/BGR, выравнивание строк и другие прелести), и считать контрольную сумму уже для него. Для lossless форматов типа gif, bmp или png это сработает, для jpeg и webp или avif — нет. Само изображение при сохранении каждый раз подвергается сжатию. Почти невидимые глазу изменения в младших битах (или вполне видимые, как у jpeg) делают использование хешей для сравнения бесполезным.
Сначала хотел тут поместить заезженный мем «у вас JPEG», но сдержался. Сами найдете, не маленькие.
Но если хеш для пережатых изображений не подходит, может сравнивать каждый пиксель в отдельности? Они ведь почти не отличаются. Самый дубовый метод — Mean Squared Error (MSE). Мы берем две картинки одинакового размера, вычитаем значения яркости соответствующих пикселей друг из друга, возводим в квадрат и считаем среднее. Вот формула в Tex, чтобы статья казалась умнее:
Квадрат здесь нужен, чтобы плюс не компенсировал минус, и чтобы крупные косяки, например, добавленный логотип, били по рейтингу сходства в разы сильнее, чем легкое зерно на фото.
Будет работать? Да. Если у вас есть два скриншота, где на одном просто сдвинулся курсор мышки или появился водяной знак, MSE покажет минимальное отклонение. Но как только мы выходим в мир фоток, начинается ад и израиль. Чувствительность у MSE — как у параноика на допросе. Стоит сдвинуть одну картинку всего на 1 пиксель в любую сторону, и вся математика летит в бездну. Для MSE это будут два абсолютно разных изображения, потому что значения в ячейках матриц больше не совпадают.
Чтобы хоть как‑то улучшить MSE, придумали PSNR (Peak Signal‑to‑Noise Ratio) — пиковое отношение сигнала к шуму в децибелах. Это все та же MSE, но завернутая в красивую логарифмическую обертку. Чем выше PSNR, тем больше картинки похожи. Метрика PSNR — это такая специальная олимпиада среди разработчиков кодеков в дисциплине «чей алгоритм сжатия меньше намусорил». Но для поиска похожих фото в архиве на миллион файлов он почти бесполезен так же, как и MSE. Любой кроп, поворот на 1 градус, изменение яркости — и эти ваши пафосные децибелы сходства превращаются в тыкву.
Поняв, что голые цифры разницы пикселей не отражают реальности, светлые умы предложили SSIM. Его идея в том, что человеческое зрение адаптировано для извлечения структуры, а не для подсчета яркости отдельных точек. SSIM не просто вычитает пиксели, он сравнивает изображения по трем параметрам в небольших окнах, обычно 8×8. Для каждого окна считается яркость, контраст и структура, самая мякотка — взаимное расположение пикселей. Вот еще одна и последняя формула, обещаю:
Выглядит страшно, но за такое уважают и накидывают карму. Почему это лучше, чем MSE и PSNR, вместе взятых? SSIM гораздо терпимее к изменению освещения и равномерному шуму. Если вы сохранили фото Элизабет Уинстон в JPEG с диким сжатием, SSIM скажет: «спокойно, структура лица сохранена, это та же картинка, просто шакальная». Но — он всё еще требует одинакового разрешения и пасует перед любым серьезным сдвигом. Для сравнения таких изменений, потребуется более суровый матан, чем сложение и умножение.
Не знаю, кто первым догадался сравнивать не пиксели, а спектры изображения, но он явно был семи пядей во лбу. Если искать корни, то концепция устойчивого хеширования всплыла на рубеже 90-х и нулевых. В народные массы и опенсорс классический pHash ворвался благодаря трудам Нила Кравеца и библиотеки pHash.org. Идея проста: изображение — это не только матрица пикселей, но и визуальный сигнал, в котором есть низкие и высокие частоты.
Суть pHash можно объяснить даже собственной бабушке — мы берем картинку любого размера, ужимаем ее до 32×32, обесцвечиваем, приводя все цвета к яркостям... а далее начинается магия — DCT, «дискретное косинус преобразование» — тот самый крутой матан. Как я и обещал, формулы не привожу, чтобы не отпугнуть гуманитариев. Суть его в том, что черно‑белое изображение 32×32 превращается в матрицу такого же размера, но каждая ячейка теперь содержит частотную составляющую исходного изображения. Верхний левый угол — низкие частоты, передающие структуру, а нижний правый — высокие частоты, шум и цифровой мусор. Их мы отбрасываем — из матрицы 32×32 забираем только верхний левый угол 8×8, в котором содержится самая важная информация. Кстати, верхняя левая ячейка нам не нужна — там базовый уровень сигнала, бесполезный в сравнении. Среди оставшихся 63 ячеек (8 x 8 — 1) мы считаем среднее, а далее строим хеш длиной 64 бита, в котором пишем 0 — если ячейка меньше среднего, и 1 — если больше. А он прекрасно влезает в unsigned long, в котором старший бит всегда равен нулю (базовый сигнал, как я говорил). Кстати, если нашу матрицу 8×8 подвергнуть обратному DCT, мы получим... сильно заблуренное исходное черно‑белое изображение. По сути, мы просто избавились от высокочастотной составляющей.
Ну и сравнивать два изображения становится неприлично просто — считаем число отличающихся бит в двух 64-битных словах (это называют расстояние Хемминга). Современный процессор делает это мгновенно, одной инструкцией POPCNT.
Из личной практики скажу, что почти одинаковые изображения различаются несколькими битами. Если закрывать глаза на 10–20 различающихся бит, можно найти отбалансированные по цветам, слегка сдвинутые, слегка обрезанные изображения (но большинство будет все‑таки ложными совпадениями). А вот если число разных бит больше 20 — это на 99.99% разные изображения, без вопросов.
На Хабре про него уже писали: «Выглядит похоже». Как работает перцептивный хэш / Хабр
Но и такой pHash — не волшебная палочка. Главная его беда — хрупкость перед геометрией. Он слепнет от поворота на жалких 10 градусов, пасует перед отзеркаливанием и впадает в депрессию от укладывания фотки на бок. Чтобы все‑таки находить подобные пары, приходится подвергать исходное изображение всем этим издевательствам, для каждого считать хеш и уже искать похожие хеши в базе для каждого преобразования независимо. Microsoft в свое время разродился PhotoDNA — основанным на DCT хешем, повторенным несколько раз для разных преобразований, чтобы находить известные непотребства, выложенные извращенцами на Microsoft OneDrive.
Кроме попсового pHash, напридумывали еще кучу всяких мутантов на том же принципе разложения на частоты — радиальный хеш, якобы устойчивый к поворотам, цветной хеш, различающий цвета, но все они отправились в Круговерть Пустоты, не выдержав конкуренции с простым и понятным DCT. А можно ли вообще придумать хеш, устойчивый к любым преобразованиям? Да, моменты и простая гистограмма решает.
Метод древний, как фекалии мамонта, но в умелых руках и при наличии чистого фона, работает безотказно. По сути, это попытка превратить кучу пикселей в несколько чисел, которые описывают «форму» и «развесовку» пятна. Математики честно стащили это понятие из механики. Представим, что каждый пиксель — это маленькая гирька, вес которой равен его яркости. Моменты позволяют понять, где у этой конструкции центр тяжести, как она наклонена и насколько она раскидистая.
Формально они называются Hu Moments — это семь магических чисел. Они инвариантны к переносу, масштабу, повороту и даже зеркальному отражению. То есть, как бы ты ни крутил или ни увеличивал свой объект, эти семь чисел должны оставаться в теории непоколебимыми.
Но надо помнить, что моменты — это инструмент для работы с однотонными силуэтами и фонами. Если у нас есть четко выделенный объект на контрастном фоне, например, черная литера на белой бумаге, они незаменимы. Им запросто отличить силуэт гайки от болта — у них принципиально разные распределения масс, и моменты Hu их разделят на раз‑два. В доисторических системах распознавания текста именно моменты помогали понять, что перед нами — буква «О» или «П».
Но если бы моменты были идеальны, мы бы не мучились с поиском алгоритмов. У них есть родовые травмы, которые не лечатся: перекрытия, чувствительность к шуму, ложные совпадения от разных силуэтов. Да и для сравнения фоток из отпуска моменты бесполезны. Там нет ни четкого объекта, ни фона, а только есть хаос пикселей. Попытка посчитать моменты для фотографии леса выдаст набор случайных чисел, которые изменятся хаотично от малейшего дуновения ветра. Гистограммы — совсем другое дело.
Повороты на любой угол, отзеркаливание, изменение разрешения или даже если мы просто разрежем фото на мелкие кусочки и перемешаем их в блендере — будет одна и та же форма гистограммы. Даже наши дедушки умели раскладывать цветную картинку на три цветовых канала и строить гистограммы яркостей по ним. А уже способов сравнивать гистограммы накопилось неимоверное множество. Выбирайте: L1 (Манхэттенское расстояние) — просто сумма разниц между столбцами, L2 (по старику Эвклиду) — сумма квадратов разниц, тут большая разница штрафуется сильнее, пересечение — какой процент от площади перекрыт, Хи‑квадрат, чувствительный к пустым столбцам, корреляция по Спирмену, жутко тормозящий (O(N3) как‑никак), но крутой EMD — тысячи их.
Можно пойти дальше и от трех независимых гистограмм перейти в 3D. В колориметрии есть формула подсчета яркости по RGB:
Заметьте — зеленый (G) дает почти 60% вклада в то, что мы называем яркостью. Почему так — эволюционно наш глаз приспособлен различать больше оттенков зеленого, меньше красного и совсем мало — синего. Думаю, уже знаете или догадались, почему. Так что, на красный и синий можно оставить по три бита, а зеленому дать четыре. Это даст нам 1024 корзины цветов. Вектор из 1024 значений тяжеловат для хранения, но зато более точен. Почему?
Представь две картинки — на одной левая половина ярко‑красная, правая половина ярко‑синяя, а другая равномерно залита фиолетовым цветом (смесь красного и синего). Для трех 1D гистограмм эти изображения идентичны. В обоих случаях у нас будет один пик в красном канале и один пик в синем. Алгоритм скажет: «Это дубликаты!». А вот в 3D гистограмме красные, синие и фиолетовые пикселы аккуратно лягут по своим корзинам.
Хранить три гистограммы, это 23 + 24 + 23 значений или 32 числа. Это крошечный вектор, и информативен он примерно как описание подозреваемого: «две руки, две ноги и одна голова». Схема 3:4:3 (1024 корзины) — это в 32 раза больше данных, но и точность выше на порядок.
Однако работать с каналами RGB сейчас — все равно, что сидеть за ЭЛТ монитором из 90-х. Главная беда RGB — он разрабатывался без учета особенностей зрения. Расстояние между цветами в RGB кубе никак не коррелирует с тем, насколько их разными видит человек. Например, мы хорошо видим разницу при небольшой яркости и почти не замечаем при высокой. Да и в синих тонах вы можете сдвинуться на километр и не заметить разницы, а в зеленых — малейший шаг в сторону превратит оттенок «салатный» в «болотный». У нас глаза древесных предков, помните?
В 2020-м миру явилось цветовое пространство OKLAB. Это перцептивно однородное пространство — там рулит кусочная функция, где линейная, а где кубическая, которое исправляет косяки RGB. В OKLAB тоже три канала: L — яркость, как ее видит глаз, a — спектр от зеленого к пурпурному, b — спектр от синего к желтому. Эвклидово расстояние между цветами в OKLAB — это и есть визуальная разница — так как разницу видит усредненный глаз, не страдающий патологиями.
Но даже самая крутая OKLAB 3D‑гистограмма страдает от болезни, которую называют «мерцание корзин». Представьте, что цвет пикселя находится ровно на границе между двумя корзинами. Малейший шум, артефакт сжатия или изменение яркости на 0.001% — и пиксель перепрыгивает в соседнюю корзину. Хеш меняется, хотя картинка визуально та же.
Вместо того чтобы тупо швырять пиксель в одну корзину, мы представляем пиксель не как точку, а как маленькое облачко вероятности — гауссиану. Мы размазываем вклад одного пикселя по всем корзинам: центр облака получает максимум веса, а края — почти нуль. Вот и вся магия — даже если цвет немного поплыл, он всё равно вносит вклад в те же корзины, просто пропорции чуть меняются. Гистограмма становится плавной и устойчивой. Этот трюк позволяет находить фотографии одного и того же, сделанные в разное время суток и при разном освещении, которые классический метод считал бы совершенно непохожими.
Если возиться с полноценной гистограммой лень, можно вспомнить про четыре цветовых момента. Это такая попытка описать всю палитру изображения через чистую статистику, превращая картинку в компактный вектор из нескольких чисел. Первый момент — это матожидание, то есть средний цвет по всему кадру. Второй — стандартное отклонение, которое показывает разброс и общую контрастность палитры. Третий момент, асимметрия, говорит о том, насколько распределение цветов перекошено в ту или иную сторону. И четвертый — эксцесс, по которому можно судить, насколько график острый, имеем ли мы дело с однородными заливками или же перед нами месиво с резкими пиками. 4 момента на каждый цветовой канал = 12 чисел. Такой подход отлично работает в качестве первого эшелона фильтрации, когда нужно мгновенно отсеять явно лишнее из огромного датасета.
Гистограммы незаменимы при поиске серий — одной фотосессии или соседних кадров фильма. Но часто одного цвета недостаточно. Кит в синем море и самолет в небе для такой гистограммы — одно и то же. Нужно как‑то различать объекты и текcтуры, но как это сделать?
На помощь приходят текстурные дескрипторы, которые пытаются понять не какого цвета суп, а насколько мелко в нем нарезаны овощи. По сути это попытка описать микрорельеф изображения. Самый народный и понятный из них — LBP (Local Binary Patterns). Идея LBP проста как топор, и в этом её гениальность. Мы берем каждый пиксель и смотрим на его соседей в квадрате 3×3. Если краевой пиксель ярче центрального — пишем 1, если нет — 0. Выписываем эти восемь единичек и ноликов по кругу — получаем 8-битное число от 0 до 255. В итоге мы строим гистограмму этих самых LBP‑кодов для всего изображения. Она показывает профиль поверхности: сколько в кадре ровных плато, сколько углов, а сколько — шума.
Если хочется посложнее — их есть у меня. GLCM (Gray‑Level Co‑occurrence Matrix) — матрица смежности. Она считает, как часто пиксель яркости X встречается рядом с пикселем яркости Y под определенным углом. Это дает нам такие параметры, как контраст, энергия и гомогенность. Метод пахнет учебниками по анализу спутниковых снимков 80-х, но работает до сих пор.
Или вот фильтры Габора — попытка смоделировать работу зрительной коры млекопитающих. Мы накладываем на картинку сетку из полосок разной частоты и направления. Идеально детектирует периодические узоры — ткань, кирпич, сетка, но заставляет грустно вздыхать от объема вычислений.
Но и у цветовых гистограмм, и текстурных есть общая проблема — они глобальные. Если вы возьмете фото, разрежете его пополам и поменяете половинки местами — и цветовая гистограмма и текcтурная не заметят подвоха. Статистика‑то осталась прежней! Чтобы понять, что глаз находится выше носа, а не наоборот, нужно перейти от статистики к локальным признакам.
Вначале придется разобраться, что такое особые точки (feature points) на изображении. Если совсем для мимокрокодилов — это те самые приметные «родинки» и «шрамы» на теле изображения, по которым мы узнаем старого знакомого, даже если он надел парик, солнечные очки или сильно похудел.
Представьте, что мы смотрим на фотографию через крошечную замочную скважину. Если мы видим абсолютно белую стену, то совершенно не понимаем, где находимся. Можно бросать взгляды влево, вправо, вверх — картинка не меняется. Это однородная область, и для поиска она полезна примерно так же, как пустой лист бумаги.
Если в скважине видна прямая линия, например край стола, мы понимаем чуть больше. Однако если смотреть вдоль этой линии, картинка снова остается статичной. Только при движении поперек мы замечаем резкие изменения. Это ребро, и оно уже лучше однородного фона, но все еще не дает уникальной привязки.
Магия начинается, когда мы попадаем на угол или пересечение линий. Любое движение в сторону мгновенно меняет картинку. Это и есть простейшая особая точка. Математически это место, где градиент яркости резко меняется сразу в нескольких направлениях.
Если я сфотографирую котика на последний айфон и на древнюю мыльницу, алгоритм должен найти одни и те же точки на морде. Точка на кончике носа не должна быть похожа на точку на кончике хвоста. У каждой должен быть уникальный паспорт, который называют дескриптором. Хорошая точка обязана оставаться собой, даже если мы увеличили или уменьшили картинку, повернули её вверх ногами или выкрутили яркость.
Допустим, я ищу оригинал фото лица среди гор пережатого мусора и журнальных сканов. Обычная гистограмма увидит просто много телесного цвета и выдаст тысячу похожих результатов. Метод локальных дескрипторов же найдет сотни уникальных точек на лице. Даже если на скане половина лица закрыта заголовком статьи, алгоритм RANSAC найдет оставшиеся точки, сопоставит их геометрию с оригиналом и подтвердит совпадение.
Дэвид Лоу в 1999 году представил миру SIFT, который на десять лет стал золотым стандартом. Алгоритм строит целую пирамиду из размытых копий изображения, используя разность Гауссиан (Difference of Gaussians). Те точки, что выживают при любом уровне размытия, признаются особыми. Каждой такой точке присваивается направление на основе градиентов яркости. Теперь, если фото повернуть, дескриптор повернется вместе с ним, сохраняя ориентацию.
Сам дескриптор SIFT устроен довольно хитро. Вокруг точки берется квадрат размером 16×16 пикселей, который разбивается на сетку из 16 зон. В каждой такой зоне считается направление градиентов яркости по восьми направлениям. В итоге мы получаем вектор длиной 16×8 = 128. Это и есть цифровой отпечаток окрестностей точки.
Метод феноменально устойчивый. Дескриптор выживает, даже если картинку по‑разному осветили, повернули или облили кофе. Но за точность приходится платить скоростью: SIFT медленный, как очередь в поликлинике. На контрастных снимках он находит тысячи точек, и сравнение таких 128-байтных векторов заставляет любой процессор закипеть. Кроме того, на неконтрастных фото с туманом или облаками, точек находится катастрофически мало, и они ведут себя непредсказуемо.
В 2006 году появился SURF. Авторы решили ускорить процесс, заменив честные Гауссианы на упрощенные коробчатые фильтры. Он получился менее точным, зато работал значительно быстрее, а дескриптор в нем сократили до 64 чисел.
Однако настоящая революция в скорости произошла с приходом ORB. ORB быстр как пуля, потому что его дескриптор — это не массив float, а бинарная строка длиной 256 бит. Вместо долгих вычислений градиентов он просто сравнивает пары точек вокруг центра: если левая точка ярче правой, пишется единица, иначе ноль. Такие строки сравниваются через расстояние Хэмминга, что для современного процессора является практически бесплатной операцией (упомянутая выше инструкция POPCNT рулит).
Ну и под занавес эпохи классических дескрипторов появился FREAK. Это попытка сделать ORB с человеческим лицом, а точнее — глазом. Он имитирует строение нашей сетчатки: пиксели вокруг точки выбираются не по сетке, а по концентрическим кругам. Плотность этих кругов растет к центру. Вектор у него тоже бинарный, поэтому сравнение двух дескрипторов происходит мгновенно.
Однако даже продвинутые дескрипторы не понимают смысла изображения. Они видят углы и линии, но не осознают, что на картинке изображен именно человек. Работа с малоконтрастными изображениями превращается в ад, точки находятся непредсказуемо, да и хранить сотни и тысячи найденных дескрипторов для каждого изображения — отдельная проблема.
Как‑то вылезали за счет Bag Of Words: брали все фото из тренировочного датасета, вытаскивали из них все дескрипторы и сваливали в одну кучу. Затем с помощью K‑means разбивали это облако векторов на кластеры, скажем, на 10 тысяч групп. Центр каждого кластера — это визуальное слово. Так получался словарь — одно слово может означать глаз, другое — фрагмент кирпичной кладки, третье — листок на дереве.
Теперь каждая картинка превращается в гистограмму слов. Вместо гигантской свалки из миллиардов векторов у нас остается один компактный вектор длиной в 10 тысяч ячеек, где написано, сколько раз в кадре встретилось слово номер 42.
Это позволило использовать перевернутые индексы — ту же технологию, на которой работал и Google и Яндекс. Если я ищу все фото с лицами, я просто смотрю, в каких файлах часто встречаются слова, описывающие черты лица. Это мгновенно отсеивает 99% мусора.
Конечно, у BoW есть родовая травма — он полностью теряет информацию о том, где именно находятся эти слова и как они расположены относительно друг друга. Для алгоритма глаза на лбу, а нос ниже рта — это нормально, ведь количество слов в мешке совпадает. Чтобы хоть как‑то это лечить, делили картинку на квадраты и считали мешки слов для каждого отдельно, надеясь, что геометрия не поплывет слишком сильно. Это был славный костыль, который позволял выживать в начале десятых, пока нейросети не сделали весь этот цирк с конями ненужным.
Пока высоколобые математики десятилетиями упарывались по формулам для описания углов и текстур, пытаясь формализовать каждый алгоритм, индустрия внезапно осознала: ручной труд — это тупик для неудачников. Наступила эра глубокого обучения, когда мы честно расписались в собственном бессилии и отдали всю грязную работу сверточным нейросетям, признав, что этот черный ящик найдет закономерности в куче пикселей гораздо лучше любого профессора.
VGG16 из 2014 года стала тем самым бронепоездом, который на полном ходу переехал все классические дескрипторы. В отличие от своих предшественников, которые пытались казаться умнее, чем они есть, VGG16 поражала архитектурной прямолинейностью на уровне кирпича. Вместо того чтобы городить огород из хитрых фильтров разного калибра под каждую задачу, авторы просто пошли по пути грубой силы: наслоили друг на друга бесконечные блоки из микроскопических сверток 3×3. Просто, как два байта переслать, но именно эта пирамида блинов внезапно начала видеть мир с пугающей точностью.
На входе у нас обычно цветное стандартное корыто размером 224×224 пикселя. Сеть работает как многослойный фильтр: первые слои — это самые примитивные детекторы, которые замечают только вертикальные палочки, горизонтальные линии и точки. Чем глубже мы зарываемся в недра VGG16, тем более забористые галлюцинации начинают посещать нейроны. На средних этапах они уже вовсю реагируют на кружочки, текстуру меха или хитросплетения ткани. К финалу сеть начинает видеть сложные концепты вроде человеческого глаза, колеса автомобиля и характерного строения челюсти.
Всё это кунг‑фу сеть выучила не по учебникам, а в ходе суровой дрессуры на датасете ImageNet, где больше миллиона размеченных картинок. Процесс обучения — это не просто игра, а бесконечная череда попыток угадать, что на картинке. После каждого неверного ответа она мучительно подкручивает свои внутренние веса через обратное распространение ошибки, чтобы в следующий раз быть хоть на долю процента точнее.
В оригинальной VGG16 последний слой — это классификатор Softmax, эдакая трибуна с тысячью нейронов‑крикунов. Каждый из них отвечает за свою категорию. Задача слоя — выдать вердикт в виде вероятности. Если на фото кошка, нейрон «кошка» будет орать громче всех, выдавая значение в районе 0.96, пока остальные уныло молчат. Беда в том, что этот слой — жуткий формалист. Он обучен выбирать строго из тысячи известных ему классов объектов. Если скормить ему какого‑нибудь редкого манула, которого не было в ImageNet, сеть начнет лихорадочно подгонять несчастного зверя под знакомые шаблоны, пытаясь убедить вас, что это персидский кот либо меховая шапка.
И тут кому‑то пришла в голову мысль: а зачем нам вообще слушать этот финальный бред? Чтобы сеть могла уверенно опознать кошку, на предыдущем этапе она уже должна была собрать все признаки кошачести в один плотный вектор. Так мы начали выдирать данные из предпоследнего слоя — это обычно вектор из 4096 чисел с плавающей точкой. Это и есть семантический эмбеддинг, цифровая душа изображения.
Теперь сравнение картинок превращается в чистый примитивную операцию: мы считаем косинусное расстояние между этими векторами. Если они смотрят в одну сторону, то косинусное расстояние близко к 1 — значит, на картинках изображено нечто очень близкое по смыслу, если около 0 — ничего похожего. Для ускорения сккозного поиска вектора нормализуются, но это уже скучные детали. Именно благодаря сравнению эмбеддингов нейронка понимает, что студийная фотография человека и кривой карандашный набросок оного — это одна и та же сущность, хотя на уровне пикселей у них нет вообще ничего общего.
Правда, VGG16 по современным меркам — это уже почтенный ископаемый ящер, неповоротливый и дико прожорливый. Индустрия быстро осознала, что тупое нагромождение слоев — это путь в никуда. Главный косяк VGG16 был в её избыточности: 138 миллионов параметров требовали памяти как не в себя, а при попытке сделать сеть еще глубже, она просто выпадала в осадок из‑за затухания градиентов.
Представьте, что VGG16 — это дремучая бюрократическая контора, где ваш запрос должен пройти через шестнадцать кабинетов строго по цепочке. В каждом сидит чиновник и переписывает бумагу так, как считает нужным. К последнему кабинету суть запроса искажается настолько, что на выходе получается дичь. Информация о том, где именно сеть облажалась, просто не доходит до последних слоев, теряясь в бесконечных математических преобразованиях.
ResNet — это тот же коридор с кабинетами, но в нем наконец‑то прорубили запасные двери — skip‑connections. Теперь документ не просто кочует из рук в руки. У сети появилась возможность пробросить копию оригинала сразу через три кабинета вперед по специальному байпасу. В VGG16 сеть на каждом этапе мучительно пыталась перерисовать всю картину заново. В ResNet она просто ищет дельту: тут добавим теней, здесь уточним контур прически. Эта стратегия позволила строить монстров глубиной в 152 слоя, не боясь, что они превратятся в генератор белого шума. В итоге эмбеддинги стали качественнее, а памяти всё это хозяйство стало жрать на порядок меньше.
Но даже ResNet в итоге уперся в стену. Свертка по своей сути — это маленькое близорукое окошко, которое ползает по картинке и видит только то, что находится в радиусе пары пикселей. Чтобы сообразить, как связаны объекты в разных углах кадра, сигналу приходится долго и нудно пробираться через десятки слоев. Это как пытаться прочитать газету через замочную скважину: буквы видишь, а смысл смысл доходит, только когда дочитаешь до конца страницы.
Вторая беда — индуктивный сдвиг. Сверточные сети с рождения запрограммированы верить, что близкие пиксели важнее далеких. Это отлично помогает искать текстуру ковра, но мешает понимать сложную композицию. Им не хватает гибкости, чтобы осознать, что два объекта в разных концах фото связаны общим контекстом, а не просто случайно оказались в одном кадре.
Трансформеры зашли в эту область с ноги, просто вынеся концепцию локальности с вертушки. Если VGG16 — это классическая совковая вертикаль власти, где рядовой пиксель не имеет права голоса и может общаться только со своим начальником, то трансформер — это тотальная анархия и общий чат на миллион рыл. Здесь каждый пиксель может в любой момент пнуть любой другой пиксель напрямую, даже если он находится на другом конце кадра. Чтобы этот балаган хоть как‑то работал, картинку сначала шинкуют в винегрет, превращая её в набор патчей — эдаких визуальных слов. И вот тут в игру вступает Self‑Attention — «самовнимание». Это та самая магия, которая позволяет каждому куску изображения «взвесить» всех остальных соседей и решить, кто тут важная шишка, а кто — просто шум на фоне. В итоге мы получаем не просто набор признаков, а глобальный контекст, где нейрон, отвечающий за левый глаз, видит правую пятку, не дожидаясь, пока сигнал доползет через сорок слоев сверток.
Главная фишка трансформеров — в полном отсутствии так называемого индуктивного сдвига. Если старые добрые сверточные сети с рождения страдают тяжелой формой близорукости и свято верят, что соседние пиксели — это семья, а те, что находятся на разных краях кадра — вообще не знакомы, то трансформеру на эти родовые травмы глубоко плевать. Он анализирует на картинку, не имея ни малейшего понятия о том, что такое линия или объект. Он выучивает всё с нуля, буквально выгрызая смысл из сырых данных без всяких подсказок со стороны.
Разумеется, за такую свободу от приходится платить монетой. Трансформер жрет вычислительные ресурсы так, будто завтра не наступит. Там, где ветеран ResNet скромно довольствовался домашним GPU и обучался за пару часов, этот левиафан требует кластеры из H100 и терабайты датасетов. Зато семантическая точность эмбеддингов зашкаливает так, что старые алгоритмы начинают казаться детским лепетом. Система перестает тупо сравнивать количество синей краски в кадре и начинает понимать контекст. Теперь для неё просто синий фон на студийном фото и высокое синее небо — это два фундаментально разных объекта, даже если по цветовым гистограммам они идентичны.
Пока остальные мучительно вычисляли градиенты и углы, OpenAI выкатили CLIP — мультимодального монстра, которого натаскали на сотнях миллионов пар «картинка‑текст», буквально заставив слова и пиксели жить в одном общем загоне для смыслов. Идея была наглой и простой: давайте загоним и текстовое описание, и изображение в одно и то же векторное пространство так, чтобы фраза «рыжеволосая девушка в синем платье на фоне заката» и реальные пиксели с этой самой девушкой и закатом оказались в одной точке пространства. Технически это выглядело как тотальный допрос с пристрастием: нейросеть заставляли угадывать, какая подпись подходит к какой картинке, пока она не научилась выстраивать мостики между абстрактными понятиями и визуальным хаосом. Теперь не нужно было мучительно искать похожую картинку — можно было просто вбить в поиск «Слай в образе киборга» и получить результат. Увы, у CLIP была одна досадная проблема: он отлично схватывал общий настрой изображения, но тупил в деталях. Для него «человек кусает собаку» и «собака кусает человека» выглядят одинаково, потому что в мешке слов и наборе пикселей присутствовали и те, и другие. Кроме того, CLIP легко велся на «типографские атаки» — если на заборе написать не «Дрова», а другое слово, нейронка верила написанному, а не увиденному.
И тут ребята из запрещенной организации выкатили DinoV2. Главная проблема моделей VIT до него — это нестабильность при масштабировании. Как только ты пытаешься скормить сети не миллион, а сто миллионов картинок, градиенты начинают улетать в стратосферу, а loss — плясать джигу. Чтобы справиться с этим, скрестили два подхода — во‑первых, совместили первый Dino, который учит картинку целиком и iBOT, который заставляет сеть додумывать зацензуренные черные квадраты, зазубривая устройство частично скрытых объектов. Во‑вторых, внедрили нормализацию. Если по‑простому: это алгоритм, который не дает учителю и ученику сговориться и начать выдавать один и тот же пустой вектор для любой картинки — так называемая проблема коллапса признаков.
Если сравнивать с CLIP, который долгое время был королем горы, то DINOv2 выигрывает в геометрии. CLIP учится на текстовых подписях, которые часто врут или описывают только самое очевидное. CLIP видит собаку, но не видит структуру её шерсти. DINOv2 вообще не видел подпись под картинкой при обучении — он смотрел только на 142 миллиона очищенных от дублей картинок — датасет LVD-142M. То есть DinoV2 сам, без единой подсказки человека, сегментирует объекты. Крылья птицы будут одного цвета, голова другого, а фон третьего. Это то, что делает его монстром в задачах, где нужно не просто понять смысл, а четко сопоставить детали.
Буквально на днях выкатили семимиллиардный DINOv3 (он же ViT-7B), и я уже успел засунуть в него руки по локоть. Получить веса c Hugging Face был тот еще квест: нужно заполнить анкету, сдать все пароли и явки и поклясться в верности, объясняя, зачем тебе понадобился такой левиафан. Впрочем, мой запрос одобрили меньше чем за час.
На обучение этой махины ушло больше 60 тысяч часов на Nvidia H100 — счет за электричество там наверняка такой, что можно было бы купить небольшой остров. Главный техническая киллер‑фича, о котором сейчас трубят из каждого утюга — это матрица Грама (Gram Anchoring). Если раньше нейронки на краях объектов часто выдавали мыло и неуверенность, то теперь матрица, описывающая попарные сходства между всеми патчами изображения, жестко якорит структуру. В теории это дает невероятно четкие маски сегментации. Детализация больше не плывет даже на самых сложных краях объектов, где раньше всё превращалось в кашу из пикселей — неужели он сможет отделить одно облако от другого?
Еще один важный апгрейд касается того, как сеть понимает пространство. DINOv2 использовал классические обучаемые позиционные эмбеддинги, которые фактически привязывали модель к стандартной квадратной сетке. Скармливаешь ей картинку нестандартного разрешения — и она впадает в легкий ступор, теряя ориентацию. В DINOv3 архитектуру пересадили на RoPE.
Эта штука пришла из мира больших языковых моделей и позволила картинкам стать инвариантными к разрешению. Теперь можешь засунуть в сеть хоть 4K панораму, хоть сверхдлинный скриншот — она не потеряет мелкие детали и не забудет, где у изображения верх, а где низ.
Но самое вкусное приберегли на десерт. DINOv3 теперь умеет в CLIP‑стиль, то есть понимает текстовые запросы в связке с фото. Но делает это не на уровне «угадай вайб», а с точностью локальных дескрипторов. Это уже не просто ленивый поиск в духе «где‑то на картинке есть песик и велосипед», а точное сопоставление, где каждый объект знает свое место на изображении.
DinoV2 не занимается унылым пересчетом пикселей в надежде, что их цвет совпадет — он ищет близость в пространстве смыслов, которое сам выстроил в процессе обучения. В этом инфернальном гиперпространстве на 1024 или 1536 измерений картинки с похожим сюжетом кучкуются рядом.
Если хотите посмотреть, как приложение на C# вытаскивает вектор DinoV2 из произвольного изображения, вот мой код: ImgMzx/ImgMzx/ImagesVectors.cs at main · wmlabtx/ImgMzx Использована библиотека ImageSharp для декодинга изображений и Microsoft ML.NET для работы с обученным DinoV2, запеченным в ONNX формате (dinov2-base.onnx, 330Мб).
Для двух фотографий разных девиц, играющих в теннис в разную погоду и на разных кортах, косинусное сходство векторов будет в районе 0.9. Для алгоритма это практически одно и то же событие, потому что семантика «теннис» перевешивает разницу в цвете травы или фасоне кроссовок. А если взять цветное фото своего авто, сделать его черно‑белым, отзеркалить и заблурить в хлам, сходство будет стремиться к единице. Модель в курсе, что от смены палитры или ориентации суть объекта не меняется — это всё та же колымага, только в профиль. В теории, DinoV3 должен размазать предшественников по стенке, обеспечив точность на уровне «вижу каждый прыщ на лице» даже при поиске по смыслу. Но за этот банкет придется платить. Модель на семь миллиардов параметров — это уже не просто «скачал и запустил».
Подводя итог этой эпической саги о том, как мы перестали ждать новинок от математиков и начали поклоняться видеокартам, можно констатировать одно: эра «ручного» компьютерного зрения официально мертва и уже начинает попахивать. Для тех, кто всё еще держится за старые методы из чувства ностальгии или экономии ресурсов, у меня плохие новости. Весь этот матан с гистограммами и мешками слов теперь выглядит как попытка собрать адронный коллайдер из палок и желудей. Пока мы колебались между перцептивными хешами и цветовыми моментами, трансформеры просто отменили границы. Конечно, за этот цифровой триумф придется платить — но такова цена прогресса: либо ты кормишь этого монстра и получаешь свободный поиск за миллисекунды по 400 тысячам изображений, либо продолжаешь смотреть, как твои старые алгоритмы галлюцинируют, пытаясь найти смысл там, где его нет. На этом всё. SIFT и компания отправляется в музей, к перфокартам и дискетам, а мы погружаемся в DINOv3, надеясь, что следующая версия не потребует для работы энергию небольшой звезды.