Adventures in color type conversions (fun with templates, anyone?)

Status
Not open for further replies.

christoph

Well-known member
I'm trying to come up with flexible color classes and conversions between them (sumotoy and I are discussing a flexible framework for writing display drivers, but more flexible than adafruit's gfx library). Have a look at this please:

colors.h
Code:
#ifndef COLORS_H
#define COLORS_H

#include <stdint.h>

namespace thirdExperiment
{

using namespace std;

namespace channel
{

static constexpr struct left_aligned_t {} left_aligned = left_aligned_t();
static constexpr struct right_aligned_t {} right_aligned = right_aligned_t();

template<typename T, unsigned int Offset_, unsigned int Width_>
class Proxy
{
  public:
    /* Some checks and typedefs */
    static_assert(std::is_unsigned<T>::value, "ChannelProxy: T must be an unsigned arithmetic type.");
    typedef T data_type;
    static constexpr unsigned int Width = Width_;
    static_assert(Width <= 8, "ChannelProxy: Width must be <= 8.");
    static constexpr unsigned int Offset = Offset_;
    static_assert((Offset + Width) <= 8*sizeof(T), "ChannelProxy: Channel is out of the data type's bounds. Check data type, offset and width.");

    Proxy(T& data) : data_(data) {}

    uint8_t read(right_aligned_t) const
    {
      return ((data_ & read_mask) >> Offset);
    }

    uint8_t read(left_aligned_t) const
    {
      return read(right_aligned) << (8-Width);
    }

    void write(const uint8_t& value, right_aligned_t)
    {
      // input data is right aligned
      data_ = (data_ & write_mask) | ((value & value_mask) << Offset);
    }

    void write(const uint8_t& value, left_aligned_t)
    {
      // input data is left aligned, so shift right to right align, then write
      write(value >> (8-Width), right_aligned);
    }

  private:
    static constexpr uint8_t value_mask = (uint8_t)((1<<Width)-1);
    static constexpr T read_mask = (value_mask << Offset);
    static constexpr T write_mask = (T)~read_mask;
    T& data_;
};

} // namespace channel

struct RGB24
{
  typedef channel::Proxy<uint8_t, 0, 8> proxy;
  typedef channel::Proxy<const uint8_t, 0, 8> const_proxy;

  RGB24() : r_(0), g_(0), b_(0) {}
  RGB24(const uint8_t& r, const uint8_t& g, const uint8_t& b)
    : r_(r), g_(g), b_(b) {}

  // unfortunately, we need different proxies for read and write access (data_type constness)
  const_proxy r() const {return const_proxy(r_);}
  proxy r() {return proxy(r_);}
  const_proxy g() const {return const_proxy(g_);}
  proxy g() {return proxy(g_);}
  const_proxy b() const {return const_proxy(b_);}
  proxy b() {return proxy(b_);}

  template <typename From>
  RGB24& operator=(const From& from)
  {
    convert(*this, from);
    return *this;
  }

  uint8_t r_;
  uint8_t g_;
  uint8_t b_;
};

struct RGB565 // 16 bits: MSB | RRRRR GGGGGG BBBBB | LSB
{
  typedef uint16_t data_type;

  typedef channel::Proxy<data_type, 0, 5> b_proxy;
  typedef channel::Proxy<const data_type, 0, 5> const_b_proxy;
  typedef channel::Proxy<data_type, 5, 6> g_proxy;
  typedef channel::Proxy<const data_type, 5, 6> const_g_proxy;
  typedef channel::Proxy<data_type, 11, 5> r_proxy;
  typedef channel::Proxy<const data_type, 11, 5> const_r_proxy;

  RGB565() : data_(0) {}

  template <typename alignment_type = channel::right_aligned_t>
  RGB565(const uint8_t& r_, const uint8_t& g_, const uint8_t& b_, alignment_type = alignment_type())
  {
    alignment_type alignment;
    r().write(r_, alignment);
    g().write(g_, alignment);
    b().write(b_, alignment);
  }

  template <typename From>
  RGB565& operator=(const From& from)
  {
    convert(*this, from);
    return *this;
  }

