Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Describing pixel format information in C++ in a way that is usable at both compile-time and runtime

I have a library that does operations on pixels. The pixels can by in many different formats. I am looking for an effective way to describe the formats in the library API (internally and externally).

For some classes the pixel format is a template argument, for others it is a runtime argument. So the pixel formats need to be usable both runtime (as constructor or function argument) and compile time (as template argument). I want to describe the pixel formats only once.

What I have now is something like this:

enum class color_space : uint8_t { rgb, cmyk /* , etc... */ };

struct pixel_layout {
    color_space space;
    uint8_t channels;
    /* etc... */
};

template <color_space ColorSpace, uint8_t Channels /* etc.. */>
struct pixel_type {
    static constexpr color_space space = ColorSpace;
    static constexpr uint8_t channels = Channels;
    /* etc... */

    static constexpr pixel_layout layout() {
        return {space, channels /* , etc... */ };
    }
};

struct rgb  : public pixel_type<color_space::rgb, 3 /* , etc... */ > {};
struct rgba : public pixel_type<color_space::rgb, 4 /* , etc... */ > {};

This works fairly well. I can use these as runtime and compile time arguments:

template <class PixelType>
class image { };

struct transform {
    transform(const pixel_layout from, const pixel_layout to)
        : from(from), to(to) { /* ... */ }

    pixel_layout from;
    pixel_layout to;
};

Also convert from compile-time type to runtime type:

transform(rgb::layout(), rgba::layout());

However, duplicating and storing the pixel_layout details of the pixel types whenever they are used at runtime seems silly to me. Conceptually, all the program should need is an ID/address/reference to a specific pixel_type and a way to retrieve the associated properties (color space, channels, etc) at both compile time and runtime.

Also, if I want to get a derived property from a pixel type, I need to implement it on pixel_layout if I want to avoid duplicating logic. Then to use it at compile time, I need to go from pixel_type<...> class to pixel_layout instance to derived property. That too, seems a little silly.

Can I avoid passing around the pixel_layout details, and instead use some kind of reference to the pixel_type<...> (sub)classes?

I tried using enums, because enums work as template argument & function argument. But I struggled to get from enum value (e.g. rgba) to pixel type property (e.g. 4 channels) at runtime & compile time in an idiomatic C++ way.

Also, enums as template arguments give far less useful diagnostics during compile error. For example, I get image<(pixel_type)2> rather than image<rgba> in compile error messages with clang. So this does not seem like a useful approach.

like image 623
molf Avatar asked Jan 25 '16 11:01

molf


2 Answers

Using non-type reference template parameters might be a solution. See http://en.cppreference.com/w/cpp/language/template_parameters . For example like this:

#include <iostream>
#include <cstdint>
#include <array>

enum class color_space : std::uint8_t { rgb, cymk, other };

// PIXEL LAYOUT
// Can be created/modified at runtime, but a predefined set of pixel_layouts
// exists for compile-time use.
struct pixel_layout {
    color_space space;
    std::uint8_t channels;
};

constexpr bool operator==(const pixel_layout& a, const pixel_layout& b) {
    return (a.space == b.space) && (a.channels == b.channels);
}
constexpr bool operator!=(const pixel_layout& a, const pixel_layout& b) {
    return (a.space != b.space) || (a.channels != b.channels);
}

// Predefined pixel_layout instances, for use as template arguments
// As static constexpr members of class, to make sure they have external linkage,
// required for use as reference template arguments.
struct default_pixel_layouts {
    static constexpr pixel_layout rgb{ color_space::rgb, 3 };
    static constexpr pixel_layout cymk{ color_space::cymk, 4 };        
};

// Definitions for the pixel_layouts
constexpr pixel_layout default_pixel_layouts::rgb;
constexpr pixel_layout default_pixel_layouts::cymk;


// PIXEL TYPE
// Takes pixel_layout reference as non-type template argument.
template<const pixel_layout& Layout>
struct pixel {
    static constexpr const pixel_layout& layout = Layout;

    // Because layout is constexpr, can use its members (e.g. channels),
    // for example as template argument.
    // Here size of pixel depends on number of channels in pixel_layout
    std::array<std::uint32_t, layout.channels> data;
};

// RGB and CYMK pixel_types as type aliases
using rgb = pixel<default_pixel_layouts::rgb>;
using cymk = pixel<default_pixel_layouts::cymk>;


// IMAGE
// Takes pixel type as template argument.
template<class PixelType>
class image {
public:
    using pixel_type = PixelType;
};


// TRANSFORM
// Takes pixel_layouts to transform from/to at runtime. Can for with the predefined
// ones, but also with new ones creates at runtime.
class transform {
private:
    const pixel_layout& from_;
    const pixel_layout& to_;

public:
    transform(const pixel_layout& from, const pixel_layout& to) :
    from_(from), to_(to) { }

    // Example: function working on an image
    template<class Image>
    void run(Image& img) {
        // Need to make sure that Image's pixel_layout (compile-time) matches
        // pixel_layout of the transform (runtime).
        if(Image::pixel_type::layout != from_)
            std::cout << "Wrong pixel type on input image" << std::endl;
        else
            std::cout << "transforming..." << std::endl;
    }

};



int main() {
    image<rgb> rgb_img;
    image<cymk> cymk_img;

    // Transform from rgb to cymk
    transform tr(default_pixel_layouts::rgb, default_pixel_layouts::cymk);  
    tr.run(rgb_img); // ok
    tr.run(cymk_img); // error: input to run() must have rgb pixel_layout

    // Creating a new pixel_layout at runtime
    pixel_layout custom_layout = { color_space::other, 10 };
    transform tr2(custom_layout, default_pixel_layouts::cymk);  

    return 0;
}

http://coliru.stacked-crooked.com/a/981e1b03b3b815c5

For the use cases where the pixel_layout is used at compile time, the different available pixel_layout instances need to be instantiated as global static constexpr objects.

pixel_type then instantiates to different classes, depending on the pixel_layout& given as template argument.

But they still can be used at runtime also.

like image 52
tmlen Avatar answered Oct 11 '22 04:10

tmlen


I would start by making rgb and rgba etc empty classes:

struct rgb{};
struct rgba{};
struct cmyk{};
//...

Use these instead of enums for templates to get better diagnostics.

Then you can define a bunch of free constexpr functions that yield specific data, e.g.

constexpr uint8_t channels(rgb) { return 3; }
constexpr uint8_t channels(rgba) { return 4; }

This way you can offer the values that make sense for any given format. If a feature is not available for a certain format, just do not offer the overload.

You can construct the pixel_layout using such functions where needed, but I would imagine that classes like transform would just get a template constructor and gather the information they need without a pixel_layout intermediate.

I might be missing some use cases, so you might need to adjust this a bit, but I hope it helps.

like image 31
Rumburak Avatar answered Oct 11 '22 04:10

Rumburak