如何在C ++中编写易于扩展的颜色类集

克里斯多夫

背景

我正在使用许多不同的显示器(硬件)和不同的画布(是画布的复数吗?)。每个都可以使用不同的颜色。示例案例:

  1. RGB16画布-> RGB16显示(这很简单,但我不希望仅使用异国组合)
  2. RGB16画布-> RGB24显示
  3. 单色画布-> RGB16显示,其中“ true”必须在运行时显示为颜色集。

目前,我确实在处理mono-> rgb16组合(3),以红色和可调光显示所有内容。可能出现的每个显示器也可能具有略有不同的颜色类型。

我想拥有什么

我想要一个易于扩展的颜色类别(C ++)集。我的目标是能够编写简单的作业,例如

Monochrome m; // default value set at runtime
RGB556 rgb; // default value set at runtime

rgb = m; // conversion function known at compile time

并且

pixelBuffer<Monochrome,w*h> src;
pixelBuffer<RGB556,w*h> dest;

std::copy(src.begin(), src.end(), dest.begin());

使用模板应该可以做到这一点。但是,我不确定该怎么做以及如何使整个事情保持足够简单,以便以后可以添加更多颜色类型而不必重写大多数代码,但仍然可以影响细节。

我确信可以编写一个带有类型的类模板,并自动提供24位RGB的转换方法,以便可以将任何颜色转换为该颜色,然后将其“向下”转换为小于该颜色的任何内容。

我尝试过的

好吧,老实说,没有什么特别的。我曾考虑过使用CRTP为常用的方法提供编译时继承,但是我未能提出一种可以编译并执行所需功能的实现。当我还有其他东西时,请在此处添加。

也欢迎使用完全不同的方法,因为我还没有编写使用我的颜色类必须提供的预定义接口的代码。


实验1:抽象界面与直接分配

正如评论和第一个答案中所建议的那样,我编写了一个简单的测试来了解时机。这是我的标题:

#ifndef COLORS_H
#define COLORS_H

#include <stdint.h>

class Color
{
  public:
    virtual uint8_t r() const = 0;
    virtual uint8_t g() const = 0;
    virtual uint8_t b() const = 0;
    virtual void setR(uint8_t v) = 0;
    virtual void setG(uint8_t v) = 0;
    virtual void setB(uint8_t v) = 0;
    static uint32_t copies;
};

class RGB24 : public Color
{
  public:
    RGB24(uint8_t r = 0, uint8_t g = 0, uint8_t b = 0)
      : r_(r),
      g_(g),
      b_(b)
    {

    }
    uint8_t r() const {return r_;}
    uint8_t g() const {return g_;}
    uint8_t b() const {return b_;}
    void setR(uint8_t v) {r_ = v;}
    void setG(uint8_t v) {g_ = v;}
    void setB(uint8_t v) {b_ = v;}

    RGB24& operator=(const Color& other)
    {
      copies++;
      setR(other.r());
      setG(other.g());
      setB(other.b());
      return *this;
    }

    RGB24& operator=(bool b)
    {
      copies++;
      if (b)
      {
        setR(0xFF);
        setG(0xFF);
        setB(0xFF);
      }
      return *this;
    }

  private:
    uint8_t r_;
    uint8_t g_;
    uint8_t b_;
};

class Monochrome : public Color
{
  public:
    Monochrome(bool b = false)
      : b_(b)
    {

    }
    uint8_t r() const {return b_ ? 0xFF : 0;}
    uint8_t g() const {return b_ ? 0xFF : 0;}
    uint8_t b() const {return b_ ? 0xFF : 0;}
    void setR(uint8_t v) {b_ = (v != 0);}
    void setG(uint8_t v) {b_ = (v != 0);}
    void setB(uint8_t v) {b_ = (v != 0);}

    Monochrome& operator=(const Color& other)
    {
      setR(other.r());
      setG(other.g());
      setB(other.b());
      return *this;
    }

  private:
    bool b_;

};

#endif // COLORS_H

