-
Notifications
You must be signed in to change notification settings - Fork 0
Convert double[][] -> Img & other fun stuff #258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
472ba4a
Convert double[][] -> Img & other fun stuff
gselzer 1e413f9
ParameterConversion: various small fixes
gselzer 8118fcc
Parameter conversion: output conversion, summary
gselzer 5359adc
ParameterConversion: More small fixes
gselzer b5e4935
ParameterConversion: remove unnecessary long[] {}
gselzer dab83f7
ParameterConversions: remove "simple"
gselzer 559033f
ParameterConversion: fix integer division
hinerm 422fa6b
ParameterConversion: correct return javadoc
hinerm b4f0c0f
ParameterConversion: rename engine.convert ops
hinerm 4a159f8
ParameterConversion: unify spaces and tabs
hinerm f5120b1
ParameterConversion: tune copy Op discussion
hinerm f8859da
ParameterConversion: fix copy op javadoc
hinerm cd32113
ParameterConversion: refer back to Op methods
hinerm 21fcbd8
ParameterConversion: differentiate arrayToRAIs
hinerm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,293 @@ | ||
| ======================================== | ||
| Parameter Conversion for Developers | ||
| ======================================== | ||
|
|
||
| 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. | ||
|
|
||
| Basics | ||
| ====== | ||
|
|
||
| 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**. | ||
|
|
||
| .. figure:: https://media.scijava.org/scijava-ops/1.0.1/parameter-conversion-opencv.svg | ||
|
|
||
| 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. | ||
|
|
||
| .. _original-op: | ||
|
|
||
| An example ``Function`` | ||
| ======================= | ||
|
|
||
| Suppose we have a ``Function`` Op that inherently operates on ``RandomAccessibleInterval<DoubleType>``\ s: | ||
|
|
||
| .. code-block:: java | ||
|
|
||
| /** | ||
| * Convolves an image with a kernel, returning the output in a new object | ||
| * | ||
| * @param input the input data | ||
| * @param kernel the kernel | ||
| * @return the convolution of {@code input} and {@code kernel} | ||
| * @implNote op names="filter.convolve" | ||
| */ | ||
| public static RandomAccessibleInterval<DoubleType> convolveNaive( | ||
| final RandomAccessibleInterval<DoubleType> input, | ||
| final RandomAccessibleInterval<DoubleType> kernel | ||
| ) { | ||
| // convolve convolve convolve // | ||
| } | ||
|
|
||
| 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: | ||
|
|
||
|
|
||
| .. code-block:: java | ||
|
|
||
| Img<DoubleType> in = ... | ||
| // 3x3 averaging kernel | ||
| double[][] kernel = { // | ||
| { 1/9d, 1/9d, 1/9d}, // | ||
| { 1/9d, 1/9d, 1/9d}, // | ||
| { 1/9d, 1/9d, 1/9d} // | ||
| }; | ||
| // transform double[][] into a RandomAccessibleInterval | ||
| Img<DoubleType> kernel = ArrayImgs.doubles(data, 3, 3); | ||
| var cursor = kernel.cursor(); | ||
| while(cursor.hasNext()) | ||
| cursor.next().set(kernel[cursor.getIntPosition(0)][cursor.getIntPosition(1)]); | ||
|
|
||
| var result = ops.op("filter.convolve") // | ||
| .input(in, kernel) // | ||
| .outType(new Nil<RandomAccessibleInterval<DoubleType>>() {}) // | ||
| .apply(); | ||
|
|
||
| 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. | ||
|
|
||
| An ``engine.convert`` Op | ||
| ============================== | ||
|
|
||
| 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>``: | ||
|
|
||
| .. code-block:: java | ||
|
|
||
| /** | ||
| * @param input the input data | ||
| * @return an image ({@link RandomAccessibleInterval}) whose values are equivalent to {@code input}s | ||
| * values but converted to {@link DoubleType}s. | ||
| * @implNote op names='engine.convert', type=Function | ||
| */ | ||
| public static RandomAccessibleInterval<DoubleType> arrayToRAI(final double[][] input) | ||
| { | ||
| // Creates an empty image of doubles | ||
| var img = ArrayImgs.doubles(input.length, input[0].length); | ||
| var ra = img.randomAccess(); | ||
| // Deep copies the double[][] into the RAI | ||
| for(int i = 0; i < input.length; i++) { | ||
| for(int j = 0; j < input[0].length; j++) { | ||
| ra.setPositionAndGet(i, j).set(input[i][j]); | ||
| } | ||
| } | ||
| return img; | ||
| } | ||
|
|
||
| Using this ``engine.convert`` Op, SciJava Ops can match our ``filter.convolve`` Op to the user's data, **without explicit translation**. | ||
|
|
||
| .. code-block:: java | ||
|
|
||
| Img<DoubleType> in = ... | ||
| // 3x3 averaging kernel | ||
| double[][] kernel = { // | ||
| { 1/9d, 1/9d, 1/9d}, // | ||
| { 1/9d, 1/9d, 1/9d}, // | ||
| { 1/9d, 1/9d, 1/9d} // | ||
| }; | ||
|
|
||
| // Ideal case - no need to wrap to Img | ||
| var result = ops.op("filter.convolve") // | ||
| .input(in, kernel) // | ||
| .outType(new Nil<RandomAccessibleInterval<DoubleType>>() {}) // | ||
| .apply(); | ||
|
|
||
| At runtime, the Op matcher will invoke the following steps: | ||
|
|
||
| * The ``Img<DoubleType> input`` is left alone, as it is already of the type expected by the Op. | ||
| * The ``double[][] kernel`` is converted to a ``RandomAccessibleInterval<DoubleType> kernel1`` using our ``engine.convert`` Op. | ||
| * The Op convolves ``input1`` with ``kernel1``, returning an ``Img<DoubleType> output1`` | ||
| * The ``Img<DoubleType> input1`` is left alone and returned to the user, as it is already of the type expected by the user. | ||
|
|
||
|
|
||
| Adding efficiency | ||
| ================= | ||
|
|
||
| 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. | ||
|
|
||
| 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: | ||
|
|
||
| .. code-block:: java | ||
|
|
||
| /** | ||
| * @param input the input data | ||
| * @return an image ({@link RandomAccessibleInterval}) backed by the input {@code double[][]} | ||
| * @implNote op names='engine.convert', type=Function | ||
| */ | ||
| public static RandomAccessibleInterval<DoubleType> arrayToRAIWrap(final double[][] input) | ||
| { | ||
| // Wrap 2D array into DoubleAccess usable by ArrayImg | ||
| var access = new DoubleAccess() { | ||
|
|
||
| private final int rowSize = input[0].length; | ||
|
|
||
| @Override | ||
| public double getValue(int index) { | ||
| var row = index / rowSize; | ||
| var col = index % rowSize; | ||
| return input[row][col]; | ||
| } | ||
|
|
||
| @Override | ||
| public void setValue(int index, double value) { | ||
| var row = index / rowSize; | ||
| var col = index % rowSize; | ||
| input[row][col] = value; | ||
| } | ||
| }; | ||
| return ArrayImgs.doubles(access, input.length, input[0].length); | ||
| } | ||
|
|
||
| .. _function-output: | ||
|
|
||
| Converting ``Function`` outputs | ||
| =============================== | ||
|
|
||
| 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: | ||
|
|
||
| .. code-block:: java | ||
|
|
||
| double[][] in = ... | ||
| // 3x3 averaging kernel | ||
| double[][] kernel = { // | ||
| { 1/9d, 1/9d, 1/9d}, // | ||
| { 1/9d, 1/9d, 1/9d}, // | ||
| { 1/9d, 1/9d, 1/9d} // | ||
| }; | ||
|
|
||
| double[][] result = ops.op("filter.convolve") // | ||
| .input(in, kernel) // | ||
| .outType(double[][].class) // | ||
| .apply(); | ||
|
|
||
| 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[][]``: | ||
|
|
||
| .. code-block:: java | ||
|
|
||
| /** | ||
| * @param input the input data | ||
| * @return a {@code double[][]} representation of the input image ({@link RandomAccessibleInterval}) | ||
| * @implNote op names='engine.convert', type=Function | ||
| */ | ||
| public static double[][] raiToArray(final RandomAccessibleInterval<DoubleType> input) | ||
| { | ||
| // Create the array | ||
| var width = input.dimension(0); | ||
| var height = input.dimension(1); | ||
| var result = new double[(int) width][(int) height]; | ||
|
|
||
| // Unfortunately, we have to deep copy here | ||
| var ra = input.randomAccess(); | ||
| for(int i = 0; i < width; i++) { | ||
| for(int j = 0; j < height; j++) { | ||
| result[i][j] = ra.setPositionAndGet(i, j).get(); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| When the user tries to invoke our ``filter.convolve`` ``Function`` Op on all ``double[][]``\ s, the following happens: | ||
|
|
||
| #. Each ``double[][]`` is converted into a ``RandomAccessibleInterval<DoubleType>`` using our ``arrayToRAIWrap`` ``engine.convert`` Op. | ||
| #. The ``filter.convolve`` Op is invoked on the ``RandomAccessibleInterval<DoubleType>``\ s, returning a ``RandomAccessibleInterval<DoubleType>`` as output. | ||
| #. This output ``RandomAccessibleInterval<DoubleType>`` is converted into a ``double[][]`` using our ``raiToArray`` ``engine.convert`` Op. | ||
| #. The **converted** ``double[][]`` output is returned to the user. | ||
|
|
||
| The result is offering to the user a ``filter.convolve(input: double[][], kernel: double[][]) -> double[][]`` Op, even though we never wrote one! | ||
|
|
||
| Converting ``Computer`` and ``Inplace`` outputs | ||
| =============================================== | ||
|
|
||
| Finally, consider our ``filter.convolve`` Op example, instead written as a ``Computer``. | ||
|
|
||
| .. code-block:: java | ||
|
|
||
| /** | ||
| * Convolves an image with a kernel, placing the result in the output buffer | ||
| * | ||
| * @param input the input data | ||
| * @param kernel the kernel | ||
| * @param output the result buffer | ||
| * @implNote op names="filter.convolve" | ||
| */ | ||
| public static void convolveNaive( | ||
| final RandomAccessibleInterval<DoubleType> input, | ||
| final RandomAccessibleInterval<DoubleType> kernel, | ||
| final RandomAccessibleInterval<DoubleType> output | ||
| ) { | ||
| // convolve convolve convolve // | ||
| } | ||
|
|
||
| Suppose that again the user wants to call this Op using *only* ``double[][]``\ s: | ||
|
|
||
| .. code-block:: java | ||
|
|
||
| double[][] in = ... | ||
| // 3x3 averaging kernel | ||
| double[][] kernel = { // | ||
| { 1/9d, 1/9d, 1/9d}, // | ||
| { 1/9d, 1/9d, 1/9d}, // | ||
| { 1/9d, 1/9d, 1/9d} // | ||
| }; | ||
| double[][] result = new double[in.length][in[0].length]; | ||
|
|
||
| ops.op("filter.convolve").input(in, kernel).output(result).apply(); | ||
|
|
||
| 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). | ||
|
|
||
| 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**. | ||
|
|
||
| Below is an ``engine.copy`` Op that would store the converted Op's output ``double[][]`` back into the user's Object: | ||
|
|
||
| .. code-block:: java | ||
|
|
||
| /** | ||
| * Copy one {@code double[][]} to another. | ||
| * | ||
| * @param opOutput the {@code double[][]} converted from the Op output | ||
| * @param userBuffer the original {@code double[][]} provided by the user | ||
| * @implNote op names="engine.copy" type=Computer | ||
| */ | ||
| public static void copyDoubleMatrix( | ||
| final double[][] opOutput, | ||
| final double[][] userBuffer | ||
| ) { | ||
| for(int i = 0; i < opOutput.length; i++) { | ||
| System.arraycopy(opOutput[i], 0, userBuffer[i], 0, opOutput[i].length); | ||
| } | ||
| } | ||
|
|
||
| When the user tries to invoke our ``filter.convolve`` ``Computer`` Op on all ``double[][]``\ s, the following happens: | ||
|
|
||
| #. Each ``double[][]`` is converted into a ``RandomAccessibleInterval<DoubleType>`` using our ``arrayToRAIWrap`` ``engine.convert`` Op. | ||
| #. The ``filter.convolve`` Op is invoked on the ``RandomAccessibleInterval<DoubleType>``\ s, returning a ``RandomAccessibleInterval<DoubleType>`` as an output. | ||
| #. The output ``RandomAccessibleInterval<DoubleType>`` is converted into a ``double[][]`` using our ``raiToArray`` ``engine.convert`` Op. | ||
| #. The **converted** output ``double[][]`` is *copied* back into the user's ``double[][]`` buffer using our ``copyDoubleMatrix`` ``engine.copy`` Op. | ||
|
|
||
| Summary | ||
| ======= | ||
|
|
||
| All in all, you can enable parameter conversion from type ``A`` to type ``B`` by providing the following Ops: | ||
|
|
||
| * An ``engine.convert(input: A) -> B`` for input conversion | ||
| * An ``engine.convert(input: B) -> A`` for output conversion | ||
| * 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. | ||
|
|
||
| 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. | ||
|
|
||
| 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``. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.