【问题标题】:How should I organise a pile of singly used functions?我应该如何组织一堆单独使用的功能?
【发布时间】:2017-05-22 03:18:59
【问题描述】:

我正在编写一个基于 C++ OpenCV 的计算机视觉程序。该程序的基本思想可以描述如下:

  1. 从相机读取图像。

  2. 对图像做一些魔法。

  3. 显示转换后的图像。

程序核心逻辑的实现(第2步)落入顺序调用OpenCV函数进行图像处理。大约有 50 个函数调用。创建了一些临时图像对象来存储中间结果,但除此之外,不会创建其他实体。第 2 步中的函数只使用一次。

我对组织这种类型的代码感到困惑(感觉更像是脚本)。我曾经为图像处理的每个逻辑步骤创建几个类。比如说,在这里我可以创建ImagePreprocessorImageProcessorImagePostprocessor等3个类,并将上述50个OpenCV调用和临时图像在它们之间进行相应的拆分。但感觉不像是合理的 OOP 设计。这些类只不过是存储函数调用的一种方式。

main() 函数仍然只会为每个类创建一个对象,然后调用它们的方法:

image_preprocessor.do_magic(img);
image_processor.do_magic(img);
image_postprocessor.do_magic(img);

在我的印象中,这与一个一个调用 50 个 OpenCV 函数基本相同。

我开始怀疑这种类型的代码是否需要 OOP 设计。毕竟我可以简单地提供一个函数do_magic(),或者三个函数preprocess()process()postprocess()。但是,这种方法也不是一个好的做法:它仍然只是一堆函数调用,被分成不同的函数。

我想知道,是否有一些常见的做法来组织这种类似脚本的代码?如果这段代码是大型 OOP 系统的一部分,会是什么方式?

【问题讨论】:

  • 好的,我给出了部分答案,因为完整的解决方案需要大量的工作。对于视频/图像阅读器,创建一个抽象类。无论您是处理视频还是图像,处理都是一次完成一帧,因此创建一个抽象类(接口)ImageProcessor。没有 Pre 和 Post。例如,retinex 处理用作后期处理,但某些应用程序可以将其用作预处理。 Retinex 处理类将实现 ImageProcessor。
  • 接下来,如何制作管道。创建一个包含类 PipeLineElement 的 LinkedList 的类管道。每个元素都将保存其 ImageProcessor 和 Next PipeLineElement 对象。 pipelineElement 会将一个数据包(所有数据)传递给 ImageProcessor 并接收更新的数据包。更新的数据包将被传递到管道的下一个元素。请注意,此管道是串行管道,即您不能在同一最后一个数据包中处理两个元素,但您可以进一步推广链表。
  • 每个元素现在可以并行处理。第一个元素将处理第 n 个元素,第二个元素将处理第 n-1 个元素,并且很快,但随之而来的更多问题,例如管道瓶颈和由于每个元素中较少的多线程导致的额外延迟将导致严重的实时延迟。去过那里并从经验中讲述:D。总的来说,祝你好运,这个设计非常好,但需要非常高的专业知识。另一方面,每个库都可以像处理链表一样轻松地从管道中添加和删除。
  • 此外,您可以在内部处理一个 ImageProcessor 实现,而无需对整个管道进行任何更改。最后,不要使用 Mat 作为唯一需要传递的数据。使用像 PipeLinePacket 这样的类来存储应该传递的所有信息。这也将允许您将在早期 ImageProcessor 中完成的任何处理存储在管道中,并允许以后的 ImageProcessor 使用它。
  • 告售者!! :P,看起来我几乎给出了完整的解决方案。稍后将其添加为答案。

标签: oop design-patterns architecture code-organization


【解决方案1】:

通常,在图像处理中,您有一个由各种图像处理模块组成的管道。同样适用于视频处理,其中每个图像都根据其在视频中的时间戳顺序进行处理。

在设计此类管道之前要考虑的约束:

  1. 这些模块的执行顺序并不总是相同的。因此,管道应该易于配置。
  2. 管道的所有模块都应该可以相互并行执行。
  3. 管道的每个模块也可能有一个多线程操作。 (超出了这个答案的范围,但是当单个模块成为管道的瓶颈时是一个好主意)。
  4. 每个模块都应易于遵循设计并具有内部实现更改的灵活性,而不会影响其他模块。
  5. 一个模块对帧进行预处理的好处应该可以用于以后的模块。

提议的设计。

视频管道

视频管道是模块的集合。现在,假设模块是一个类,它的过程方法被调用了一些数据。每个模块如何执行将取决于这些模块如何存储在 VideoPipeline 中!为了进一步解释,请看以下两个类别:-

在这里,假设我们有模块 A、B 和 C,它们总是以相同的顺序执行。我们将通过第 1 帧、第 2 帧和第 3 帧的视频讨论解决方案。

一个。链表:在单线程应用程序中,第 1 帧首先由 A 执行,然后由 B 执行,然后由 C 执行。该过程在下一帧重复执行,依此类推。所以链表似乎是单线程应用程序的绝佳选择。

对于多线程应用程序,速度至关重要。因此,当然,您会希望所有模块都运行 128 核机器。这就是 Pipeline 类发挥作用的地方。如果每个 Pipeline 对象在单独的线程中运行,则可能有 10 或 20 个模块的整个应用程序开始运行多线程。请注意,单线程/多线程方法可以进行配置

