【问题标题】:Using Boost.GIL to convert an image into "raw" bytes使用 Boost.GIL 将图像转换为“原始”字节
【发布时间】:2015-01-26 09:54:02
【问题描述】:

目标

我正在尝试转向 Boost GIL,以替换我已实现的一些类似功能,这些功能已达到其可维护生命周期的终点。

我现有的代码可以使用uint8_t* 处理 24 BPP、8 位 RGB 图像。我无法改变这一点,因为相同的接口用于从不同的地方(例如 OpenGL 缓冲区)公开图像,并且已经有相当多的代码。

因此,我尝试以小步骤使用 GIL,首先读取文件并将像素逐字节复制到 std::vector<uint8_t> 中,我可以用它来管理存储,但仍然通过使用获得 uint8_t* &vector[0].

这可以透明地放在现有接口后面,直到重构有意义为止。

我尝试过的

我认为这应该是使用 copy_pixels() 和两个适当构造的视图的简单案例。

我整理了一个最小的、完整的示例,说明了我通过查看文档和尝试实现的目标的总和:

#include <boost/gil/rgb.hpp>
#include <boost/gil/extension/io/png_dynamic_io.hpp>
#include <stdint.h>
#include <vector>

int main() {
  std::vector<uint8_t> storage;
  {
    using namespace boost::gil;
    rgb8_image_t img;
    png_read_image("test.png", img);

    // what should replace 3 here to be more "generic"?
    storage.resize(img.width()*img.height()*3);

    // doesn't work, the type of the images aren't compatible.
    copy_pixels(const_view(img), 
                interleaved_view(img.width(), img.height(), &storage[0], 3*img.width()));
  }
}

我被困在哪里

这不会编译:

error: cannot convert ‘const boost::gil::pixel<unsigned char, boost::gil::layout<boost::mpl::vector3<boost::gil::red_t, boost::gil::green_t, boost::gil::blue_t> > >’ to ‘unsigned char’ in assignment

这是不言自明的 - RGB 像素无法自动转换为单个 unsigned char。我想我会尝试使用copy_and_convert_pixels() 来解决这个问题,但是我看不到 3:1 的解决方法(即,对于源图像中的每个像素,我在输出中有 3 个unsigned chars)问题转换。转换似乎更针对色彩空间转换(例如 RGB->HSV)或包装更改。

【问题讨论】:

  • 只是想知道,您最终是否设法让这段代码正常工作?我也刚刚开始尝试使用 GIL,通过 GIL 从文件中获取数据到 OpenGL 是一个挑战。你有我可以看看的工作示例吗?
  • @thecoshman - 查看我正在处理的代码,接受的答案与我使用的非常接近。我从我的代码库中提取了一个 MWE,并用它回答了这个问题。

标签: c++ image boost boost-gil


【解决方案1】:

我会单独 push_back rgb8_pixel_t 的每种颜色:

struct PixelInserter{
        std::vector<uint8_t>* storage;
        PixelInserter(std::vector<uint8_t>* s) : storage(s) {}
        void operator()(boost::gil::rgb8_pixel_t p) const {
                storage->push_back(boost::gil::at_c<0>(p));
                storage->push_back(boost::gil::at_c<1>(p));
                storage->push_back(boost::gil::at_c<2>(p));
        }
};

int main() {
  std::vector<uint8_t> storage;
  {
    using namespace boost::gil;
    rgb8_image_t img;
    png_read_image("test.png", img);
    storage.reserve(img.width() * img.height() * num_channels<rgb8_image_t>());
    for_each_pixel(const_view(img), PixelInserter(&storage));
  }
...
}

...但我也不是 GIL 方面的专家。

【讨论】:

  • 这看起来将是我猜测的唯一安全方式(如果num_channels 小于 3,则可能会出现调用 push_back 3 次的问题,但可以使用 @987654324 修复@重载)
【解决方案2】:

我刚刚遇到了同样的问题;现在我已经为自己解决了这个问题,我现在可以想出答案,以供将来参考:

