【问题标题】:Calculating the vertical gradient of 2D image causes strange output计算二维图像的垂直梯度会导致奇怪的输出
【发布时间】:2020-05-05 16:00:19
【问题描述】:

我想对 .ppm 文件中的图像应用简单的派生/渐变滤镜 [-1, 0, 1]。

.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;(即只计算水平梯度)时,输出看起来是正确的:

当我改为设置grad_h = 0;(即只计算垂直梯度)时,输出很奇怪:

图像的一部分似乎被循环移动了,但我不明白为什么。此外,我尝试了许多图像,并且出现了同样的问题。

任何人都可以看到任何问题吗?非常感谢!

【问题讨论】:

  • 您可能会加载 RGB 图像吗?每个像素都有三个值,而不是一个。所以下面的相邻像素是 3*width 字节,而不是宽度。您的输出与该差异一致(图像中的三个垂直带)。
  • 如果您需要更详细的帮助,请阅读minimal reproducible example,然后阅读edit您的问题。它目前没有足够的信息来写答案。
  • 你能给我们你输入图像的表示吗?
  • 抱歉没有包含更多代码——我担心添加太多。我用一个最小的例子更新了这个问题。希望没事?谢谢!
  • 你知道梯度幅度的范围吗?为什么你使用 32 位整数并且只保存最低有效字节?

标签: c++ image-processing


【解决方案1】:

好的,第一个线索是图像看起来是圆形移动的。这暗示步幅是错误的。你的问题的核心很简单:

    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()));

但是在documentation你可以阅读:

每个 PPM 图像包含以下内容:

  1. 用于识别文件类型的“幻数”。 ppm 图像的幻数是两个 字符“P6”。
  2. 空白(空格、TAB、CR、LF)。
  3. 宽度,格式为十进制的 ASCII 字符。
  4. 空白。
  5. 高度,同样是 ASCII 十进制。

[...]

宽度是列,高度是行。这就是你在实现图像处理时遇到的经典错误:交换行和列。

从教学的角度来看,你为什么会犯这个错误?我的猜测:糟糕的调试工具。在根据您的问题制作一个工作示例之后(如果您提供了MCVE,我会节省的工作量),我运行到图像加载结束并使用 Image Watch 来查看带有@mem(raw_image_data, UINT8, 3, n_cols, n_rows, n_cols*3) 的图像内容。结果:

好的,让我们尝试交换它们:@mem(raw_image_data, UINT8, 3, n_rows, n_cols, n_rows*3)。结果:

好多了。不幸的是,我不知道如何在 Image Watch @mem 伪命令中指定 RGB 而不是 BGR,所以颜色错误。

然后我们回到您的代码:请在编译时打开所有警告。然后我会使用更多std::stream 功能来解析您的输入,而更少使用std::stoi()find()。使用std::vector 避免内存分配,并为图像创建一个(可能是模板)类。即使您坚持使用指向指针的指针,也不要为每一行创建多个 new:为第 0 行的指针创建一个 new,并让其他指针指向它:

    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(错误): 使用错误的版本,您会看到值应大于 255 的暗像素。

【讨论】:

  • 哇,非常感谢所有这些!对我来说非常愚蠢的错误。这与大学作业有关,我们收到的文档交换了行和列的顺序,而且我们使用的是方形图像,所以大部分都没有注意到——虽然我应该跟进。另外,感谢您的其他建议。关于提供的 MCVE,我应该给出更多还是更少的代码?只是为了让我下次知道。谢谢!
  • 您的代码就快到了。下次提供可以复制并粘贴到编辑器中并运行/调试的完整文件。因此,测试您的代码需要一秒钟,而无需经历添加正确包含和执行多次复制/粘贴的痛苦。
  • 会的!再次感谢。
猜你喜欢
  • 2023-01-16
  • 2016-06-07
  • 1970-01-01
  • 1970-01-01
  • 2014-04-02
  • 2020-04-03
  • 1970-01-01
  • 2013-02-08
  • 2015-01-21
相关资源
最近更新 更多