这是我用来获取一些执行时间的代码(在目标硬件Cortex-M4上运行):

  RGB24 rgb[N];

  Monochrome m[N] = {true};
  elapsedMicros t;
  Serial.printf("Abstract Interface:\n");
  std::copy(m, &m[N], rgb);
  uint32_t us = t;
  Serial.printf("time = %u us\n", us);
  Serial.printf("rgb[N/2].r() = 0x%02x\n", rgb[N/2].r());
  Serial.printf("copies: %u\n\n", Color::copies);

  bool b[N] = {false};
  Serial.printf("Direct copy:");
  t = elapsedMicros();
  std::copy(b, &b[N], rgb);
  us = t;
  Serial.printf("time = %u us\n", us);
  Serial.printf("rgb[N/2].r() = 0x%02x\n", rgb[N/2].r());
  Serial.printf("copies: %u\n\n", Color::copies);

并输出:

Abstract Interface:
time = 1241 us
rgb[N/2].r() = 0x00
copies: 1000

Direct copy:
time = 157 us
rgb[N/2].r() = 0x00
copies: 2000

当我外推第一个数字1241微秒,以25 fps的分辨率显示128 * 128时,仅在颜色之间进行转换将占用50%的CPU时间。计算公式为:128*128*25 pixels per second * 1241 us / 1000 pixels = 0.51 seconds针对这种情况编写的画布/驱动程序组合可以在约0.1秒内完成,并且确实必须每秒复制和转换这么多像素,因为整个显示都是在每一帧中绘制的。

这种比较可能有点不公平,但是请允许我。我对剖析的经验不是很丰富;并且编写代码来对我所拥有的和我想要拥有的进行公平的比较是不可能的。关键是,Monochrome本质上只是一个布尔值,当我拥有适当的代码时,编译器应该可以对此进行优化。


实验2:使用模板化的转换功能分离类

正如André所建议的那样,我编写了RGB24类,将其定义为MostPreciseFormat和一个模板化的自由convert函数。也就是说,我不确定这是否正是他的意思:

class RGB24
{
  public:
    RGB24() : r_(0), g_(0), b_(0) {}
    uint8_t r_, g_, b_;

    uint8_t r() const {return r_;}
    uint8_t g() const {return g_;}
    uint8_t b() const {return b_;}

    void setR(const uint8_t& r) {r_ = r;}
    void setG(const uint8_t& g) {g_ = g;}
    void setB(const uint8_t& b) {b_ = b;}

