Вычисление вертикального градиента 2D-изображения вызывает странный вывод - PullRequest
0 голосов
/ 03 мая 2020

Я хочу применить простой фильтр извлечения / градиента [-1, 0, 1] к изображению из файла .ppm.

Необработанные двоичные данные из файла .ppm считываются в одномерный массив:

uint8_t* raw_image_data;
size_t n_rows, n_cols, depth;

// Open the file as an input binary file
std::ifstream file;
file.open("test_image.ppm", std::ios::in | std::ios::binary);
if (!file.is_open())  { /* error */ }

std::string temp_line;
// Check that it's a valid P6 file
if (!(std::getline(file, temp_line) && temp_line == "P6")) {}
// Then skip all the comments (lines that begin with a #)
while (std::getline(file, temp_line) && temp_line.at(0) == '#');

// Try read in the info about the number of rows and columns
try {
    n_rows = std::stoi(temp_line.substr(0, temp_line.find(' ')));
    n_cols = std::stoi(temp_line.substr(temp_line.find(' ')+1,temp_line.size()));
    std::getline(file, temp_line);
    depth = std::stoi(temp_line);
} catch (const std::invalid_argument & e) { /* stoi has failed */}

// Allocate memory and read in all image data from ppm
raw_image_data = new uint8_t[n_rows*n_cols*3];
file.read((char*)raw_image_data, n_rows*n_cols*3);
file.close();

Затем я считываю изображение в градациях серого из данных в двумерный массив, называемый image_grayscale:

uint8_t** image_grayscale;
image_grayscale = new uint8_t*[n_rows];
for (size_t i = 0; i < n_rows; ++i) {
    image_grayscale[i] = new uint8_t[n_cols];
}

// Convert linear array of raw image data to 2d grayscale image
size_t counter = 0;
for (size_t r = 0; r < n_rows; ++r) {
    for (size_t c = 0; c < n_cols; ++c) {
        image_grayscale[r][c] = 0.21*raw_image_data[counter]
            + 0.72*raw_image_data[counter+1]
            + 0.07*raw_image_data[counter+2];
        counter += 3;
    }
}

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

uint32_t** gradient_magnitude;
// Allocate memory
gradient_magnitude = new uint32_t*[n_rows];
for (size_t i = 0; i < n_rows; ++i) {
    gradient_magnitude[i] = new uint32_t[n_cols];
}

// Filtering operation
int32_t grad_h, grad_v;
for (int r = 1; r < n_rows-1; ++r) {
    for (int c = 1; c < n_cols-1; ++c) {
        grad_h = image_grayscale[r][c+1] - image_grayscale[r][c-1];
        grad_v = image_grayscale[r+1][c] - image_grayscale[r-1][c];
        gradient_magnitude[r][c] = std::sqrt(pow(grad_h, 2) + pow(grad_v, 2));
    }
}

Наконец, я записываю отфильтрованное изображение в выходной файл .ppm.

std::ofstream out;
out.open("output.ppm", std::ios::out | std::ios::binary);

// ppm header
out << "P6\n" << n_rows << " " << n_cols << "\n" << "255\n";

// Write data to file
for (int r = 0; r < n_rows; ++r) {
    for (int c = 0; c < n_cols; ++c) {
        for (int i = 0; i < 3; ++i) {
            out.write((char*) &gradient_magnitude[r][c],1);
        }
    }
}
out.close();

Однако выходное изображение , это беспорядок.

Когда я просто устанавливаю grad_v = 0; в l oop (т. е. рассчитываю только горизонтальный градиент), вывод выглядит корректно:

Horizontal Gradient

Когда я вместо этого устанавливаю grad_h = 0; (т.е. рассчитываю только вертикальный градиент), выходные данные выглядят странно:

Vertical Gradient

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

Может кто-нибудь увидеть какие-либо проблемы? Большое спасибо!

1 Ответ

0 голосов
/ 05 мая 2020

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

    n_rows = std::stoi(temp_line.substr(0, temp_line.find(' ')));
    n_cols = std::stoi(temp_line.substr(temp_line.find(' ')+1,temp_line.size()));

, но в документации вы можете прочитать:

Каждый образ PPM состоит из следующего:

  1. «Волшебный c номер» для определения типа файла. Волшебное число c изображения ppm - это два символа "P6".
  2. Пробелы (пробелы, TAB, CRs, LFs).
  3. A ширина , отформатированный в десятичном виде в формате ASCII.
  4. Пробелы.
  5. A высота , снова в десятичном виде ASCII.

[...]

Ширина - столбцы, высота - строки , Так что это классическая ошибка, которую вы получаете при реализации обработки изображений: замена строк и столбцов.

С точки зрения дидактики c, почему вы делаете эту ошибку? Мое предположение: плохие инструменты отладки. Сделав рабочий пример из вашего вопроса (усилия, которые я бы сэкономил, если бы вы предоставили MCVE ), я побежал до конца загрузки изображения и использовал Image Watch, чтобы просмотреть содержимое вашего изображения с помощью @mem(raw_image_data, UINT8, 3, n_cols, n_rows, n_cols*3). Результат:

wrong result

Хорошо, давайте попробуем поменять их местами: @mem(raw_image_data, UINT8, 3, n_rows, n_cols, n_rows*3). Результат:

correct result

Намного лучше. К сожалению, я не знаю, как указать RGB вместо BGR в псевдо-команде Image Watch @mem, поэтому неправильные цвета.

Тогда мы вернемся к вашему коду: скомпилируйте все предупреждения. Тогда я бы использовал больше функций std::stream для анализа вашего ввода и меньше std::stoi() или find(). Избегайте выделения памяти с помощью std::vector и создайте (возможно, шаблон) класс для изображений. Даже если вы придерживаетесь указателя на указатель, не делайте кратных new для каждой строки: создайте один новый для указателя в строке 0, а другие указатели указывают на него:

    uint8_t** image_grayscale = new uint8_t*[n_rows];
    image_grayscale[0] = new uint8_t[n_rows*n_cols];
    for (size_t i = 1; i < n_rows; ++i) {
        image_grayscale[i] = image_grayscale[i - 1] + n_cols;
    }

Тот же эффект, но легче освободить и управлять как один кусок памяти. Например, сохранение в виде PGM становится:

    {
        std::ofstream out("output.pgm", std::ios::binary);
        out << "P5\n" << n_rows << " " << n_cols << "\n" << "255\n";
        out.write(reinterpret_cast<char*>(image_grayscale[0]), n_rows*n_cols);
    }

Заполните ваши границы! Используя один стиль выделения, который я показал вам, вы можете сделать это следующим образом:

    uint32_t** gradient_magnitude = new uint32_t*[n_rows];
    gradient_magnitude[0] = new uint32_t[n_rows*n_cols];
    for (size_t i = 1; i < n_rows; ++i) {
        gradient_magnitude[i] = gradient_magnitude[i - 1] + n_cols;
    }
    std::fill_n(gradient_magnitude[0], n_rows*n_cols, 0);

Наконец, величина градиента представляет собой целое значение от 0 до 360 (вы использовали uint32_t). Тогда вы сохраняете только младший байт! Конечно, это неправильно . Вам необходимо отобразить от [0,360] до [0,255]. Как? Вы можете насытить (если больше 255 установить на 255) или применить линейное масштабирование (*255/360). Конечно, вы можете делать и другие вещи, но это не важно.

Здесь вы можете увидеть результат в увеличенной версии трех случаев: насыщенный, масштабный, только LSB (неправильно): Comparison of different saving modalities При неправильной версии вы видите темные пиксели, значение которых должно быть больше 255.

...