我正在使用许多不同的显示器(硬件)和不同的画布(是画布的复数吗?)。每个都可以使用不同的颜色。示例案例:
目前,我确实在处理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为常用的方法提供编译时继承,但是我未能提出一种可以编译并执行所需功能的实现。当我还有其他东西时,请在此处添加。
也欢迎使用完全不同的方法,因为我还没有编写使用我的颜色类必须提供的预定义接口的代码。
正如评论和第一个答案中所建议的那样,我编写了一个简单的测试来了解时机。这是我的标题:
#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本质上只是一个布尔值,当我拥有适当的代码时,编译器应该可以对此进行优化。
正如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。
首先,从这个问题来看,我认为解决方案必须具有非常快的执行时间,因此该要求应推动我们尝试解决问题的方法。
得出的结论是,我们应该避免在每个像素的基础上调用虚函数,否则CPU将不得不为每个像素进行一个额外的不必要的间接调用。但是,我们根本不应该避免使用虚函数,因为在每个画布操作中使用虚函数是完全可以接受的。
因此,我建议的一般解决方案是将注意力集中在画布类的运行时灵活性上,以便例如可以对每种画布类型使用继承,并着重于像素操作的编译时绑定。
该问题表明颜色类要解决的最重要功能是它们之间的颜色格式转换,因此,我现在将重点介绍它。您可以使用三种方法在类型之间实现转换功能:
为了使编译器选择正确的转换函数,模板是我能想到的最优雅的解决方案。我将尝试使其保持简单,但是当然,这可能会导致一些限制。
补充:局限性之一是局部功能模板的专业化,当某些转换功能代码应用于多于一对格式时,这实际上很有用。解决此问题的建议方法是使用特征系统描述一种颜色格式。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
两个To
和From
,其中包括情况下该格式是一样的To
和From
。更具体地说,对于上面的代码,至少需要以下专业化:
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] 删除。
我来说两句