模板还是不模板?(图片类)

To template or not template? (Image class)

本文关键字:      更新时间:2023-10-16

我有以下用例:

我需要创建一个Image类。图像的定义是:

  • 像素数(宽度*高度),
  • 像素类型(char, short, float, double)
  • 通道数(单通道,3通道(RGB), 4通道(RGBA)

上述类型的所有组合都是可能的。

,

    我有一些算法对这些图像进行操作。这些算法使用像素类型的模板。
  • 我需要与完全通用的文件格式(例如TIFF)接口。在这些文件格式中,像素数据保存为二进制流。

我的问题是:我应该使用模板化的Image类,还是通用接口?例子:

// 'Generic' Image interface
class Image {
  ...
  protected:
    // Totally generic data container
    uint8_t* data;
};

// Template Image interface
template <typename PixelType>
class Image {
  ...
  protected:
    // Template data container
    PixelType* data;
};

使用模板Image

我现在的问题是,如果我使用模板化的Image类,我的文件输入/输出将是混乱的,因为当我打开一个图像文件,我不知道先验图像类型将是什么,所以我不知道什么模板类型返回。

这可能是最优的解决方案,如果我能想出一种方法来创建一个泛型函数,从文件中读取图像并返回一个泛型对象,类似于

ImageType load(const char* filename);

但是由于ImageType必须是一个模板,我不知道如何以及是否可以这样做。

使用通用Image

然而,如果我使用一个通用的Image类,我所有的算法将需要一个包装函数与if/switch语句,如:

Image applyAlgorithmWrapper(const Image& source, Arguments args) { 
  if (source.channels() == 1) {
    if      (source.type() == IMAGE_TYPE_UCHAR) {
      return FilterFunction<unsigned char>(source, args);
    } 
    else if (source.type() == IMAGE_TYPE_FLOAT) {
      return FilterFunction<float>(source, args);
    } else if ...
  } else if (source.channels() == 3) {
    if      (source.type() == IMAGE_TYPE_UCHAR) {
      return FilterFunction<Vec3b>(source, args);
    }
    ...
  }

(注意:Vec3b是一个通用的3字节结构,如

)
struct Vec3b {
  char r, g, b; 
};

我认为模板化类是首选的解决方案。

它将为你提供模板的所有优点,这基本上意味着你的代码库将更干净,更容易理解和维护。

当使用模板化类时,你所说的问题并不是什么大问题。当用户想要读取图像时,他/她应该知道他/她想要接收图像文件输出的数据类型。因此,用户应该这样做:

Image<float>* img;
LoadFromTIFF(*img, <filename>);

这与ITK等库中的操作方式非常相似。在您的模块中(您可能会写入并从TIFF模块读取),您将执行这种类型转换,以确保返回用户声明的类型。

当手动创建图像时,用户应该这样做:

Image<float>*img;
img->SetSize(<width>, <height>);
img->SetChannels(<enum_channel_type>);

从长远来看,这比拥有一个非模板化的类要简单得多。

您可以查看ITK的源代码,以了解如何在最一般的意义上实现这一点,因为ITK是一个高度模板化的库。

编辑(附录)如果您不希望用户对图像数据类型进行先验控制,您应该考虑在TIFF头文件中使用SMinSampleValue和SMaxSampleValue标记。这些标头存在于任何现代TIFF文件(6.0版本)中。它们具有与TIFF文件中的示例数据类型匹配的TYPE。我相信这会解决你的问题

为了对模板和非模板做出正确的决定(基于事实而不是观点),我的策略是衡量并比较两种解决方案(模板和非模板)。我喜欢衡量以下指标:

  • 代码行数
  • 表演
  • 编译时间

以及其他更主观的衡量标准,如:

  • 易于维护
  • 大一新生需要多少时间才能理解代码

我开发了一个相当大的软件[1],基于这些度量,我的image类不是模板。我知道其他提供这两种选项的成像库[2](但我不知道他们有什么机制/代码是否仍然非常清晰)。我也有一些算法操作不同维度的点(2d, 3d,…Nd),对于这些人来说,将算法作为模板会带来性能提升,这是值得的。

简而言之,要做出正确的决定,要有明确的标准,明确的衡量方法,并在一个玩具示例中尝试这两种选择。

[1] http://alice.loria.fr/software/graphite/doc/html/

[2] http://opencv.org/

模板。还有一个变体。如果你还没有c++ 14,还有一个"接口助手"。让我解释一下。

当你对一个给定的操作有一组有限的专门化时,你可以将它们建模为满足接口或概念的类。如果这些可以表示为一个模板类,那么就这样做。当用户只需要给定的专门化时,它可以帮助他们,而当你从未类型化的源(例如文件)读取时,你所需要的只是一个工厂。注意,无论如何都需要一个工厂,只是返回类型通常定义良好。这就是我们要做的……

变体。当您不知道返回类型,但在编译时知道可能返回类型的集合时,请使用变体。为你的变体定义类型,使它看起来像一个基类(注意,没有继承或虚函数),然后使用访问者。在c++ 14中编写访问器的一种特别简单的方法是通过引用捕获所有内容的泛型lambda。实际上,从代码中的这一点开始,您就拥有了特定的类型。因此,将特定的/模板化类作为函数参数。

现在,boost::variant<>(或者std::variant<>,如果有的话)不能有成员函数。要么你驻留在'C-API风格'的泛型函数(可能只是委托给成员函数)对称操作符;或者您有一个从变体类型创建的助手类。如果你的CR允许,你可能会从variant继承——注意,有些人认为这种风格很糟糕,其他人接受它作为库作者的意图(因为,如果作者想要禁止继承,他们已经写了final)。

代码草图,不要尝试编译:

enum PixelFormatEnum { eUChar, eVec3d, eDouble };
template<PixelFormatEnum>
struct PixelFormat;
template<>
struct PixelFormat<eUChar>
{
    typedef unsigned char type;
};
// ...
template<PixelFormatEnum pf>
using PixelFormat_t = typename PixelFormat<pf>::type;
template<PixelFormatEnum pf>
struct Image
{
    std::vector<std::vector<PixelFormat_t<pf> > > pixels; // or anything like that
    // ...
};
typedef boost::variant< Image<eUChar>, Image<eVec3d>, Image<eDouble> > ImageVariant;
template<typename F>
struct WithImageV : boost::static_visitor<void>
{
    // you could do this better, e.g. with compose(f, bsv<void>), but...
    F f_;
    template<PixelFormatEnum e>
    void operator()(const Image<e>& img)
    {
        f_(img);
    }
}
template<typename F>
void WithImage(const ImageVariant& imgv, F&& f)
{
    WithImageV v{f};
    boost::apply_visitor(v, img);
}
std::experimental::optional<ImageVariant> ImageFactory(std::istream& is)
{
    switch (read_pixel_format(is))
    {
    case eUChar: return Image<eUchar>(is);
    // ...
    default: return std::experimental::nullopt;
    }
}
struct MyFavoritePixelOp : public boost::static_visitor<int>
{
    template<PixelFormatEnum e>
    int operator()(PixelFormat_t<e> pixel) { return pixel; }
    template<>
    int operator()(PixelFormat_t<eVec3d> pixel) { return pixel.r + pixel.g + pixel.b; }
};
int f_for_variant(const ImageVariant& imgv)
{
    // this is slooooow. Use it only if you have to, e.g., for loading.
    // Move the apply_visitor out of the loop whenever you can (here you could).
    int sum = 0;
    for (auto&& row : imgv.pixels)
       for (auto&& pixel : row)
           sum += boost::apply_visitor(MyFavoritePixelOp(), pixel);
    return sum;
}
template<PixelTypeEnum e>
int f_for_type(const Image<e>& img)
{
    // this is faster
    int sum = 0;
    for (auto&& row : img)
       for (auto&& pixel : row)
           sum += MyFavoritePixelOp()(pixel);
    return sum;        
}
int main() {
    // ...
    if (auto imgvOpt = ImageFactory(is))
    {
        // 1 - variant
        int res = f_for_variant(*imgvOpt);
        std::cout << res;
        // 2 - template
        WithImage(*imgvOpt, [&](auto&& img) {
           int res2 = f_for_type(img);
           std::cout << res2;
        });
    }
}
相关文章:
  • 没有找到相关文章