  const_r_proxy r() const {return const_r_proxy(data_);}
  r_proxy r() {return r_proxy(data_);}
  const_g_proxy g() const {return const_g_proxy(data_);}
  g_proxy g() {return g_proxy(data_);}
  const_b_proxy b() const {return const_b_proxy(data_);}
  b_proxy b() {return b_proxy(data_);}

  data_type data_;
};

typedef bool Monochrome;

template <typename To, typename From>
void convert(To& to, const From& from)
{
  to.r().write(from.r().read(channel::left_aligned), channel::left_aligned);
  to.g().write(from.g().read(channel::left_aligned), channel::left_aligned);
  to.b().write(from.b().read(channel::left_aligned), channel::left_aligned);
}

template <>
void convert<RGB565, Monochrome>(RGB565& to, const Monochrome& from)
{
  to.data_ = from ? 0xFFFF : 0;
}

template<>
void convert<RGB24, Monochrome>(RGB24& to, const Monochrome& from)
{
  to.r_ = from ? 0xFF : 0;
  to.g_ = from ? 0xFF : 0;
  to.b_ = from ? 0xFF : 0;
}


}
#endif // COLORS_H

main.cpp
Code:
#include <WProgram.h>

#include <algorithm>
#include <array>

#include "colors.h"

#define N 1000

static const uint8_t analogPin = A0; // leave unconnected, we need noise

template <typename T>
T generateRGBfromAdc()
{
  uint8_t val = analogRead(analogPin);
  uint8_t r = val;
  val = analogRead(analogPin);
  uint8_t g = val;
  val = analogRead(analogPin);
  uint8_t b = val;
  return T(r,g,b);
}

template <typename T>
T generateMonochromeFromAdc()
{
  uint8_t val = analogRead(analogPin);
  return T(val & 0x01);
}

void setup()
{
  elapsedMicros t;
  uint32_t us;

  while(!Serial.available());
  while(Serial.available())
  {
    Serial.read();
  }

  analogReadResolution(16);

  {
    using namespace thirdExperiment;
    std::array<RGB24, N> rgb;
    std::fill(rgb.begin(), rgb.end(), RGB24());

    std::array<RGB565, N> rgb565;
    std::generate(rgb.begin(), rgb.end(), generateRGBfromAdc<RGB565>);
    Serial.printf("rgb565[0].r = 0x%02x, .g = 0x%02x, .b = 0x%02x\n",
                  rgb565[0].r().read(channel::right_aligned),
                  rgb565[0].g().read(channel::right_aligned),
                  rgb565[0].b().read(channel::right_aligned));

    Serial.printf("Third Experiment (channel templates): RGB565 to RGB24\n");
    t = elapsedMicros();
    std::copy(rgb565.begin(), rgb565.end(), rgb.begin());
    us = t;
    Serial.printf("time = %u us\n", us);
    Serial.printf("rgb[0].r = 0x%02x, .g = 0x%02x, .b = 0x%02x\n", rgb[0].r_, rgb[0].g_, rgb[0].b_);

    std::array<Monochrome, N> m;
    std::generate(m.begin(), m.end(), generateMonochromeFromAdc<Monochrome>);
    Serial.printf("Third Experiment (channel templates): Monochrome to RGB24\n");
    t = elapsedMicros();
    std::copy(m.begin(), m.end(), rgb.begin());
    us = t;
    Serial.printf("time = %u us\n", us);
    Serial.printf("rgb[0].r = 0x%02x, .g = 0x%02x, .b = 0x%02x\n", rgb[0].r_, rgb[0].g_, rgb[0].b_);
    Serial.printf("rgb[1].r = 0x%02x, .g = 0x%02x, .b = 0x%02x\n", rgb[1].r_, rgb[1].g_, rgb[1].b_);

  }

  pinMode(LED_BUILTIN, OUTPUT);
}

void loop()
{
  digitalWriteFast(LED_BUILTIN, true);
  delay(500);
  digitalWriteFast(LED_BUILTIN, false);
  delay(500);
}

So it's possible to create custom color classes that a device-specific encoding (look at the RGB565 class for an example) and to convert between them with a generic method as long as they expose a common interface for the templates. Any other conversion can be achieved with a template specialization of convert(). I'm still working on this, but it looks promising. It seems that the compiler can highly optimize this code.

Regards

Christoph
 
Status
Not open for further replies.
Back
Top