Запись 16 битных grayscale изображений в видео без потерь с помощью OpenCV

На прошлой неделе, когда работал с Kinect v2, столкнулся со следующей задачей – было необходимо писать карты глубины сцены в видео-файл без потерь. Поскольку, второй кинект отдаёт карты глубины в виде 16 битного изображения в градациях серого, то задача становится не совсем тривиальной. Я придумал пару решений данной задачи, которые можно применять для записи любых 16 битных grayscale изображений. У каждого способа есть как свои плюсы, так и свои недостатки.

Решение первое: реитерпритация изображения

В OpenCV любое изображение представляет собой одномерный массив типа unsigned char, в который построчно записаны пиксели изображения. Поскольку изображение у нас 16 битное, то на запись каждого пикселя уходит 2 байта. Итого размер массива в байтах, в котором хранятся пиксели, 2 * w * h, где w – ширина изображения, а h – его высота. Дальше действует крайне простая идея – а что если интерпретировать массив пикселей не как 16 битное изображение размером w x h, а как 32 битное RGB, но уже с другим размером? А будет хорошо, 32 битные RGB изображения спокойно записываются в видео и сжимаются без потерь, например, с помощью Lagarith Lossless Codec (в коде используется именно он). С обратным преобразованием всё тоже очень просто – считываем цветное изображение, а потом массив, в котором хранятся пиксели этого изображения, интерпретируем как 16 битное изображение, которое мы подвергли реинтерпретации.

В коде запись выглядит так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>

#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>

int main()
{
    // Прочитаем изображение.
    cv::Mat input_image = cv::imread("input.png", cv::ImreadModes::IMREAD_ANYDEPTH);
   
    // Предобработка.
    int new_width = input_image.cols;
    int new_height = input_image.rows;
    cv::Mat final_image = input_image;
   
    // Если ширина и ширина изображения не делятся на 3,
    // придётся изображение немного сжать.
    // Здесь будем сжимать изображение по ширине с сохранением соотношения сторон.
    if (input_image.cols % 3 != 0 && input_image.rows % 3 != 0) {
        new_width -= new_width % 3;
        new_height = static_cast<int>(static_cast<double>(new_width) / input_image.cols * input_image.rows);

        cv::resize(input_image, final_image, cv::Size(new_width, new_height));
    }

    // Найдём размеры RGB изображения, которое мы запишем.
    int writable_image_width = 0;
    int writable_image_height = 0;

    if (new_width % 3 == 0) {
        writable_image_width = new_width * 2 / 3;
        writable_image_height = new_height;
    } else {
        writable_image_width = new_width;
        writable_image_height = new_height * 2 / 3;
    }

    // После того, как знаем размер записываемых фреймов, можно создать
    // объект для записи видео.
    cv::VideoWriter test_video(
        "test_video.avi",
        cv::VideoWriter::fourcc('L', 'A', 'G', 'S'),
        25,
        cv::Size(writable_image_width, writable_image_height),
        true);

    // Сделаем матрицу-заголовок над данными.
    cv::Mat writable_image(writable_image_height, writable_image_width, CV_8UC3, (void *)final_image.data);

    // Запишем полученное изображение в видео.
    test_video.write(writable_image);
   
    // Закроем видео.
    test_video.release();

    return 0;
}

А чтение вот так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>

int main()
{
    // Размеры изображения, которое мы должны восстановить.
    // В примере в посте, оно было немного уменьшено.
    int restored_width = 510;
    int restored_height = 422;

    // Прочитаем закодированное изображение.
    cv::Mat input_image = cv::imread("output_1.png");

    // Раскодируем.
    cv::Mat restored_image(restored_height, restored_width, CV_16UC1, (void *)input_image.data);

    // Запишем результат в файл.
    cv::imwrite("restored_input_1.png", restored_image);

    return 0;
}

 

Визуально, из такого изображения:

Исходное 16 битное изображение в градациях серого

