Skip to content

Latest commit

 

History

History
725 lines (480 loc) · 30.9 KB

File metadata and controls

725 lines (480 loc) · 30.9 KB

十二、Boost 的冰山一角

在本章中,我们将介绍:

  • 使用图形
  • 可视化图形
  • 使用真随机数发生器
  • 使用便携式数学函数
  • 编写测试用例
  • 在一个测试模块中组合多个测试用例
  • 操纵图像

介绍

Boost 是一个庞大的图书馆集合。其中一些图书馆很小,适合日常使用,而另一些图书馆需要一本单独的书来描述它们的所有功能。这一章专门介绍了一些大型图书馆,并提供了对它的基本理解。

前两个食谱将解释Boost.Graph的用法。这是一个拥有数量惊人的算法的大图书馆。我们将看到一些基础知识,可能也是开发中最重要的部分——图形可视化。

我们还将看到一个非常有用的生成真随机数的方法。这是编写安全密码系统的一个非常重要的方法。

一些 C++ 标准库缺少数学函数。我们将看看如何使用 Boost 修复这个问题。但是,这本书的格式没有留下描述所有功能的空间。

编写测试用例在编写测试用例中描述,在一个测试模块中组合多个测试用例。这对任何生产质量系统都很重要。

最后一个食谱是关于一个图书馆,它在我大学期间帮助了我很多课程。使用它可以创建和修改图像。我个人使用它来可视化不同的算法,在图像中隐藏数据,在图像上签名,以及生成纹理。

不幸的是,即使是这一章也不能告诉你所有的 Boost 库。也许有一天,我会再写一本书,然后,再写几本。

使用图形

有些任务需要将数据表示为图形。Boost.Graph是一个库,旨在提供一种在内存中构建和表示图形的灵活方式。它还包含许多处理图的算法,例如拓扑排序、广度优先搜索、深度优先搜索和 Dijkstra 最短路径。

好了,我们用Boost.Graph来执行一些基本任务吧!

准备好

这个食谱只需要 C++ 和模板的基础知识。

怎么做...

在这个食谱中,我们将描述一个图形类型,创建一个该类型的图形,给图形添加一些顶点和边,并搜索一个特定的顶点。这应该足以从Boost.Graph开始。

  1. 我们从描述图形类型开始:
#include <boost/graph/adjacency_list.hpp> 
#include <string> 

typedef std::string vertex_t; 
typedef boost::adjacency_list< 
    boost::vecS 
    , boost::vecS 
    , boost::bidirectionalS 
    , vertex_t 
> graph_type; 
  1. 现在,我们构建它:
int main() {
    graph_type graph; 
  1. 让我们来实施一些加快图形构建的未记录的技巧:
    static const std::size_t vertex_count = 5; 
    graph.m_vertices.reserve(vertex_count); 
  1. 现在,我们准备向图中添加顶点:
    typedef boost::graph_traits<
        graph_type
    >::vertex_descriptor descriptor_t;

    descriptor_t cpp
        = boost::add_vertex(vertex_t("C++"), graph);
    descriptor_t stl
        = boost::add_vertex(vertex_t("STL"), graph);
    descriptor_t boost
        = boost::add_vertex(vertex_t("Boost"), graph);
    descriptor_t guru
        = boost::add_vertex(vertex_t("C++ guru"), graph);
    descriptor_t ansic
        = boost::add_vertex(vertex_t("C"), graph);
  1. 现在是连接顶点和边的时候了:
    boost::add_edge(cpp, stl, graph); 
    boost::add_edge(stl, boost, graph); 
    boost::add_edge(boost, guru, graph); 
    boost::add_edge(ansic, guru, graph); 
} // end of main()
  1. 我们可以做一个搜索顶点的函数:
inline void find_and_print(
    const graph_type& graph, boost::string_ref name)
{
  1. 接下来是一个获取所有顶点迭代器的代码:
    typedef typename boost::graph_traits<
        graph_type
    >::vertex_iterator vert_it_t;

    vert_it_t it, end;
    boost::tie(it, end) = boost::vertices(graph);
  1. 是时候搜索所需的顶点了:
    typedef typename boost::graph_traits<
        graph_type
    >::vertex_descriptor desc_t;

    for (; it != end; ++ it) {
        const desc_t desc = *it;
        const vertex_t& vertex = boost::get(
            boost::vertex_bundle, graph
        )[desc];

        if (vertex == name.data()) {
            break;
        }
    }

    assert(it != end);
    std::cout << name << '\n';
} /* find_and_print */

