Skip to content

Commit 7ea591f

Browse files
authored
Merge pull request #258 from scijava/scijava-ops-image/double-mat-to-img-converter
Add developer example explaining converter framework needs
2 parents 52b6201 + 21fcbd8 commit 7ea591f

File tree

4 files changed

+347
-26
lines changed

4 files changed

+347
-26
lines changed
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
========================================
2+
Parameter Conversion for Developers
3+
========================================
4+
5+
In this example, we explain parameter conversion to a developer audience. This page provides an overview of what parameter conversion involves, how it works, and how you can enable conversion for your own data types.
6+
7+
Basics
8+
======
9+
10+
A :ref:`value <driving-values>` of SciJava Ops is flexibility, and flexibility is (in part) achieved through **parameter conversion**. At its core, parameter conversion allows *translation* of data stored in one data structure (e.g. an ImgLib2 ``RandomAccessibleInterval``) into a different data structure (e.g. an OpenCV ``Mat``) **on the fly**. This allows SciJava Ops to execute Ops backed by OpenCV code **on ImgLib2 data structures**.
11+
12+
.. figure:: https://media.scijava.org/scijava-ops/1.0.1/parameter-conversion-opencv.svg
13+
14+
At matching time, parameter conversion is invoked when an Op matches a user request in name and in Op type, but differing in individual parameter types. In these situations, it looks for ``engine.convert`` Ops that could potentially convert the user's provided inputs into the required Op inputs, and the same, in the other direction, for the output.
15+
16+
.. _original-op:
17+
18+
An example ``Function``
19+
=======================
20+
21+
Suppose we have a ``Function`` Op that inherently operates on ``RandomAccessibleInterval<DoubleType>``\ s:
22+
23+
.. code-block:: java
24+
25+
/**
26+
* Convolves an image with a kernel, returning the output in a new object
27+
*
28+
* @param input the input data
29+
* @param kernel the kernel
30+
* @return the convolution of {@code input} and {@code kernel}
31+
* @implNote op names="filter.convolve"
32+
*/
33+
public static RandomAccessibleInterval<DoubleType> convolveNaive(
34+
final RandomAccessibleInterval<DoubleType> input,
35+
final RandomAccessibleInterval<DoubleType> kernel
36+
) {
37+
// convolve convolve convolve //
38+
}
39+
40+
Suppose a user wants to use this Op with a small, fixed kernel, which for ease is written as a ``double[][]``. Without additional aid, they'd have to manually convert their ``double[][]`` into a ``RandomAccessibleInterval<DoubleType>``, requiring knowledge of how to do that and baking extra boilerplate into their workflow:
41+
42+
43+
.. code-block:: java
44+
45+
Img<DoubleType> in = ...
46+
// 3x3 averaging kernel
47+
double[][] kernel = { //
48+
{ 1/9d, 1/9d, 1/9d}, //
49+
{ 1/9d, 1/9d, 1/9d}, //
50+
{ 1/9d, 1/9d, 1/9d} //
51+
};
52+
// transform double[][] into a RandomAccessibleInterval
53+
Img<DoubleType> kernel = ArrayImgs.doubles(data, 3, 3);
54+
var cursor = kernel.cursor();
55+
while(cursor.hasNext())
56+
cursor.next().set(kernel[cursor.getIntPosition(0)][cursor.getIntPosition(1)]);
57+
58+
var result = ops.op("filter.convolve") //
59+
.input(in, kernel) //
60+
.outType(new Nil<RandomAccessibleInterval<DoubleType>>() {}) //
61+
.apply();
62+
63+
Ideally, the user could just pass their ``double[][]`` to their Op matching call directly. Parameter conversion enables this, through the use of ``engine.convert`` Ops written by developers.
64+
65+
An ``engine.convert`` Op
66+
==============================
67+
68+
All ``engine.convert`` Ops are ``Function``\ s that are given user arguments and return a *translation* of that data into the type expected by the Op. For our example ``Function``, we want to convert *from* the user's ``double[][]`` into a ``RandomAccessibleInterval<DoubleType>``:
69+
70+
.. code-block:: java
71+
72+
/**
73+
* @param input the input data
74+
* @return an image ({@link RandomAccessibleInterval}) whose values are equivalent to {@code input}s
75+
* values but converted to {@link DoubleType}s.
76+
* @implNote op names='engine.convert', type=Function
77+
*/
78+
public static RandomAccessibleInterval<DoubleType> arrayToRAI(final double[][] input)
79+
{
80+
// Creates an empty image of doubles
81+
var img = ArrayImgs.doubles(input.length, input[0].length);
82+
var ra = img.randomAccess();
83+
// Deep copies the double[][] into the RAI
84+
for(int i = 0; i < input.length; i++) {
85+
for(int j = 0; j < input[0].length; j++) {
86+
ra.setPositionAndGet(i, j).set(input[i][j]);
87+
}
88+
}
89+
return img;
90+
}
91+
92+
Using this ``engine.convert`` Op, SciJava Ops can match our ``filter.convolve`` Op to the user's data, **without explicit translation**.
93+
94+
.. code-block:: java
95+
96+
Img<DoubleType> in = ...
97+
// 3x3 averaging kernel
98+
double[][] kernel = { //
99+
{ 1/9d, 1/9d, 1/9d}, //
100+
{ 1/9d, 1/9d, 1/9d}, //
101+
{ 1/9d, 1/9d, 1/9d} //
102+
};
103+
104+
// Ideal case - no need to wrap to Img
105+
var result = ops.op("filter.convolve") //
106+
.input(in, kernel) //
107+
.outType(new Nil<RandomAccessibleInterval<DoubleType>>() {}) //
108+
.apply();
109+
110+
At runtime, the Op matcher will invoke the following steps:
111+
112+
* The ``Img<DoubleType> input`` is left alone, as it is already of the type expected by the Op.
113+
* The ``double[][] kernel`` is converted to a ``RandomAccessibleInterval<DoubleType> kernel1`` using our ``engine.convert`` Op.
114+
* The Op convolves ``input1`` with ``kernel1``, returning an ``Img<DoubleType> output1``
115+
* The ``Img<DoubleType> input1`` is left alone and returned to the user, as it is already of the type expected by the user.
116+
117+
118+
Adding efficiency
119+
=================
120+
121+
While the above ``engine.convert`` Op is *functional*, it may not be *fast* as the data size increases. This is due to the **copy** inherent in its execution, as the ``ArrayImg`` contains new data structures.
122+
123+
In such cases, devising methods to instead *wrap* user arguments will maximize performance and wow your users. In our case, we can refine our ``engine.convert`` Op to wrap user data, using the ``DoubleAccess`` interface of ImgLib2:
124+
125+
.. code-block:: java
126+
127+
/**
128+
* @param input the input data
129+
* @return an image ({@link RandomAccessibleInterval}) backed by the input {@code double[][]}
130+
* @implNote op names='engine.convert', type=Function
131+
*/
132+
public static RandomAccessibleInterval<DoubleType> arrayToRAIWrap(final double[][] input)
133+
{
134+
// Wrap 2D array into DoubleAccess usable by ArrayImg
135+
var access = new DoubleAccess() {
136+
137+
private final int rowSize = input[0].length;
138+
139+
@Override
140+
public double getValue(int index) {
141+
var row = index / rowSize;
142+
var col = index % rowSize;
143+
return input[row][col];
144+
}
145+
146+
@Override
147+
public void setValue(int index, double value) {
148+
var row = index / rowSize;
149+
var col = index % rowSize;
150+
input[row][col] = value;
151+
}
152+
};
153+
return ArrayImgs.doubles(access, input.length, input[0].length);
154+
}
155+
156+
.. _function-output:
157+
158+
Converting ``Function`` outputs
159+
===============================
160+
161+
Now, imagine that the user wished to execute the Op using **only** ``double[][]``\ s. In other words, they have a ``double[][] input``, a ``double[][] kernel``, and want back a ``double[][]`` containing the result:
162+
163+
.. code-block:: java
164+
165+
double[][] in = ...
166+
// 3x3 averaging kernel
167+
double[][] kernel = { //
168+
{ 1/9d, 1/9d, 1/9d}, //
169+
{ 1/9d, 1/9d, 1/9d}, //
170+
{ 1/9d, 1/9d, 1/9d} //
171+
};
172+
173+
double[][] result = ops.op("filter.convolve") //
174+
.input(in, kernel) //
175+
.outType(double[][].class) //
176+
.apply();
177+
178+
Looking back at our :ref:`original Op<original-op>`, we would have to write an *additional* converter to turn the output ``RandomAccessibleInterval<DoubleType>`` back into a ``double[][]``:
179+
180+
.. code-block:: java
181+
182+
/**
183+
* @param input the input data
184+
* @return a {@code double[][]} representation of the input image ({@link RandomAccessibleInterval})
185+
* @implNote op names='engine.convert', type=Function
186+
*/
187+
public static double[][] raiToArray(final RandomAccessibleInterval<DoubleType> input)
188+
{
189+
// Create the array
190+
var width = input.dimension(0);
191+
var height = input.dimension(1);
192+
var result = new double[(int) width][(int) height];
193+
194+
// Unfortunately, we have to deep copy here
195+
var ra = input.randomAccess();
196+
for(int i = 0; i < width; i++) {
197+
for(int j = 0; j < height; j++) {
198+
result[i][j] = ra.setPositionAndGet(i, j).get();
199+
}
200+
}
201+
return result;
202+
}
203+
204+
When the user tries to invoke our ``filter.convolve`` ``Function`` Op on all ``double[][]``\ s, the following happens:
205+
206+
#. Each ``double[][]`` is converted into a ``RandomAccessibleInterval<DoubleType>`` using our ``arrayToRAIWrap`` ``engine.convert`` Op.
207+
#. The ``filter.convolve`` Op is invoked on the ``RandomAccessibleInterval<DoubleType>``\ s, returning a ``RandomAccessibleInterval<DoubleType>`` as output.
208+
#. This output ``RandomAccessibleInterval<DoubleType>`` is converted into a ``double[][]`` using our ``raiToArray`` ``engine.convert`` Op.
209+
#. The **converted** ``double[][]`` output is returned to the user.
210+
211+
The result is offering to the user a ``filter.convolve(input: double[][], kernel: double[][]) -> double[][]`` Op, even though we never wrote one!
212+
213+
Converting ``Computer`` and ``Inplace`` outputs
214+
===============================================
215+
216+
Finally, consider our ``filter.convolve`` Op example, instead written as a ``Computer``.
217+
218+
.. code-block:: java
219+
220+
/**
221+
* Convolves an image with a kernel, placing the result in the output buffer
222+
*
223+
* @param input the input data
224+
* @param kernel the kernel
225+
* @param output the result buffer
226+
* @implNote op names="filter.convolve"
227+
*/
228+
public static void convolveNaive(
229+
final RandomAccessibleInterval<DoubleType> input,
230+
final RandomAccessibleInterval<DoubleType> kernel,
231+
final RandomAccessibleInterval<DoubleType> output
232+
) {
233+
// convolve convolve convolve //
234+
}
235+
236+
Suppose that again the user wants to call this Op using *only* ``double[][]``\ s:
237+
238+
.. code-block:: java
239+
240+
double[][] in = ...
241+
// 3x3 averaging kernel
242+
double[][] kernel = { //
243+
{ 1/9d, 1/9d, 1/9d}, //
244+
{ 1/9d, 1/9d, 1/9d}, //
245+
{ 1/9d, 1/9d, 1/9d} //
246+
};
247+
double[][] result = new double[in.length][in[0].length];
248+
249+
ops.op("filter.convolve").input(in, kernel).output(result).apply();
250+
251+
We will certainly need the ``engine.convert(in: double[][]) -> RandomAccessibleInterval<DoubleType>`` Op and the ``engine.convert(in: RandomAccessibleInterval<DoubleType>) -> double[][]`` Op we wrote above, however if we follow the same procedure with :ref:`Functions <function-output>`, the ``result`` array they provided will be empty/unmodified. This is because our ``raiToArray` ``engine.convert`` Op we wrote above *creates a new ``double[][]``*. Writing ``engine.convert`` Ops as wrappers is ideal, but in cases like this may not be possible (i.e. we can't create a custom ``double[][]`` implementation).
252+
253+
Because SciJava Ops cannot guarantee that ``engine.convert`` Ops wrap user arguments, an additional step is required for parameter conversion with ``Computer`` Ops. This is done by calling an ``engine.copy`` Op to copy the converted output *back into the user's object*. **If you want to enable parameter conversion** on ``Computer``\ s or ``Inplace``\ s, **you must implement** an ``engine.copy`` identity Op for your data type in addition to any ``engine.convert`` Ops. Because there is no way to know how Ops will be implemented (and ``Computer``\s do make a large portion of current Ops) **this is highly recommended**.
254+
255+
Below is an ``engine.copy`` Op that would store the converted Op's output ``double[][]`` back into the user's Object:
256+
257+
.. code-block:: java
258+
259+
/**
260+
* Copy one {@code double[][]} to another.
261+
*
262+
* @param opOutput the {@code double[][]} converted from the Op output
263+
* @param userBuffer the original {@code double[][]} provided by the user
264+
* @implNote op names="engine.copy" type=Computer
265+
*/
266+
public static void copyDoubleMatrix(
267+
final double[][] opOutput,
268+
final double[][] userBuffer
269+
) {
270+
for(int i = 0; i < opOutput.length; i++) {
271+
System.arraycopy(opOutput[i], 0, userBuffer[i], 0, opOutput[i].length);
272+
}
273+
}
274+
275+
When the user tries to invoke our ``filter.convolve`` ``Computer`` Op on all ``double[][]``\ s, the following happens:
276+
277+
#. Each ``double[][]`` is converted into a ``RandomAccessibleInterval<DoubleType>`` using our ``arrayToRAIWrap`` ``engine.convert`` Op.
278+
#. The ``filter.convolve`` Op is invoked on the ``RandomAccessibleInterval<DoubleType>``\ s, returning a ``RandomAccessibleInterval<DoubleType>`` as an output.
279+
#. The output ``RandomAccessibleInterval<DoubleType>`` is converted into a ``double[][]`` using our ``raiToArray`` ``engine.convert`` Op.
280+
#. The **converted** output ``double[][]`` is *copied* back into the user's ``double[][]`` buffer using our ``copyDoubleMatrix`` ``engine.copy`` Op.
281+
282+
Summary
283+
=======
284+
285+
All in all, you can enable parameter conversion from type ``A`` to type ``B`` by providing the following Ops:
286+
287+
* An ``engine.convert(input: A) -> B`` for input conversion
288+
* An ``engine.convert(input: B) -> A`` for output conversion
289+
* An ``engine.copy(converted_output: B, user_buffer: B)`` for ``Computer``\ s and ``Inplace``\ s, to move the converted output into the user's buffer object.
290+
291+
Note that, in the process of creating your ``engine.convert`` ``Function`` Ops, you'll likely want to write some ``engine.create`` Ops that could produce objects of type ``B``. In addition to making your ``engine.convert`` Ops more granular by using them as Op dependencies, but they'll additionally help enable features like Op adaptation.
292+
293+
Beyond this, it would also be helpful to ensure that an ``engine.copy(converted_output: A, user_buffer: A)`` Op exists, such that users can also call *your* ``Computer`` and ``Inplace`` Ops using objects of type ``A``.

0 commit comments

Comments
 (0)