b.有向无环图:当您具有高处理能力并希望减少管道输入和响应时间之间的延迟时,可以进一步改进上述链表实现。这种情况是模块 C 不依赖于 B,而是依赖于 A。在这种情况下,任何帧都可以由模块 B 和模块 C 使用基于 DAG 的实现并行处理。但是,我不建议这样做,因为与增加的复杂性相比,好处并没有那么大,因为模块 B 和 C 的输出的进一步管理需要通过模块 D 来完成,其中 D 依赖于 B 或 C 或两者。场景数量增加。

因此,为简单起见,让我们使用基于 LinkedList 的设计。

管道

  1. 创建 PipelineElement 的链表。
  2. 制作流水线的处理方法调用第一个元素的处理方法。

管道元素

  1. 首先,PipelineElement 通过调用它的 ImageProcessor(阅读下文)来处理信息。 PipelineElement 会将一个数据包(包含所有数据,见下文)传递给 ImageProcessor 并接收更新后的数据包。
  2. 如果下一个元素不为空,则调用下一个 PipelineElement 进程并传递更新的数据包。
  3. 如果 PipelineElement 的下一个元素为空,则停止。这个元素很特别,因为它有一个 Observer 对象。 Observer 字段的其他 PipelineElement 将设置为 null。

FrameReader(VIdeoReader/ImageReader)

对于视频/图像阅读器,创建一个抽象类。无论您处理视频或图像还是多个,处理都是一次完成一帧,因此创建一个抽象类(接口)ImageProcessor。

  1. FrameReader 对象存储对管道的引用。
  2. 对于每一帧,它通过调用Pipeline的process方法将信息推入。

图像处理器

没有前置和后置图像处理器。例如,retinex 处理用作后期处理,但某些应用程序可以将其用作预处理。 Retinex 处理类将实现 ImageProcessor。每个元素都将保存其 ImageProcessor 和 Next PipeLineElement 对象。

观察者
扩展 PipelineElement 并使用 GUI 或磁盘提供有意义的输出的特殊类。

多线程
1. 让每个方法在其线程中运行。
2. 每个线程都会从一个 BlockingQueue(2-3 帧这样的小尺寸)中轮询消息,作为两个 PipelineElement 之间的缓冲区。注意:队列有助于平均每个模块的速度。因此,小的抖动(一个模块花费太长时间一帧)不会影响视频输出速率并提供流畅的播放。

数据包
一个数据包将存储所有信息,例如输入或配置类对象。通过这种方式,您可以存储中间计算,并使用配置管理器观察更改算法配置的实时效果。

总而言之,现在每个元素都可以并行处理。第一个元素将处理第 n 帧,第二个元素将处理第 n-1 帧,很快就会出现更多问题,例如管道瓶颈和由于每个元素可用的核心功率减少而导致的额外延迟。

【讨论】:

  • 我在使用 Packet 类时遇到了一些麻烦。 ImageProcessor 的输入和输出可以是各种类(MatPointvector<Point>Matvector<Point>等)。我最终在 Packet 中只有map<string, ICustomType*>,其中ICustomType 是模板类的基类,它可以包含任何类型的值,string 只是所需对象的名称(例如“TARGET_IMAGE”),我调用 ImageProcessor。这对于我的项目范围来说已经足够简单了,但是您可能对 Packet 类设计的通用性还有其他建议吗?
  • 实际上,这不是那么通用的方法,因为需要在模块之间重用哪些信息将取决于这些模块。您可以尝试为每个模块添加另一个类来保存数据包,例如继承 Packet 的 RetinexPacket 对象,但是您将再次需要一个函数来在输入期间将值从 Packet 对象映射到 RetinexPacket 对象,并在给出输出时将 RetinexPacket 映射到 Packet 对象。这将减少 Packet 类所需的数据量并使其更短,但工作量太大。
  • 其他好处将是使每个模块独立并清除它们的接口。例如,您可以拥有 RetinexInputPacket 和 RetinexOutputPacket。可以轻松检查 RetinexInputPacket 以查看启动所需的信息。然而,我们从来不必超越这种方法,因为它保持我们的界面干净,并且数据传递简单而高效。此外,每个模块都是独立的。请注意,Packet、Pipeline 和 PipelineElement 类将放在单独的库中,任何处理模块库(例如 Retinex 处理)都将依赖于该库。
  • 没错,map 的解决方案不是通用的,偶尔会导致一些硬代码(那些存储对象的名称),但它感觉像是一个合理的权衡,而不是过度设计相对简单的系统.另一方面,即使使用建议的 Packet 和 ConcretePacket 方法,我也没有真正看到在不依赖于了解管道上下文的情况下实现接口的完全通用性的方法。提到的数据包映射函数仍然需要知道管道结构。
  • 考虑:你需要一张人脸边缘的图像。您已将 Mat 与来自网络摄像头的源 img 缓存在数据包中。然后你组装一个Pipeline,比如(1)FaceDetector->(2)EdgeDetector。在 (1) 之后,您还缓存了带有检测到的面部的Mat。但是,如果通用,步骤 (2) 只需要一个 Mat 作为输入,您需要判断是传递源 img 还是检测到的人脸 img。如果不引入一些上下文感知的观察者(例如,Algorithm),我看不到这样做的方法,因为 Packet itef 无法自行进行映射。
【解决方案2】:

这种结构适用于管道和过滤器架构(参见面向模式的软件架构第 1 卷:Frank Buschmann 的模式系统):

管道和过滤器架构模式提供了一个结构 处理数据流的系统。每个处理步骤是 封装在过滤器组件中。数据通过管道传递 相邻过滤器之间。重新组合过滤器允许您构建 相关系统的家族。

另请参阅《企业集成模式》一书中的简短 description(带图片)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-05
    相关资源
    最近更新 更多