它是如何工作的...

第 1 步中,我们描述了我们的图形必须是什么样子,以及它必须基于什么类型。boost::adjacency_list是一个将图形表示为二维结构的类,其中第一维包含顶点,第二维包含该顶点的边。boost::adjacency_list必须是表示图形的默认选择,因为它适合大多数情况。

第一个模板参数boost::adjacency_list描述了用于表示每个顶点的边列表的结构。第二个描述了存储顶点的结构。我们可以使用特定的选择器为这些结构选择不同的标准库容器,如下表所示:

| 选择器 | 标准库容器 | | boost::vecS | std::vector | | boost::listS | std::list | | boost::slistS | std::slist | | boost::setS | std::set | | boost::multisetS | std::multiset | | boost::hash_setS | std::hash_set |

第三个模板参数用于制作间接、有向或双向图。分别使用boost::undirectedSboost::directedSboost::bidirectionalS选择器。

第五个模板参数描述了用作顶点的数据类型。在我们的例子中,我们选择std::string。我们还可以支持边的数据类型,并将其作为模板参数提供。

第 2 步第 3 步很简单,但是在第 4 步中,你可能会看到一些没有记录的方法来加快图形的构建。在我们的例子中,我们使用std::vector作为存储顶点的容器,所以我们可以强制它为所需数量的顶点保留内存。这导致在向图中插入顶点的过程中,内存分配/解除分配和复制操作减少。这个步骤不是很便携,在未来版本的 Boost 中可能会中断,因为这个步骤高度依赖于boost::adjacency_list的当前实现和所选择的存储顶点的容器类型。

步骤 4 中,我们看到了如何将顶点添加到图形中。注意boost::graph_traits<graph_type>是如何被使用的。boost::graph_traits类用于获取特定于图形类型的类型。我们将在本章后面看到它的用法和一些特定于图形的类型的描述。第五步展示了我们需要什么来连接顶点和边。

If we had provided some datatype for the edges, adding an edge would look as follows: boost::add_edge(ansic, guru, edge_t(initialization_parameters), graph)

在*步骤 6 中,*图形类型是一个template参数。建议这样做,以实现更好的代码重用性,并使该函数与其他图形类型一起工作。

步骤 7 中,我们看到如何迭代图的所有顶点。顶点迭代器的类型是从boost::graph_traits接收的。函数boost::tieBoost.Tuple的一部分,用于从元组中获取变量的值。因此,调用boost::tie(it, end) = boost::vertices(g)会将begin迭代器放入it变量,将end迭代器放入end变量。

这可能会让你感到惊讶,但是取消顶点迭代器的引用并不会返回顶点数据。相反,它返回顶点描述符desc,可以在boost::get(boost::vertex_bundle, g)[desc]中使用它来获取顶点数据,就像我们在步骤 8 中所做的那样。顶点描述符类型用于许多Boost.Graph函数。我们已经在步骤 5 中看到了它在边缘构建功能中的使用。

As already mentioned, the Boost.Graph library contains implementations of many algorithms. You may find many search policies implemented, but we won't discuss them in this book. We limit this recipe just to the basics of the graph library.

还有更多...

Boost.Graph库不是 C++ 17 的一部分,也不会是下一个 C++ 标准的一部分。当前的实现不支持像右值引用这样的 C++ 11 特性。如果我们使用难以复制的顶点,我们可以通过以下技巧获得速度:

 vertex_descriptor desc = boost::add_vertex(graph);
 boost::get(boost::vertex_bundle, g_)[desc] = std::move(vertex_data);

它避免了在boost::add_vertex(vertex_data, graph)内的复制构造,而是使用带有移动赋值的默认构造。

Boost.Graph的效率取决于多种因素,例如底层容器类型、图形表示、边和顶点数据类型。

请参见

阅读可视化图表食谱可以帮助您轻松处理图表。您也可以通过以下链接阅读其官方文档:http://boost.org/libs/graph

可视化图形