    template<typename Other>
    RGB24(const Other& other)
    {
      convert(*this, other);
    }

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

typedef RGB24 MostPreciseFormat;

template <typename To, typename From>
void convert (To& to, const From& from)
{
//  Serial.println("Convert() called"); Serial.flush();
  MostPreciseFormat precise;
  precise.setR(from.r());
  precise.setG(from.g());
  precise.setB(from.b());
  to = precise;
}

template <>
void convert(RGB24& to, const bool& from)
{
  if (from)
  {
    to.setR(0xFF);
    to.setG(0xFF);
    to.setB(0xFF);
  }
  else
  {
    to.setR(0);
    to.setG(0);
    to.setB(0);
  }
}

这种转换需要209微秒才能完成1000个像素的转换,这似乎是合理的。但是我说对了吗?


我现在有什么

根据Andrés的回答,这可以按预期工作。它存在一些问题,可能需要在这里和那里进行一些重组。我尚未查看花费的CPU时间:

#include <bitset>
#include <iostream>
#include <stdint.h>

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

/* bool to RGB565 wouldn't work without this: */
template <>
void convert<RGB565, Monochrome>(RGB565& to, const Monochrome& from)
{
  to.data_ = from ? 0xFFFF : 0;
}

int main()
{
  cout << "Initializing RGB24 color0(0b11111101, 0, 0)\n\n";
  RGB24 color0(0b11111101, 0, 0);

  cout << "Initializing RGB24 color1(default)\n\n";
  RGB24 color1;

  cout << "color 1 = color0\n";
  color1 = color0;
  cout << "color1.r() = " << std::bitset<8*sizeof(uint8_t)>(color1.r().read(channel::right_aligned)) << "\n";
  cout << "color1.g() = " << std::bitset<8*sizeof(uint8_t)>(color1.g().read(channel::right_aligned)) << "\n";
  cout << "color1.b() = " << std::bitset<8*sizeof(uint8_t)>(color1.b().read(channel::right_aligned)) << "\n\n";

  cout << "Initializing RGB565 color2(0b10001, 0b100100, 0b10100)\n";
  RGB565 color2(0b10001, 0b100100, 0b10100);
  cout << "color2.data = " << std::bitset<8*sizeof(uint16_t)>(color2.data_) << "\n";
  cout << "color2.b(right aligned) = " << std::bitset<8*sizeof(uint8_t)>(color2.b().read(channel::right_aligned)) << "\n";
  cout << "color2.b(left aligned) = " << std::bitset<8*sizeof(uint8_t)>(color2.b().read(channel::left_aligned)) << "\n\n";

  cout << "color 0 = color2\n";
  color0 = color2;
  cout << "color0.b(right aligned) = " << std::bitset<8*sizeof(uint8_t)>(color0.b().read(channel::right_aligned)) << "\n";
  cout << "color0.b(left aligned) = " << std::bitset<8*sizeof(uint8_t)>(color0.b().read(channel::left_aligned)) << "\n\n";

  cout << "Initializing Monochrome color3(true)\n\n";
  Monochrome color3 = true;

  cout << "color 2 = color3\n";
  color2 = color3;
  cout << "color2.data = " << std::bitset<8*sizeof(uint16_t)>(color2.data_) << "\n";
  cout << "color2.b(right aligned) = " << std::bitset<8*sizeof(uint8_t)>(color2.b().read(channel::right_aligned)) << "\n";
  cout << "color2.b(left aligned) = " << std::bitset<8*sizeof(uint8_t)>(color2.b().read(channel::left_aligned)) << "\n\n";

  return 0;
}

使用此代码,将1000像素从RGB565转换为RGB24需要296 us,而源像素是由ADC噪声生成的(编译器在此处不能采用任何关于源数据的捷径)。使用convert()的模板特化功能,将1000像素从单色转换为RGB24需要313 us。

安德烈·萨西(AndréSassi)

首先,从这个问题来看,我认为解决方案必须具有非常快的执行时间,因此该要求应推动我们尝试解决问题的方法。

得出的结论是,我们应该避免在每个像素的基础上调用虚函数,否则CPU将不得不为每个像素进行一个额外的不必要的间接调用。但是,我们根本不应该避免使用虚函数,因为在每个画布操作中使用虚函数是完全可以接受的。

因此,我建议的一般解决方案是将注意力集中在画布类的运行时灵活性上,以便例如可以对每种画布类型使用继承,并着重于像素操作的编译时绑定。

该问题表明颜色类要解决的最重要功能是它们之间的颜色格式转换,因此,我现在将重点介绍它。您可以使用三种方法在类型之间实现转换功能:

  • 星号:每种颜色格式均可与最精确的格式转换。要将A转换为B,首先将A转换为最精确的格式,然后再从该格式转换为B。这很简单且可扩展,因为添加新格式只需要定义两个以上的函数,即函数的数量会增加与格式数(O(N)线性相关
  • 完全连接:每种颜色都可以与其他每种颜色格式相互转换。这要快得多,因为只需要一次转换就可以以最小的努力和最大的优化潜力。但是,函数的数量为O(N 2
  • 混合:如果定义了直接转换,则使用它,否则,使用最精确的格式作为中介。

为了使编译器选择正确的转换函数,模板是我能想到的最优雅的解决方案。我将尝试使其保持简单,但是当然,这可能会导致一些限制。

补充:局限性之一是局部功能模板的专业化,当某些转换功能代码应用于多于一对格式时,这实际上很有用。解决此问题的建议方法是使用特征系统描述一种颜色格式。convert以下代码中函数将以traits格式作为模板方法编写。此答案未涵盖此范围。

让我们有一些代码(编辑):

// Complete and repeat the class definition below for every color format.
// There is no specific interface to follow, but all classes must have a
// template constructor and a template assignment operator to convert from
// other color formats.
class ColorXYZ {
public:
    ...
    template <class Other>
    ColorXYZ(const Other& other) {
        convert(*this, other);
    }
    template <class Other>
    ColorXYZ& operator=(const Other& other) {
        convert(*this, other);
        return *this;
    }
    ...
};

// These should be class definitions, not just forward declarations:
class ColorMono;
class ColorRGB16;
class ColorRGB24;

// Every format must be able to convert to and from the MostPreciseFormat
typedef ColorRGB24 MostPreciseFormat;

// Generic conversion of color formats that converts to MostPreciseFormat and
// then to the required format.
template <class To, class From>
void convert(To& to, const From& from) {
    MostPreciseFormat precise(from);
    convert(to, precise);
}

// Specialization to convert from Mono to RGB24.
template <>
void convert<ColorRGB24, ColorMono>(ColorRGB24& to, const ColorMono& from) {
    // specific code to convert from mono to RGB24.
    to.setR(from.value() ? 255 : 0);
    to.setG(from.value() ? 255 : 0);
    to.setB(from.value() ? 255 : 0);
}

... // A lot of other specializations of convert here.

编译将在存在时选择最专门的转换函数,否则它将回退到使用的转换MostPreciseFormat

补充:这一点很重要的所有专业convert都定义为MostPreciseFormat两个ToFrom,其中包括情况下该格式是一样的ToFrom更具体地说,对于上面的代码,至少需要以下专业化:

convert<ColorRGB24, ColorMono>
convert<ColorRGB24, ColorRGB16>
convert<ColorRGB24, ColorRGB24>
convert<ColorRGB24, ColorXYZ>
convert<ColorMono,  ColorRGB24>
convert<ColorRGB16, ColorRGB24>
convert<ColorRGB24, ColorRGB24>
convert<ColorXYZ,   ColorRGB24>

补充:其他专长,例如从Mono到RGB16的转换可以使用通用方法,将作为从Mono到RGB24以及从RGB24到RGB16的转换实例化。这是低效率的(星型方法),但是有效。对于常见的情况,最好也具有专长(朝完全连接的方法发展)。

您应该注意,所有颜色之间没有通用的基类,因此不应该存在。我希望用户拥有一个Canvas类,该类通过虚拟方法抽象化处理每种像素格式的细节。例如:

class Canvas {
public:
    ...
    virtual void setPixel(unsigned int index, ColorRGB24 color) = 0;
    ...
};

class CanvasMono {
public:
    ...
    virtual void setPixel(unsigned int index, ColorRGB24 color) {
        pixel[index] = color; // converting from RGB24 to mono
    }
    ...
private:
    ColorMono* pixel;
};

根据最常见的用例,可能值得每种格式(至少是最常见的格式)都有重载。但是,如果用户不应该频繁使用格式化的颜色值,则只能有一个重载,并且它总是使用该convert功能。

我希望我涵盖了您要创建的色彩系统的最相关方面。

本文收集自互联网,转载请注明来源。

如有侵权,请联系[email protected] 删除。

编辑于
0

我来说两句

0条评论
登录后参与评论

相关文章

来自分类Dev

如何在Ruby-C ++扩展的C ++类中编写非静态方法?

来自分类Dev

如何在Ruby-C ++扩展的C ++类中编写非静态方法?

来自分类Dev

如何在C#中编写不可更改的类?

来自分类Dev

如何在C ++ Builder中从TADOQuery扩展类?

来自分类Dev

如何在C ++中编写哈希函数?

来自分类Dev

如何在[C]中编写适当的循环

来自分类Dev

如何在 C 中编写 for 循环

来自分类Dev

如何在R中构造易于扩展的蒙特卡洛模型

来自分类Dev

如何在C ++中更改颜色?

来自分类Dev

如何在C ++的#define中扩展“#”?

来自分类Dev

如何在Cython中遍历C ++集?

来自分类Dev

如何在C ++中访问对集

来自分类Dev

如何在C#中编写通用扩展方法以转换类型

来自分类Dev

如何在Dart中扩展Rectangle类?

来自分类Dev

如何在TypeScript中扩展类?

来自分类Dev

如何在CodeIgniter中扩展多个类?

来自分类Dev

如何在WPF中扩展Ellipse类

来自分类Dev

如何在C ++中制作可比类

来自分类Dev

如何在C ++中实现静态类?

来自分类Dev

如何在C#中调用类

来自分类Dev

如何在C ++中制作可比类

来自分类Dev

如何在C ++类中输入变量?

来自分类Dev

如何在C ++类中调试变量?

来自分类Dev

如何在Swift 2.2+中编写非C类的for循环?

来自分类Dev

如何在A.py中扩展C.py使用的B.py中定义的类

来自分类Dev

如何在A.py中扩展C.py使用的B.py中定义的类

来自分类Dev

扩展Reader类:如何编写close()?

来自分类Dev

如何在C ++中“ sscanf”?

来自分类Dev

如何在C中截断