copy_pixels 方法很好。唯一的问题是目标类型。如果你碰巧知道 rgb8_pixel_t 在内存中被格式化为三个连续的 uint8_t,那么你需要做的就是这样:

boost::gil::rgba8_image_t image;
boost::gil::png_read_image(filePath, image);
auto view = boost::gil::view(image);
typedef decltype(view)::value_type pixel;
static_assert(sizeof(pixel) == 4, "the glTexImage2D call below assumes this");

pixel imageData[view.width() * view.height()];
boost::gil::copy_pixels(view, boost::gil::interleaved_view(view.width(), view.height(), imageData, view.width() * sizeof(pixel)));

gl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width(), image.height(), 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, imageData);

这或多或少是从我的项目中复制的;我使用的是 32 位图像,但对于任何其他单一的硬编码图像类型,它应该可以正常工作。 (我还没有学会如何使用 GIL 中的“any_”东西,所以我无法评论动态确定的图像类型。)

在上面的代码中,我通过执行 static_assert 粗略地确认 rgba8_pixel_t 是 OpenGL 将看到的“INT_8_8_8_8”或其他内容。我认为从 GIL 文档中获取这些信息可能比猜测并尝试用断言确认它更好,但我似乎无法在那里找到明确的声明(我也是 GIL 的新手,所以也许我只是想念它)。但似乎相当清楚,这是 GIL 像素类型设计意图的一部分。例如,GIL 设计指南有一次说,“最常用的像素是同质像素,其值一起存储在内存中。” “一起在记忆中”似乎正是我正在寻找的。紧接着,指南谈到了“平面”像素类型,其中一个像素的颜色通道值不会一起存储在内存中。像他们一样小心翼翼地支持这种区别,然后实际上不费心使交错像素类型将其颜色值紧密地打包在内存中,这会很奇怪。

无论如何,我已经在我自己的项目中证明了这种方法至少适用于我正在使用的 Boost 版本 (1.57),我声称如果未来的版本改变了这一点,那么我的 static_assert 几乎肯定会抓住它。

(另一种可能依赖的方法是实际继续并使用平面像素在您的 uint_8_t 数组和 for_each_pixel 为您提供的 rgb8_pixel_t 之间进行映射:

boost::gil::rgba8_image_t image;
boost::gil::png_read_image(filePath, image);
auto view = boost::gil::view(image);
uint8_t data[view.width() * view.height() * view.num_channels()];

using boost::gil::rgba8_pixel_t;
uint8_t* cursor = data;
boost::gil::for_each_pixel(view, std::function<void(rgba8_pixel_t)>(
        [&cursor](rgba8_pixel_t pixel)
        {
            boost::gil::rgba8_planar_ptr_t pixelInData(cursor++, cursor++, cursor++, cursor++);
            *pixelInData = pixel;

            // if data were an array of rgba8_pixel_t's, then we could just do this and be done with it:
            // *cursor++ = pixel;
            // (but in that case we might as well use copy_pixels!)
        }));

gl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width(), image.height(), 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, data);

但这并不真正优于 at_c 策略。我猜这只是一个很好的例子。 *_planar_ptr_t 非常聪明!)

还要注意,在当今的 C++ 中,您不必创建单独的类型来捕获“for each”循环的主体;您可以使用匿名函数,如上所述。 (我将我的包装在 std::function 中,因为我猜 GIL 会对函数对象进行一些内部复制分配,或者类似的东西,如果传入一个裸匿名函数,编译器会发疯。我猜是 std::function在这里包装可能会降低效率;在我的情况下,这似乎并不重要。)

