Утилита для обработки снимков с тепловизора: пиксели, температура которых попадает в заданный диапазон, остаются цветными, все остальные — конвертируются в градации серого. Легенда (шкала температур) в выходном изображении не изменяется — по ней видно, какой диапазон подсвечен.
Программа написана под конкретную практическую задачу, а не как универсальный инструмент для любых тепловизоров. Исходные данные — снимки внутренней структуры транзистора с различными дефектами, сделанные одним конкретным тепловизором с "запечённой" в кадр цветовой палитрой (растровый JPG со шкалой, а не сырые радиометрические данные с температурой в каждом пикселе). Цель — увидеть на этих снимках только зоны максимальной температуры, чтобы визуально локализовать дефект; остальная часть кадра при этом только мешает и её нужно убрать в градации серого.
Из-за этого в проекте намеренно зашиты как константы вещи, которые в универсальном инструменте пришлось бы определять автоматически:
- Геометрия ROI (расположение термо-картинки и шкалы в кадре) — общая для всей серии снимков одного тепловизора, измерена вручную один раз.
- Калибровка шкалы (T_min/T_max) — своя для каждого снимка, считывается вручную с подписей на шкале конкретного фото.
- Палитра — Ironbow-подобная: чёрный → фиолетовый → пурпурный → оранжевый → жёлтый → белый, монотонный градиент без циклического перехода через зелёный (как в Jet/Rainbow). Это упрощает сопоставление цвета с температурой — ниже риск спутать два разных участка шкалы с одинаковым оттенком.
Для более широкой задачи (произвольные тепловизоры и палитры) потребовались бы дополнительно: автодетект геометрии шкалы, OCR подписей min/max, база известных палитр по моделям тепловизоров — сознательно не делались, так как не нужны для 4 конкретных снимков одной серии.
- Извлечение палитры. Из ROI шкалы (colorbar) построчно вычисляется
средний цвет и по линейной формуле — соответствующая температура.
Результат — таблица «цвет в Lab → температура» (LUT), см.
PaletteExtractor::extract. - Сопоставление цвет→температура. Изображение и LUT переводятся в
CIE Lab (см.
ColorSpaceUtils::toLabFloat). Для каждого пикселя термо-картинки ищется ближайший по Lab-расстоянию цвет палитры (ColorToTempMapper::lookup). Если ближайший цвет дальше порога — пиксель не принадлежит градиенту шкалы (текст, рамка кадра, метка-крестик) и остаётся нетронутым. - Фильтр диапазона. Пиксели с температурой внутри заданного диапазона
остаются как есть, вне диапазона — обесцвечиваются
(
0.299·R + 0.587·G + 0.114·B), см.ThermalRangeFilter::apply.
Lab используется вместо RGB/HSV для сопоставления цвета с температурой, так как его метрика расстояния лучше соответствует восприятию: одинаковые на вид оттенки палитры дают меньшее Lab-расстояние, чем при сравнении в RGB, — это снижает число ошибочно классифицированных пикселей на границах соседних цветов палитры.
Конвертация в ColorSpaceUtils::toLabFloat идёт через промежуточный float
[0,1], а не через встроенный 8-битный Lab OpenCV — иначе шкала
Lab-расстояний была бы другой, и порог отсечения (см. ниже) перестал бы
быть осмысленным. Изображение и палитра обязательно конвертируются одной
и той же функцией, чтобы обе стороны сравнения были в одной системе
координат.
Измерена вручную по 4 эталонным снимкам и одинакова для всей серии съёмки: положение термо-картинки и шкалы в кадре не меняется от снимка к снимку — меняется только калибровка температур (см. ниже).
| Параметр | Значение |
|---|---|
| Размер изображения | 1633×1000 px |
| ROI термо-картинки (без легенды) | x: 0–1330, y: 0–1000 |
| ROI шкалы (colorbar) | x: 1380–1486, y: 67–933 |
| Направление шкалы | верх (y=67) = T_max, низ (y=933) = T_min |
Формула пересчёта координаты шкалы в температуру (см.
PaletteExtractor::extract):
T(y) = T_max - (y - 67) / (933 - 67) * (T_max - T_min)
где T_min/T_max — индивидуальная калибровка конкретного снимка.
T_min/T_max различаются между снимками (одна серия съёмки, но диапазоны не
идентичные — тепловизор автоматически подбирает границы шкалы под каждый
кадр), поэтому LUT цвет→температура извлекается заново для каждого файла
отдельно — общий ROI переиспользуется, привязка температур — нет.
Значения хранятся в src/palette/CalibrationRegistry.cpp.
| Файл | T_min | T_max |
|---|---|---|
| no_defect.jpg | 22.3 °C | 48.3 °C |
| defect_edge.jpg | 25.9 °C | 51.0 °C |
| defect_center.jpg | 24.5 °C | 47.0 °C |
| defect_near_center.jpg | 25.0 °C | 49.3 °C |
Ряд решений, которые в общем случае выглядели бы как недоработка, здесь — сознательный выбор под масштаб задачи (разовая обработка, а не видеопоток):
- Линейный перебор по LUT, а не k-d дерево (
cv::flann::Index) или квантованная 3D-таблица: на ROI ~1.3 миллиона пикселей и LUT ~866 записей полный перебор укладывается в ~1 секунду на снимок — быстрее структуры избыточны для такого объёма (см.ColorToTempMapper). - Порог Lab-расстояния подобран экспериментально (константа
Geometry::kLabDistanceThreshold = 14.0f): меньшие значения начинали отсекать край легитимного градиента палитры, большие — переставали отсекать метку-крестик и подписи на шкале. - Непалитровые пиксели (текст, рамка, метка-крестик) не обесцвечиваются,
а остаются как в оригинале —
if (!temp) continue;вThermalRangeFilter::apply. Их температура не определена, поэтому безопаснее оставить как есть, чем гадать. - Диапазон фильтра вводится вручную при каждом запуске (аргументы CLI или интерактивное меню демо) — эти границы решают, что именно "интересно" на конкретном снимке, и не могут быть выведены автоматически.
- Легенда (шкала) в выходном изображении всегда сохраняется как в оригинале.
- Позиционный CLI без сложного парсера аргументов (
ArgParser) — четыре параметра, разборargvпо позициям более чем достаточен. - Директория результатов единая (
output/), создаётся автоматически при первом запуске; имя каждого файла включает метку времени, чтобы результаты разных запусков не перезаписывали друг друга (см.OutputDirectory).
thermal-filter/
├── CMakeLists.txt
├── Doxyfile
├── README.md (этот файл)
├── .gitignore
├── src/
│ ├── main.cpp точка входа боевой CLI-программы
│ ├── common/ Geometry — общие для всех снимков ROI и пороги
│ ├── io/ ImageLoader, ImageWriter, OutputDirectory
│ ├── palette/ Calibration, CalibrationRegistry, PaletteLut, PaletteExtractor
│ ├── mapping/ ColorSpaceUtils, ColorToTempMapper
│ ├── filter/ ThermalRangeFilter
│ └── cli/ ArgParser
├── demo/
│ └── demo_main.cpp точка входа интерактивной демо-программы
├── testdata/ 4 эталонных снимка
└── examples/ готовые примеры результатов (см. ниже)
src/main.cpp и demo/demo_main.cpp — единственные два файла, у которых
есть функция main; вся логика пайплайна собрана в статическую библиотеку
thermal_core и переиспользуется обеими программами.
Требуются CMake ≥ 3.10, компилятор с поддержкой C++17 и OpenCV 4
(компоненты core, imgproc, imgcodecs).
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"После сборки в build/ появятся два исполняемых файла:
thermal_filter (боевая программа) и thermal_filter_demo (демо).
Параметры — аргументы командной строки:
./build/thermal_filter <input.jpg> <filter_min_C> <filter_max_C> [output_dir]input.jpg— путь к одному из откалиброванных снимков (см. таблицу калибровки выше). Для неизвестного файла программа завершится с ошибкой — нет калибровки шкалы. Чтобы добавить новый снимок, измерьте его T_min/T_max по подписям шкалы и добавьте запись вsrc/palette/CalibrationRegistry.cpp.filter_min_C,filter_max_C— границы диапазона температур (°C), который должен остаться цветным.output_dir— необязательно; директория для результата (по умолчанию —output, создаётся автоматически при первом запуске).
Пример:
./build/thermal_filter testdata/defect_center.jpg 30 40Результат сохраняется в output/<имя>_filtered_<ГГГГММДД_ЧЧММСС>.jpg —
директория output/ переиспользуется между запусками, а метка времени в
имени файла не даёт результатам одного запуска перезаписать другой.
Не требует аргументов командной строки — вместо этого ведёт через
текстовое меню: показывает список 4 известных калиброванных снимков из
testdata/, спрашивает номер снимка и диапазон температур, обрабатывает и
сохраняет результат в ту же директорию output/. После обработки одного
снимка предлагает обработать следующий; выход — по вводу q.
./build/thermal_filter_demoВ отличие от output/ (создаётся программой при запуске), examples/
— это заранее подготовленные результаты, включённые в проект как
демонстрация без необходимости ничего собирать и запускать. На каждом из
4 снимков подсвечены только верхние 5°C диапазона шкалы ([T_max - 5, T_max]) — это и есть зона максимальной температуры, которую нужно было
увидеть в исходной задаче (см. "Контекст задачи" выше).
| Снимок | Диапазон фильтра | Результат |
|---|---|---|
| no_defect.jpg | 43.3–48.3 °C | examples/no_defect_top5C.jpg |
| defect_edge.jpg | 46.0–51.0 °C | examples/defect_edge_top5C.jpg |
| defect_center.jpg | 42.0–47.0 °C | examples/defect_center_top5C.jpg |
| defect_near_center.jpg | 44.3–49.3 °C | examples/defect_near_center_top5C.jpg |
Воспроизвести любой из них можно боевой программой, например:
./build/thermal_filter testdata/defect_center.jpg 42.0 47.0 examplesКод документирован в стиле Doxygen/Javadoc (@brief, @param, @return,
@throws) на русском языке. Чтобы сгенерировать HTML-документацию:
doxygen DoxyfileРезультат появится в docs/html/index.html.