Получим вот такое:

Результат, полученный реинтерпретацией исходного изображения

Если раскодировать вышестоящее изображение, то получим вот такое:

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

Плюсы такого метода:

  1. Простота, как записи, так и чтения, всё делается просто, быстро и понятно.
  2. Нулевой оверхед на решение – мы не записываем лишней информации.

Минуса два:

  1. Не всякое 16 битное изображение можно так записать сохранив всю исходную информацию. Если ни его ширина, ни его высота не кратны 3, то изображение придётся немного уменьшить, прежде чем подвергнуть такой процедуре записи.
  2. Полученная картинка визуально трудно интерпретируется.

Решение второе: кодирование в изображение RGB

Решение более замороченное, но всё же имеет право на жизнь. Здесь тупо побайтно переписываем исходное изображение в RGB изображение по следующему принципу – в первый байт (канал R) пишем ноль, во второй (канал G) – пишем первый байт пикселя исходного изображения и в третий (канал B) – второй байт пикселя исходного изображения, после чего записываем в видео кадр, как обычный RGB.

В коде запись выглядит так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>

#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>

int main()
{
    // Прочитаем изображение.
    cv::Mat input_image = cv::imread("input.png", cv::ImreadModes::IMREAD_ANYDEPTH);

    // Создадим видео.
    cv::VideoWriter test_video(
        "test_video.avi",
        cv::VideoWriter::fourcc('X', 'V', 'I', 'D'),
        25,
        input_image.size(),
        true);

    // Создадим трёхканальное изображение такого же размера, как и исходное.
    cv::Mat writable_image(input_image.size(), CV_8UC3);
   
    // Побайтово копируем исходное изображение в только что созданное.
    // В первый канал пишем 0.
    uchar *read_ptr = input_image.data;
    uchar *write_ptr = writable_image.data;
    for (int i = 0; i < input_image.cols * input_image.rows; ++i) {
        *write_ptr++ = 0;
        *write_ptr++ = *read_ptr++;
        *write_ptr++ = *read_ptr++;
    }

    // Запишем полученное изображение в видео.
    test_video.write(writable_image);

    // Закроем видео.
    test_video.release();

    return 0;
}

Чтение вот так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>

#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>

int main()
{
    // Прочитаем закодированное изображение.
    cv::Mat input_image = cv::imread("output_2.png");

    // Размеры закодированного изображения, совпадают с размерами исходного изображения.
    int restored_width = input_image.cols;
    int restored_height = input_image.rows;

    // Раскодируем изображение.
    cv::Mat restored_image(restored_height, restored_width, CV_16UC1);

    uchar *read_ptr = input_image.data;
    uchar *write_ptr = restored_image.data;
    for (int i = 0; i < restored_width * restored_height; ++i) {
        ++read_ptr;
        *write_ptr++ = *read_ptr++;
        *write_ptr++ = *read_ptr++;
    }

    // Запишем результат раскодирования в файл.
    cv::imwrite("restored_input_2.png", restored_image);
   
    return 0;
}

Визуально из такого изображения (точно такое же изображение было преобразовано чуть выше):

Исходное 16 битное изображение в градациях серого

Получаем вот такое:

Пример результата перекодирования в RGB

Если его раскодировать, то получим изображение, полностью совпадающее с исходным:

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

Плюсы решения:

  1. Картинка визуально поддаётся интерпретации.
  2. Подойдёт изображение любого размера, для записи никогда не потребуется изменять его размеры.

Минусы:

  1. Чтение и запись занимают больше времени, чем в первом способе.
  2. Целая треть записанной информации не имеет смысла.
Метки: , , , , , , ,
Google Bookmarks Digg Reddit del.icio.us Ma.gnolia Technorati Slashdot Yahoo My Web News2.ru БобрДобр.ru RUmarkz Ваау! Memori.ru rucity.com МоёМесто.ru Mister Wong

Напишите комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *