Search code examples
oopdesign-patternsarchitecturecode-organization

How should I organise a pile of singly used functions?


I am writing a C++ OpenCV-based computer vision program. The basic idea of the program could be described as follows:

  1. Read an image from a camera.

  2. Do some magic to the image.

  3. Display the transformed image.

The implementation of the core logic of the program (step 2) falls into a sequential calling of OpenCV functions for image processing. It is something about 50 function calls. Some temporary image objects are created to store intermediate results, but, apart from that, no additional entities are created. The functions from step 2 are used only once.

I am confused about organising this type of code (which feels more like a script). I used to create several classes for each logical step of the image processing. Say, here I could create 3 classes like ImagePreprocessor, ImageProcessor, and ImagePostprocessor, and split the abovementioned 50 OpenCV calls and temorary images correspondingly between them. But it doesn't feel like a resonable OOP design. The classes would be nothing more than a way to store the function calls.

The main() function would still just create a single object of each class and call thier methods consequently:

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

Which is, to my impression, essentially the same thing as callling 50 OpenCV functions one by one.

I start to question whether this type of code requiers an OOP design at all. After all, I can simply provide a function do_magic(), or three functions preprocess(), process(), and postprocess(). But, this approach doesn't feel like a good practice as well: it is still just a pile of function calls, separated into a different function.

I wonder, are there some common practices to organise this script-like kind of code? And what would be the way if this code is a part of a large OOP system?


Solution

  • Usually, in Image Processing, you have a pipeline of various Image Processing Modules. Same is applicable on Video Processing, where each Image is processed according to its timestamp order in the video.

    Constraints to consider before designing such pipeline:

    1. Order of Execution of these modules is not always same. Thus, the pipeline should be easily configurable.
    2. All modules of the pipeline should be executable in parallel with each other.
    3. Each module of the pipeline may also have a multithreaded operation. (Out of scope of this answer, but is a good idea when a single module becomes the bottleneck for the pipeline).
    4. Each module should easily adhere to the design and have the flexibility of internal implementation changes without affecting other modules.
    5. The benefit of preprocessing of a frame by one module should be available to later modules.

    Proposed Design.

    Video Pipeline

    A video pipeline is a collection of modules. For now, assume module is a class whose process method is called with some data. How each module can be executed will depend on how such modules are stored in VideoPipeline! To further explain, see below two categories:-

    Here, let’s say we have modules A, B, and C which always execute in same order. We will discuss the solution with a video of Frame 1, 2 and 3.

    a. Linked List: In a single-threaded application, frame 1 is first executed by A, then B and then C. The process is repeated for next frame and so on. So linked list seems like an excellent choice for the single threaded application.

    For a multi-threaded application, speed is what matters. So, of course, you would want all your modules running 128-core machine. This is where Pipeline class comes into play. If each Pipeline object runs in a separate thread, the whole application which may have 10 or 20 modules starts running multithreaded. Note that the single-thread/multithread approach can be made configurable

    b. Directed Acyclic Graph: Above-linked list implementation can be further improved when you have high processing power and want to reduce the lag between input and response time of pipeline. Such a case is when module C does not depend on B, but on A. In such case, any frame can be parallelly processed by module B and module C using a DAG based implementation. However, I wouldn’t recommend this as the benefits are not so great compared to the increased complexity, as further management of output from module B and C needs to be done by say module D where D depends on B or C or both. The number of scenarios increases.

    Thus, for simplicity sake, let’s use LinkedList based design.

    Pipeline

    1. Create a linked list of PipelineElement.
    2. Make process method of pipeline call process method of the first element.

    PipelineElement

    1. First, the PipelineElement processes the information by calling its ImageProcessor(read below). The PipelineElement will pass a Packet(of all data, read below) to ImageProcessor and receives the updated packet.
    2. If next element is not null, call next PipelineElement process and pass updated packet.
    3. If next element of a PipelineElement is null, stop. This element is special as it has an Observer object. Other PipelineElement will be set to null for Observer field.

    FrameReader(VIdeoReader/ImageReader)

    For video/image reader, create an abstract class. Whether you process video or image or multiple, processing is done one frame at a time, so create an abstract class(interface) ImageProcessor.

    1. A FrameReader object stores reference to the pipeline.
    2. For each frame, it pushes the information in by calling process method of Pipeline.

    ImageProcessor

    There is no Pre and Post ImageProcessor. For example, retinex processing is used as Post Processing but some application can use it as PreProcessing. Retinex processing class will implement ImageProcessor. Each element will hold Its ImageProcessor and Next PipeLineElement object.

    Observer
    A special class which extends PipelineElement and provides a meaningful output using GUI or disk.

    Multithreading
    1. Make each method run in its thread.
    2. Each thread will poll messages from a BlockingQueue( of small size like 2-3 Frames) to act as a buffer between two PipelineElements. Note: The queue helps in averaging the speed of each module. Thus, small jitters(a module taking too long time for a frame) does not affect video output rate and provides smooth playback.

    Packet
    A packet will store all the information such as input or Configuration class object. This way you can store intermediate calculations as well as observe a real-time effect of changing configuration of an algorithm using a Configuration Manager.

    To conclude, each element can now process in parallel. The first element will process nth frame, the second element will process n-1th frame, and soon, but with this, a lot more issues such as pipeline bottlenecks and additional delays due to less core power available to each element will pop up.