由于可视化的问题,制作操纵图形的程序从来都不容易。当我们使用标准的库容器如std::mapstd::vector时,我们总是可以打印容器的内容,看看里面发生了什么。但是当我们处理复杂的图形时,很难清晰地可视化内容;文本表示对人类不友好,因为它通常包含太多的顶点和边。

在这个食谱中,我们将使用 Graphviz 工具来看一下Boost.Graph的可视化。

准备好

要可视化图形,您需要一个 Graphviz 可视化工具。还需要了解前面的配方。

怎么做...

可视化分两个阶段完成。在第一阶段,我们让我们的程序以适合 Graphviz 的文本格式输出图形的描述。在第二阶段,我们将第一步的输出导入可视化工具。这个食谱中编号的步骤都是关于第一阶段的。

  1. 让我们按照前面的方法为graph_type编写std::ostream运算符:
#include <boost/graph/graphviz.hpp>

std::ostream& operator<<(std::ostream& out, const graph_type& g) {
    detail::vertex_writer<graph_type> vw(g);
    boost::write_graphviz(out, g, vw);

    return out;
}
  1. 前一步中使用的detail::vertex_writer结构必须定义如下:
#include <iosfwd>

namespace detail {
    template <class GraphT>
    class vertex_writer {
        const GraphT& g_;

    public:
        explicit vertex_writer(const GraphT& g)
            : g_(g)
        {}

        template <class VertexDescriptorT>
        void operator()(
            std::ostream& out,
            const VertexDescriptorT& d) const
        {
            out << " [label=\""
                << boost::get(boost::vertex_bundle, g_)[d] 
                << "\"]"; 
        }
    }; // vertex_writer
} // namespace detail

仅此而已。现在,如果我们使用std::cout << graph;命令可视化上一个配方的图形,输出可以使用dot命令行工具创建图形图片:

    $ dot -Tpng -o dot.png

    digraph G {
    0 [label="C++"];
    1 [label="STL"];
    2 [label="Boost"];
    3 [label="C++ guru"];
    4 [label="C"];
    0->1 ;
    1->2 ;
    2->3 ;
    4->3 ;
    }

下图描述了前面命令的输出:

如果命令行让你害怕,我们也可以使用 GveditXDot 程序进行可视化。

它是如何工作的...

Boost.Graph库包含以 Graphviz (DOT)格式输出图形的功能。如果我们在步骤 1 中用两个参数编写boost::write_graphviz(out, g),该函数将输出一个顶点编号为0的图形图片。那不是很有用,所以我们提供了一个输出顶点名称的手写vertex_writer类的实例。

正如我们在步骤 2 中看到的,Graphviz 工具理解 DOT 格式。如果你想为你的图表输出更多的信息,那么你可能需要阅读 Graphviz 文档来获得更多关于 DOT 格式的信息。

如果您希望在可视化过程中向边缘添加一些数据,我们需要向boost::write_graphviz提供一个边缘可视化器的实例作为第四个参数。

还有更多...

C++ 17 不包含Boost.Graph或者图形可视化的工具。但是你不需要担心,因为有很多其他的图形格式和可视化工具Boost.Graph可以使用它们。

请参见

  • 使用图形的配方包含关于Boost.Graphs构造的信息 ** 在http://www.graphviz.org/你会发现很多关于 DOT 格式和 Graphviz 的信息*

** Boost 的官方文档Boost.Graph库包含多个例子和有用的信息,可以在http://boost.org/libs/graph找到

使用真随机数发生器

我知道许多商业产品使用不正确的方法获取随机数的例子。遗憾的是,一些公司仍然在密码学和银行软件中使用rand()

让我们看看如何使用适用于银行软件的Boost.Random获得完全随机的均匀分布

入门指南

这个食谱需要 C++ 的基础知识。了解不同类型的发行版也会有所帮助。该配方中的代码需要链接到boost_random库。

怎么做...

要创建一个真正的随机数,我们需要操作系统或处理器的帮助。这是如何使用 Boost 完成的:

  1. 我们需要包含以下标题:
#include <boost/config.hpp> 
#include <boost/random/random_device.hpp> 
#include <boost/random/uniform_int_distribution.hpp>
  1. 高级随机位提供商在不同平台下有不同的名称:
int main() {
    static const std::string provider = 
#ifdef BOOST_WINDOWS 
        "Microsoft Strong Cryptographic Provider" 
#else 
        "/dev/urandom" 
#endif 
    ; 
  1. 现在,我们准备用Boost.Random初始化发电机:
    boost::random_device device(provider); 
  1. 让我们得到一个均匀分布,它返回一个介于100065535之间的值:
    boost::random::uniform_int_distribution<unsigned short> random(1000);

就这样。现在,我们可以使用random(device)调用获得真正的随机数。

它是如何工作的...

为什么rand()功能不适合银行?因为它会生成伪随机数,这意味着黑客可能会预测下一个生成的数字。这是所有伪随机数算法的问题。有些算法更容易预测,有些更难,但这仍然是可能的。

这就是为什么,我们在这个例子中使用boost::random_device(见第三步)。该设备从操作系统中收集关于随机事件的信息,以产生不可预测的统一随机位。这种事件的例子是按键之间的延迟、某些硬件中断之间的延迟以及内部中央处理器的随机位发生器。

操作系统可能有不止一种这样的随机位生成器。在我们的 POSIX 系统示例中,我们使用了/dev/urandom而不是更安全的/dev/random,因为后者会一直处于阻塞状态,直到操作系统捕获到足够多的随机事件。等待熵可能需要几秒钟,这通常不适合应用。长寿命GPG/SSL/SSH钥匙使用/dev/random

现在我们已经完成了发电机,是时候进入第 4 步并讨论分配类了。如果生成器只是生成均匀分布的位,则分布类会根据这些位生成一个随机数。在第 4 步中,我们做了一个均匀分布,返回一个unsigned short类型的随机数。参数1000意味着分布必须返回大于或等于1000的数字。我们还可以提供最大值作为第二个参数,默认情况下,它等于返回类型中可存储的最大值。

还有更多...

Boost.Random有大量的真/伪随机位发生器和分布,以满足不同的需求。避免复制发行版和生成器。这可能是一个昂贵的手术。

C++ 11 支持不同的分发类和生成器。您可以在std::命名空间的<random>头中找到这个例子中的所有类。Boost.Random库不使用 C++ 11 特性,也不是该库真正需要的。应该使用 Boost 实现还是标准库?Boost 提供了跨系统的更好的可移植性。然而,一些标准库可能有程序集优化的实现,并且可能提供一些有用的扩展。

请参见

官方文档包含了一个完整的生成器和发行版的列表以及描述。可在以下链接获得:http://boost.org/libs/random.

使用便携式数学函数

有些项目需要特定的三角函数,这是一个用于数值求解常微分方程以及处理分布和常数的库。Boost.Math的所有这些部分即使放在单独的书中也很难适应。单一的食谱肯定是不够的。所以,让我们把重点放在非常基本的日常使用的函数上来处理浮点类型。

我们将编写一个便携式函数,检查输入值是无穷大还是不是-a-Number(NaN)值,如果值为负,则更改符号。

准备好

这个食谱需要 C++ 的基础知识。了解 C99 标准的人会发现这个食谱有很多共同点。

怎么做...

执行以下步骤检查输入值是否为无穷大和 NaN 值,如果值为负,则更改符号:

  1. 我们需要以下标题:
#include <boost/math/special_functions.hpp> 
#include <cassert> 
  1. 断言无穷大和 NaN 可以这样做:
template <class T> 
void check_float_inputs(T value) { 
    assert(!boost::math::isinf(value)); 
    assert(!boost::math::isnan(value)); 
  1. 使用以下代码更改标志:
    if (boost::math::signbit(value)) { 
        value = boost::math::changesign(value); 
    } 

    // ... 
} // check_float_inputs 

就这样!现在,我们可以检查check_float_inputs(std::sqrt(-1.0))check_float_inputs(std::numeric_limits<double>::max() * 2.0)是否会触发断言。

它是如何工作的...

实类型具有无法使用相等运算符检查的特定值。例如,如果变量v包含 NaN,assert(v != v)可能通过,也可能不通过,这取决于编译器。

对于这种情况,Boost.Math提供了可以可靠地检查无穷大和 NaN 值的函数。

第三步包含boost::math::signbit功能,需要澄清。该函数返回一个有符号位,当数字为负时为1,当数字为正时为0。换句话说,如果值为负,则返回true

第三步,有些读者可能会问,为什么不能直接乘以-1而不叫boost::math::changesign?我们可以。但是,乘法运算可能比boost::math::changesign慢,并且不能保证在特殊值下有效。例如,如果您的代码可以使用nan,则第 3 步中的代码可以更改-nan的符号并将nan写入变量。

The Boost.Math library maintainers recommend wrapping math functions from this example in round parentheses to avoid collisions with C macro. It is better to write (boost::math::isinf)(value) instead of boost::math::isinf(value).

还有更多...

C99 包含本配方中描述的所有功能。为什么我们在 Boost 中需要它们?嗯,有些编译器厂商认为程序员不需要 C99 的全面支持,所以你不会在至少一个非常流行的编译器中找到那些函数。另一个原因是Boost.Math函数可能用于行为类似数字的类。

Boost.Math是一个非常快速、便携、可靠的库。数学特殊函数Boost.Math库的一部分,一些数学特殊函数被 C++ 17 接受。然而,Boost.Math提供了更多,并且有高可用性的循环版本,具有更好的复杂性和更好的适合一些任务(像数值积分)。

请参见

Boost 的官方文档包含了很多有趣的例子和教程,可以帮助你习惯Boost.Math。浏览http://boost.org/libs/math阅读相关内容。

编写测试用例

这个食谱和下一个食谱致力于使用Boost.Test库进行自动测试,许多 Boost 库都使用这个库。让我们动手为自己的班级写一些测试:

#include <stdexcept> 
struct foo { 
    int val_; 

    operator int() const; 
    bool is_not_null() const; 
    void throws() const; // throws(std::logic_error) 
}; 

准备好

这个食谱需要 C++ 的基础知识。要编译这个配方的代码,定义BOOST_TEST_DYN_LINK宏并链接到boost_unit_test_frameworkboost_system库。

怎么做...

老实说,Boost 中不止有一个测试库。我们来看看最实用的。

  1. 要使用它,我们需要定义宏并包含以下标题:
#define BOOST_TEST_MODULE test_module_name 
#include <boost/test/unit_test.hpp> 
  1. 每组测试都必须写在测试用例中:
BOOST_AUTO_TEST_CASE(test_no_1) { 
  1. 检查true结果的某些功能必须按如下方式进行:
    foo f1 = {1}, f2 = {2}; 
    BOOST_CHECK(f1.is_not_null());
  1. 检查非质量必须以下列方式进行:
    BOOST_CHECK_NE(f1, f2); 
  1. 检查抛出的异常必须如下所示:
    BOOST_CHECK_THROW(f1.throws(), std::logic_error); 
} // BOOST_AUTO_TEST_CASE(test_no_1) 

就这样!在编译和链接之后,我们将有一个自动测试foo并以人类可读的格式输出测试结果的二进制文件。

它是如何工作的...

编写单元测试很容易。你知道函数是如何工作的,在特定情况下会产生什么结果。因此,您只需检查预期结果是否与函数的实际输出相同。这就是我们在步骤 3 中所做的。我们知道f1.is_not_null()返回true,我们已经检查过了。在第 4 步我们确实知道f1不等于f2,所以我们也检查了一下。对f1.throws()的调用产生std::logic_error异常,我们检查预期类型的异常是否被抛出。

第 2 步,我们正在做一个测试用例——一组验证foo结构正确行为的检查。我们可能在一个源文件中有多个测试用例。例如,如果我们添加以下代码:

BOOST_AUTO_TEST_CASE(test_no_2) { 
    foo f1 = {1}, f2 = {2}; 
    BOOST_REQUIRE_NE(f1, f2); 
    // ... 
} // BOOST_AUTO_TEST_CASE(test_no_2) 

这段代码将与test_no_1测试用例一起运行。

传递给BOOST_AUTO_TEST_CASE宏的参数只是测试用例的唯一名称,在出现错误时会显示出来。

Running 2 test cases... 
main.cpp(15): error in "test_no_1": check f1.is_not_null() failed 
main.cpp(17): error in "test_no_1": check f1 != f2 failed [0 == 0] 
main.cpp(19): error in "test_no_1": exception std::logic_error is expected 
main.cpp(24): fatal error in "test_no_2": critical check f1 != f2 failed [0 == 0] 

*** 4 failures detected in test suite "test_module_name" 

BOOST_REQUIRE_*BOOST_CHECK_*宏有一点区别。如果BOOST_REQUIRE_*宏检查失败,当前测试用例的执行停止,Boost.Test运行下一个测试用例。然而,失败BOOST_CHECK_*并不会停止当前测试用例的执行。

第 1 步需要额外的注意。注意BOOST_TEST_MODULE宏定义。该宏必须在包含Boost.Test头之前定义;否则,链接程序将失败。更多信息可在本食谱的部分找到。

还有更多...

有些读者可能会疑惑,为什么我们在第四步BOOST_CHECK_NE(f1, f2)而不是BOOST_CHECK(f1 != f2)?答案很简单:第 4 步中的宏提供了一个更易读和详细的Boost.Test库旧版本的输出。

C++ 17 缺乏对单元测试的支持。但是Boost.Test库可以用来测试 C++ 17 和 C++ 11 之前的代码。

记住,测试越多,得到的代码就越可靠!

请参见

  • 在一个测试模块中组合多个测试用例的配方包含更多关于测试和BOOST_TEST_MODULE宏的信息。 ** 有关测试宏的完整列表和Boost.Test高级功能的信息,请参考位于http://boost.org/libs/test的 Boost 官方文档*

*# 在一个测试模块中组合多个测试用例

编写自动测试对你的项目有好处。然而,当项目很大并且许多开发人员都在做的时候,管理测试用例是很困难的。在这个食谱中,我们将看看如何运行单个测试,以及如何在单个模块中组合多个测试用例。

让我们假设两个开发人员正在测试在foo.hpp头中声明的foo结构,我们希望给他们单独的源文件来编写测试。在这种情况下,两个开发人员不会互相打扰,可能会并行工作。但是,默认的测试运行必须执行两个开发人员的测试。

准备好

这个食谱需要 C++ 的基础知识。该配方部分重用了之前配方中的代码,并且还需要定义BOOST_TEST_DYN_LINK宏并与boost_unit_test_frameworkboost_system库链接。

怎么做...

这个配方使用了前一个配方的代码。这是测试大项目非常有用的方法。不要低估它。

  1. 在上一个配方的main.cpp中的所有标题中,只留下这两行:
#define BOOST_TEST_MODULE test_module_name 
#include <boost/test/unit_test.hpp> 
  1. 让我们把前面例子中的测试用例移到两个不同的源文件中:
// developer1.cpp 
#include <boost/test/unit_test.hpp> 
#include "foo.hpp" 
BOOST_AUTO_TEST_CASE(test_no_1) { 
    // ... 
} 
// developer2.cpp 
#include <boost/test/unit_test.hpp> 
#include "foo.hpp" 
BOOST_AUTO_TEST_CASE(test_no_2) { 
    // ... 
} 

就这样!因此,编译和链接所有的源代码和两个测试用例将在程序执行中起作用。

它是如何工作的...

所有的魔法都是由BOOST_TEST_MODULE宏完成的。如果是在<boost/test/unit_test.hpp>之前定义的,Boost.Test认为这个源文件是主要的,所有的辅助测试基础设施都必须放在里面。否则,从<boost/test/unit_test.hpp>只包含测试宏。

如果您将所有BOOST_AUTO_TEST_CASE测试与包含BOOST_TEST_MODULE宏的源文件相链接,它们将会运行。当处理一个大项目时,每个开发人员可能只允许编译和链接他们自己的源代码。这使其独立于其他开发人员,并提高了开发速度——无需在调试时编译外来源代码和运行外来测试。

还有更多...

Boost.Test库很好,因为它有选择性地运行测试的能力。我们可以选择运行什么测试,并将它们作为命令行参数传递。例如,以下命令仅运行test_no_1测试用例:

    ./testing_advanced -run=test_no_1

以下命令运行两个测试用例:

    ./testing_advanced -run=test_no_1,test_no_2

可惜 C++ 17 标准没有内置测试支持,看起来 C++ 20 也不会采用Boost.Test的类和方法。

请参见

  • 编写测试用例方法包含更多关于Boost.Test库的信息。有关Boost.Test的更多信息,请阅读http://boost.org/libs/test的 Boost 官方文档。
  • 勇敢的人可能会试着看看 Boost 库中的一些测试用例。那些测试用例被分配在位于boost文件夹中的libs子文件夹中。例如,Boost.LexicalCast测试用例在boost_1_XX_0/libs/lexical_cast/test分配。

操纵图像

我给你留下了一些真正美味的甜点——Boost 的通用图像库或只是Boost.GIL,它允许你在不太担心图像格式的情况下操纵图像。

让我们用它做一些简单有趣的事情。例如,让我们制作一个否定任何图片的程序。

准备好

这个食谱需要 C++、模板和Boost.Variant的基础知识。该示例需要链接到png库。

怎么做...

为了简单起见,我们将只处理 PNG 图像。

  1. 让我们从包含头文件开始:
#include <boost/gil/gil_all.hpp> 
#include <boost/gil/extension/io/png_dynamic_io.hpp> 
#include <string> 
  1. 现在,我们需要定义希望使用的图像类型:
int main(nt argc, char *argv[]) {
    typedef boost::mpl::vector<
            boost::gil::gray8_image_t,
            boost::gil::gray16_image_t,
            boost::gil::rgb8_image_t
    > img_types;
  1. 打开现有的 PNG 图像可以这样实现:
    std::string file_name(argv[1]); 
    boost::gil::any_image<img_types> source; 
    boost::gil::png_read_image(file_name, source);
  1. 我们需要对图片进行如下操作:
    boost::gil::apply_operation( 
        view(source), 
        negate() 
    ); 
  1. 以下代码行将帮助您编写图像:
    boost::gil::png_write_view("negate_" + file_name, const_view(source)); 
  1. 让我们看一下修改操作:
struct negate { 
    typedef void result_type; // required 

    template <class View> 
    void operator()(const View& source) const { 
        // ... 
    } 
}; // negate 
  1. operator()的主体包括获取通道类型:
typedef typename View::value_type value_type; 
typedef typename boost::gil::channel_type<value_type>::type channel_t; 
  1. 它还遍历像素:
const std::size_t channels = boost::gil::num_channels<View>::value; 
const channel_t max_val = (std::numeric_limits<channel_t>::max)(); 

for (unsigned int y = 0; y < source.height(); ++ y) { 
    for (unsigned int x = 0; x < source.width(); ++ x) { 
        for (unsigned int c = 0; c < channels; ++ c) { 
            source(x, y)[c] = max_val - source(x, y)[c]; 
        } 
    } 
} 

现在让我们看看我们计划的结果:

上图是下图的底片:

它是如何工作的...

步骤 2 中,我们描述了我们希望处理的图像类型。这些图像是每像素 8 位和 16 位的灰度图像,以及每像素 8 位的 RGB 图像。

boost::gil::any_image<img_types>类是一种Boost.Variant,它可能持有一个img_types变量的图像。您可能已经猜到了,boost::gil::png_read_image将图像读入图像变量。

第 4 步中的boost::gil::apply_operation功能几乎等同于Boost.Variant库中的boost::apply_visitor。注意view(source)的用法。boost::gil::view函数在图像周围构建一个光包装,将图像解释为二维像素阵列。

你还记得在Boost.Variant的时候,我们从boost::static_visitor吸引游客吗?当我们使用 GIL 版本的变体时,我们需要在visitor内部制作一个result_type typedef。你可以在第六步看到。

一点理论:图像由称为像素的点组成。一幅图像有相同类型的像素。然而,对于单个通道,不同图像的像素在通道数和颜色位上可能不同。通道代表原色。在 RGB 图像的情况下,我们有一个由三个通道组成的像素——红色、绿色和蓝色。在灰色图像的情况下,我们有一个代表灰色的单一通道。

回到我们的形象。在步骤 2 中,我们描述了我们希望处理的图像类型。在步骤 3 中,从文件中读取这些图像类型之一,并将其存储在源变量中。在步骤 4 中,negate访问者的operator()方法被实例化为所有图像类型。

步骤 7 中,我们可以看到如何从图像视图中获取通道类型。

第 8 步,我们迭代像素和通道并否定它们。通过max_val - source(x, y)[c]进行否定,结果写回图像视图。

我们在第 5 步写回图像。

还有更多...

C++ 17 没有处理图像的内置方法。将 2D 绘图添加到 C++ 标准库中的工作正在进行中,尽管这是一种正交功能。

Boost.GIL库快速高效。编译器很好地优化了它的代码,我们甚至可以使用一些Boost.GIL方法来帮助优化器展开循环。但是这一章只讨论了一些图书馆的基础知识,所以是时候停下来了。

请参见