【讨论】:

  • 嗯,自从写了这个答案后,我对 C++ 有了更多的了解,现在我想也许我在 GIL 的文档中寻找像素结构可以方便地打包在内存中的保证是错误的?因为如果我理解正确(我可能不会),C++ 就不允许保证这一点。不过,我仍然对我的 static_assert 策略非常满意。 :(
【解决方案3】:

这是我曾经使用过的一些代码:

 unsigned char * buf = new unsigned char[w * h];

 boost::gil::gray8_view_t image =
          boost::gil::interleaved_view(w, h, (boost::gil::gray8_pixel_t*)buf, w);

 for (size_t i = 0; i < ...; ++i)
 {
   boost::gil::gray8_view_t::x_iterator it = image.row_begin(i);

   // use it[j] to access pixel[i][j]
 }

只针对灰度,不过想必彩色版也差不多。

【讨论】:

  • 对于interleaved_view 的构造函数参数的强制转换真的安全吗?看起来它可能(至少在理论上)存在对齐和/或填充问题。
  • 我确定 8 位没问题,但彩色版本可能有问题...我不确定。我想,首先创建一个正确类型的数组。
【解决方案4】:

我最终使用的实际代码的完整简化形式是:

#include <boost/gil/rgb.hpp>
#include <boost/gil/extension/io/png_dynamic_io.hpp>
#include <vector>
#include <string>
#include <cstdint>

struct dimension {
  int w,h;
};

namespace {
struct PixelInserter {
    std::vector<uint8_t>* storage;
    PixelInserter(std::vector<uint8_t>* s) : storage(s) {}
    void operator()(boost::gil::rgb8_pixel_t p) const {
        using boost::gil::at_c;
        storage->push_back(at_c<0>(p));
        storage->push_back(at_c<1>(p));
        storage->push_back(at_c<2>(p));
    }
};

// This could probably share code with the PixelInserter
struct PixelWriter {
    const uint8_t *pixels;
    PixelWriter(const uint8_t *pixels) : pixels(pixels) {}
    void operator()(boost::gil::rgb8_pixel_t& p) {
        using boost::gil::at_c;
        at_c<0>(p) = *pixels++;
        at_c<1>(p) = *pixels++;
        at_c<2>(p) = *pixels++;
    }
};
}

void savePNG(const std::string& filename, const uint8_t *pixels, const dimension& d) {
    boost::gil::rgb8_image_t img(d.w, d.h);
    for_each_pixel(view(img), PixelWriter(pixels));
    boost::gil::png_write_view(filename, view(img));
}

std::vector<uint8_t> readPNG(const std::string& fn, dimension& d) {
    boost::gil::rgb8_image_t image_type;
    image_type img;
    png_read_image(fn, img);
    d.w = img.width();
    d.h = img.height();

    std::vector<uint8_t> storage;
    storage.reserve(d.w*d.h*boost::gil::num_channels<image_type>());
    for_each_pixel(const_view(img), PixelInserter(&storage));
    return storage;
}

int main(int argc, char **argv) {
  dimension d;
  const std::vector<uint8_t> pixels = readPNG(argv[1], d);
  savePNG(argv[2], &pixels[0], d);
}

在包含任何 GIL 标头之前,我最初拥有以下内容:

#define png_infopp_NULL (png_infopp)NULL
#define int_p_NULL (int*)NULL

我不确定他们用我当时的 boost 版本解决了哪些问题,但 1.48 似乎不需要它们。

【讨论】:

  • 你有没有想过使用boost::gil::get_color(p, boost::gil::red_t()p[0] 而不是使用boost::gil::at_c&lt;0&gt;(p)。我不确定每个人的好处是什么。虽然我认为使用 'get_colour(p, red)' 样式更灵活,因为您不再依赖于颜色的顺序。
  • @thecoshman 我没有看到,在我的例子中,我知道所有输入图像总是 RGB8,所以这不是什么大问题。
  • 嗯,是的,据我所知,我们都已经完成了需要在编译时知道文件类型的溃败。我认为这三种技术的效果都是一样的。您是否对可以加载(有理由)任何格式的 png 文件并获取原始数据的代码感到很幸运?如果我理解正确,出于性能原因,动态加载器似乎加载到似乎不支持读取原始像素数据的“any_image”或“any_view”。
猜你喜欢
  • 1970-01-01
  • 2019-11-08
  • 1970-01-01
  • 2019-05-06
  • 2016-04-11
  • 2013-03-20
  • 2013-11-10
  • 2014-10-01
  • 2022-01-09
相关资源
最近更新 更多