Spoiler
My question is the following: Are there any design patterns for representing chainable functions that are for the problem described below?
High-Level Decription of the Process
I'm currently building an image-processing server, whose functionality is exposed via a web API.
Client Behavior
Clients first authenticate, then request that one or more analyses be performed by the server. Once a client has authenticated and requested his analysis stack, he streams his video data to the analysis server.
Server Behavior
On the server side, the Stream
class is used to represent the flow of data. It's the first-class object around which the pipeline is built, and it can be thought of as an ordered FIFO queue of sorts. Frames are pushed, in order of arrival, onto the Stream
instance, and each stream may optionally map a function onto its data. Results from a mapped function are projected onto a new Stream
instance.
For example:
# input_stream is asynchronously updated with incoming frames
stream = input_stream.map(do_sobel_filter) # apply Sobel filter to each incoming frame
stream2 = stream.window(fn, 10) # apply function to a sliding window of 10 frames
An important side-note is that our use of Stream
objects allows us to apply functions either to individual frames or to groups of frames. This is useful as some analyses require context.
Requirements & Constraints
The above bold text is particularly important; A given client will be authorized only to perform a subset of the image analyses offered by our system. This in turn makes it critical that we be able to dynamically compose the image-processing pipeline -- we don't want to perform expensive computations only to throw away the result.
Further complicating the matter is the fact that almost all of the analyses we offer have some prerequisites, that is, operation Oi may depend on the result of operation Oi-n. Our system needs to resolve these dependencies and run each operation exactly once.
Finally, the chaining/composition of operations must be associative. This is because we'd like to be able to compose reusable higher-level computations from lower-level atomic computations. For instance, consider the sequence of operations x -> y -> z
. We would like to be able to assign the ordered chain x->y->z
to a variable, thus CMP1 = x->y->z
. Similarly, CMP2
is the result of chaining operations a->b->c
. Ultimately, we would like to be able to do the equivalent of CMP3 = CMP1 -> CMP2
, and have this be strictly equivalent to CMP3 = x->y->z->a->b->c
(associativity).
To summarize, here are the requirements for my image analysis function objects:
- Functions must be composeable, meaning that it should be possible to manipulate a first-class object that represents a series of sub-computations.
- Ideally, functions should resolve dependencies, meaning that doing
A -> B
should implicitly evaluate toA -> X -> C
ifC
depends on bothA
andX
. This may be worthy of it's own question, and so should be treated as a soft requirement. - Chaining of operations should be associative, and composed functions should also be chainable. The idea is to have a reusable pattern for representing increasingly complex pipelines.
Are there any well-tested patterns for achieving such a result?
Edits:
1) In response to @Giorgio's questions, my (admittedly ambiguous) notation serves to distinguish between the temporal order of frames and the order of operations. Below is an example of how an operation can depend on its preceding counterparts:
FindFace -> FindEyes -> SegmentEye -> MeasurePupilDiameter
Here we can see that each function after FindFace
depends on the cumulative result of the preceding functions. Now consider:
FindFace -> FindEyes -> SegmentEye -> GetIrisColorHistogram
This second example demonstrates how there is often a common core of operations that needs to be performed across analyses. When composing operations, I'd like to do steps 1 - 3 only once, and then apply MeasurePupilDiameter
and GetIrisColorHistogram
from there.
Intuitively, it seems like the right way to do this would be to assign a dictionary to my Frame
object and fill it with features as they're computed -- a memoize pattern, if you will. From there, however, it's unclear to me how I should go about abstracting all the boilerplate code pertaining to (a) checking if a prerequisite feature has already been computed and (b) automatically calling the appropriate function if it has not.
f
to each new image, or what does the function to be applied to each image depend on? (2) By operationOi
, do you mean the application of such a functionf
to imagei
? (3) When an operationOi
depends on the result of a previous operationO(i-n)
, how far in the past is this previous operation? (4) In general, can an operation depend on the result of one previous operation only, or on more than one (list of previous results)?