I want to create a completely custom stream pipeline for my video stabilization project in c++.
The endresult should look like:
videofilelocation >> preprocessing() >> analyze() >> stabilize() >> video_out(outputfilename).flush();
Therefore, preprocessing
should accept an input string and load the video, extract frames, etc. Afterward, it should return a custom struct framevector
which should be passed to analyze
and so on.
Unfortunately, there is no clear tutorial/explanation on how to implement completely custom stream operators. (Just for std::ostream
, etc)
How can this be done?
Unfortunately, there is no clear tutorial/explanation on how to implement completely custom stream operators.
OK. Here's a short one.
the operator>>
is a binary operator. In order to make it flexible, you will want to write it as a free function overload, which means you get to customise its meaning at each step.
In order to get the syntax above, you're looking to build a chain of calls top operator>>
such that the ouput of one is the first argument to the next.
so a >> b >> c
really means: operator>>(operator>>(a, b), c)
note that the output of a >> b
is the first input of (a >> b) >> c
.
Here's a very simplified chain which compiles. You will notice I have used value semantics everywhere. If your processing step objects are strict function objects, you could pass them by const&
. If they carry state over from previous uses, then you'll need overloads for &&
r-value references.
#include<fstream>
#include<string>
// a representation of video data
// note-a value type so it will want to own its actual data through a
// unique_ptr or similar.
struct video_data
{
};
struct preprocessing
{
void process(video_data&);
};
struct analyze
{
void process(video_data&);
};
struct stabilize
{
void process(video_data&);
};
struct video_out
{
video_out(std::string const& where);
void process(video_data&);
};
struct flush
{
void process(video_out&);
};
// now define the interactions
auto operator>>(std::string const& path, preprocessing proc) -> video_data
{
video_data dat;
proc.process(dat);
return dat;
}
auto operator>>(video_data dat, analyze proc) -> video_data
{
proc.process(dat);
return dat;
}
auto operator>>(video_data dat, stabilize proc) -> video_data
{
proc.process(dat);
return dat;
}
auto operator>>(video_data dat, video_out proc) -> video_out
{
proc.process(dat);
return std::move(proc);
}
auto operator>>(video_out dat, flush proc) -> video_out
{
proc.process(dat);
return std::move(dat);
}
// now build a chain
int test(std::string const& videofilelocation, std::string const& outputfilename)
{
videofilelocation >> preprocessing() >> analyze() >> stabilize() >> video_out(outputfilename) >> flush();
}
Out of curiosity, I took this as puzzle and got the following small example:
#include <iostream>
#include <string>
// a sample object which is subject of object stream
struct Object {
// contents
std::string name;
int a, b;
// constructors.
Object(int a, int b): a(a), b(b) { }
Object(const std::string &name, int a, int b):
name(name), a(a), b(b)
{ }
};
// a functor to initialize an object (alternatively)
struct source {
Object &obj;
source(Object &obj, int a, int b): obj(obj)
{
this->obj.a = a; this->obj.b = b;
}
operator Object& () { return obj; }
};
// a clear functor
struct clear {
clear() = default;
Object& operator()(Object &in) const
{
in.a = in.b = 0;
return in;
}
};
// make clear functor applicable to object "stream"
Object& operator>>(Object &in, const clear &opClear)
{
return opClear(in);
}
// a global instance
static clear reset;
// an add functor
struct add {
const int da, db;
add(int da, int db): da(da), db(db) { }
Object& operator()(Object &in) const
{
in.a += da; in.b += db;
return in;
}
};
// make add functor applicable to object "stream"
Object& operator>>(Object &in, const add &opAdd)
{
return opAdd(in);
}
// a display functor
struct echo {
const std::string label;
explicit echo(const std::string &label = std::string()):
label(label)
{ }
Object& operator()(Object &in) const
{
std::cout << label
<< "Object '" << in.name << "' (" << in.a << ", " << in.b << ")\n";
return in;
}
};
// make display functor applicable to object "stream"
Object& operator>>(Object &in, const echo &opEcho)
{
return opEcho(in);
}
// a sink functor (actually not needed)
struct null {
null() = default;
void operator()(Object&) const { }
};
// make echo functor applicable to object "stream"
void operator>>(Object &in, const null &opNull) { opNull(in); }
// check it out:
int main()
{
Object obj("obj1", 12, 34);
// either use obj as source
obj
>> echo("before add(-2, -4): ")
>> add(-2, -4)
>> echo("after add(-2, -4): ")
>> reset
>> echo("after reset: ")
>> null();
// or a source operator
source(obj, 11, 22)
>> echo("before add(11, -11): ")
>> add(11, -11)
>> echo("after add(11, -11): ");
return 0;
}
Output:
before add(-2, -4): Object 'obj1' (12, 34)
after add(-2, -4): Object 'obj1' (10, 30)
after reset: Object 'obj1' (0, 0)
before add(11, -11): Object 'obj1' (11, 22)
after add(11, -11): Object 'obj1' (22, 11)
Live Demo on coliru
The basic principle is inspired by stream operators:
The operator>>
s get an Object
reference, process the object (changing its state) and return the reference. This allows to pass an object created in most left argument of expression through the chain of operators.
To apply "function like manipulators", functor classes are used in combination with a respective operator>>
.
To provide "manipulators" which can be used without ()
, a global instance is needed additionally.
Does this help:
class FileLocation {};
class PreProcessedData {};
class PreProcessingAction {
public:
PreProcessedData doStuff(FileLocation const& file) const
{
return PreProcessedData{};
}
};
PreProcessingAction preprocessing() {
return PreProcessingAction{};
}
PreProcessedData operator>>(FileLocation const& file, PreProcessingAction const& action)
{
return action.doStuff(file);
}
int main()
{
FileLocation location;
location >> preprocessing();
}
To be honest this seems to complicate the code more than help.
Would it not be easier to do something like this:
Data file = readData("InputName");
Data preProcData = preprocessing(file);
Data analysedData = analyze(preProcData);
Data stabalizedData = stabilize(analysedData);
Output output("OutputName");
output << stabalizedData;