4

Goal

I'm trying to move towards Boost GIL to replace some similar functionality I've implemented that is reaching the end of its maintainable life.

I have existing code that works with 24 BPP, 8bit RGB images using uint8_t*. I can't change that as the same interface is used to expose images from different places (e.g. OpenGL buffers) and there's already quite a lot of code.

Therefore I'm trying to use GIL in small steps, starting by reading a file and copying the pixels byte by byte into a std::vector<uint8_t> which I can use to manage the storage, but still get a uint8_t* by using &vector[0].

This can be dropped in transparently behind the existing interfaces until it's at a point where refactoring makes sense.

What I've tried

I thought this should be a simple case of using copy_pixels() with two appropriately constructed views.

I've put together a minimal, complete example that illustrates the sum of what I've manage to achieve by looking over the documents and trying things out:

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

Where I'm stuck

This doesn't compile:

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

Which is fairly self-explanatory - an RGB pixel can't be converted to a single unsigned char automatically. I thought I'd try using copy_and_convert_pixels() to fix this, but I can't see a way around the 3:1 (i.e. I have 3 unsigned chars in the output for every pixel in the source image) problem with these conversions. Conversion seems to be more aimed at colourspace conversions (e.g. RGB->HSV) or packing changes.

Community
  • 1
  • 1
Flexo
  • 87,323
  • 22
  • 191
  • 272
  • Just wondering, did you manage to get this code working in the end? I have just start to attempt to use GIL too, and getting the data from file to OpenGL via GIL is proving a challenge. Have you got a working example that I can have a look at? – thecoshman Jun 22 '13 at 10:29
  • @thecoshman - looking through the code I was working on the the accepted answer is pretty close to what I used. I extracted an MWE from my code base and answered this question with that. – Flexo Jun 22 '13 at 11:14

4 Answers4

5

I would just push_back each color of the rgb8_pixel_t individually:

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));
  }
...
}

...but I'm not an expert on GIL either.

Cubbi
  • 46,567
  • 13
  • 103
  • 169
  • This looks like it's going to be the only safe way I guess (with the possible problem of calling `push_back` 3 times still if `num_channels` was less than 3, butt that's fixable with the `operator()` overloads) – Flexo Nov 29 '11 at 10:08
1

I just ran into this same problem; here for possible future reference is the answer I can come up with now that I've solved it for myself:

That copy_pixels approach is good. The only problem is the destination type. If you happen to know that rgb8_pixel_t is formatted in memory as if it were three consecutive uint8_t's, then all you need to do is something like this:

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

That's copied from my project, more or less; I'm using a 32-bit image but it should work the same for any other single, hardcoded image type. (I haven't learned how to use the "any_" stuff from GIL, so I can't comment on dynamically determined image types.)

In the code above, I somewhat crudely confirm that an rgba8_pixel_t is what OpenGL will see as an "INT_8_8_8_8" or whatever, by doing that static_assert. I think it's probably better to take that information from the GIL documentation than to make a guess and try to confirm it with an assertion, but I can't seem to find a clear statement of it there (I'm also new to GIL, so maybe I'm just missing it). But it seems fairly clear that this is part of the intention of the design of GIL's pixel types. For instance, the GIL design guide says at one point, "The most commonly used pixel is a homogeneous pixel whose values are together in memory." "Together in memory" seems to be just what I'm looking for. Just after that, the guide talks about "planar" pixel types, in which the color channel values for one pixel are NOT stored together in memory. It would be strange to go to all the trouble of supporting that distinction as carefully as they do, and then not actually bother to make the interleaved pixel type pack its color values tightly together in memory.

Anyway, I've demonstrated in my own project that this approach works with at least the version of Boost that I'm using (1.57), and I claim that if a future version changes this, then my static_assert will almost certainly catch it.

(Another approach to potentially fall back on would be to actually go ahead and use a planar pixel to map between your uint_8_t array and the rgb8_pixel_t that for_each_pixel gives you:

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

But this isn't really superior to the at_c strategy. It's just kind of a neat example I guess. *_planar_ptr_t is surprisingly smart!)

Also note that in today's C++ you don't have to make a separate type to capture the body of your "for each" loop; you can use an anonymous function, as above. (I wrap mine in std::function because I guess GIL does some internal copy-assignment of the function object, or something like that, and the compiler gets mad if a naked anonymous function is passed in. I guess the std::function wrapping may reduce efficiency a bit here; in my case that doesn't seem to be important.)

mjwach
  • 1,174
  • 2
  • 9
  • 25
  • Hmm, since writing this answer I've learned a bit more about C++, and now I guess maybe I was wrong to look for assurance in GIL's documentation that pixel structs would be conveniently packed in memory? Because C++ just doesn't allow that to ever be guaranteed, if I understand correctly (which I might not). I'm still pretty satisfied with my static_assert strategy, though. :( – mjwach Mar 22 '15 at 22:36
0

A complete, simplified form of the actual code I ended up using is:

#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);
}

I originally had the following before I included any of the GIL headers:

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

I'm not sure exactly what issue they fixed with the version of boost I had at the time, but they don't seem to be required with 1.48.

Flexo
  • 87,323
  • 22
  • 191
  • 272
  • Did you my any chance look at using `boost::gil::get_color(p, boost::gil::red_t()` or `p[0]` instead of using `boost::gil::at_c<0>(p)`. I am not exactly sure what the benefits of each would be. Though I think using the 'get_colour(p, red)' style is more flexible as you are no longer dependent on what order the colours are. – thecoshman Jun 22 '13 at 15:25
  • @thecoshman I didn't see that, in my instance I knew all the input images were going to be RGB8 always so that wasn't much of a concern. – Flexo Jun 22 '13 at 20:40
  • well yes, from what I can gather, we have both gone done the rout that requires the file type to be known at compile time. These three techniques are all the same effectively, I think. Have you had much luck with code that can just load (with in reason) any format of png file and get the raw data? The dynamic loaders seem to load to a 'any_image' or 'any_view' which it seems do not support reading the raw pixel data, for performance reasons if I understand it correctly. – thecoshman Jun 23 '13 at 09:33
0

Here's some code I once used:

 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]
 }

It's only for grayscale, but presumably the coloured version is similar.

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • Is that cast for the argument of the constructor of the `interleaved_view` really safe? It looks like it might (at least theoretically) suffer from alignment and/or padding problems. – Flexo Nov 28 '11 at 18:40
  • I'm sure it's fine for 8-bit, but there may be problems for the coloured version... I'm not sure. Make an array of the correct type to begin with, I suppose. – Kerrek SB Nov 28 '11 at 18:40