There are basically four ways to access individual pixel samples in the current JavaScript runtime:
1. The Image.sample() and Image.setSample() methods.
2. Image sample and pixel iterators.
3. The Image.forEachSample() and Image.forEachMutableSample() methods.
4. The Image.getSamples() and Image.setSamples() methods.
For the last method we can use either Array objects or typed array objects (Float32Array, Uint16Array, etc).
Here is a little script that shows the existing performance differences between these methods and techniques.
JavaScript:
/*
* Pixel access benchmarks
*/
#include <pjsr/UndoFlag.jsh>
function test_setSample( image )
{
for ( let y = 0; y < image.height; ++y )
for ( let x = 0; x < image.width; ++x )
image.setSample( 1 - image.sample( x, y ), x, y );
}
function test_sampleIterator( image )
{
image.initSampleIterator();
do
image.setSampleValue( 1 - image.sampleValue() );
while ( image.nextSample() );
}
function test_forEachSample( image )
{
image.forEachSample( (x, y, c, v) => { image.setSample( 1 - v, x, y, c ); } );
}
function test_forEachMutableSample( image )
{
image.forEachMutableSample( (x, y, c, v) => 1 - v );
}
function test_sampleArray( image )
{
let A = [];
image.getSamples( A );
for ( let i = 0; i < A.length; ++i )
A[i] = 1 - A[i];
image.setSamples( A );
}
function test_sampleTypedArray( image )
{
let A = new Float32Array( image.numberOfPixels );
image.getSamples( A );
for ( let i = 0; i < A.length; ++i )
A[i] = 1 - A[i];
image.setSamples( A );
}
function main()
{
let window = new ImageWindow( 2048, /*width*/
2048, /*height*/
1, /*numberOfChannels*/
32, /*bitsPerSample*/
true, /*floatSample*/
false, /*color*/
"test_image" /*id*/ );
window.mainView.beginProcess( UndoFlag_NoSwapFile );
let image = window.mainView.image;
let T = new ElapsedTime;
T.reset();
test_setSample( image );
let t1 = T.value;
T.reset();
test_sampleIterator( image );
let t2 = T.value;
T.reset();
test_forEachSample( image );
let t3 = T.value;
T.reset();
test_forEachMutableSample( image );
let t4 = T.value;
T.reset();
test_sampleArray( image );
let t5 = T.value;
T.reset();
test_sampleTypedArray( image );
let t6 = T.value;
window.mainView.endProcess();
window.forceClose();
let t0 = Math.min( t1, t2, t3, t4, t5, t6 );
console.writeln( "<end><cbr><br>test | time | rel.t" );
console.writeln( "---------------------+------------+------" );
console.writeln( format( "setSample | %10s | %5.2f", ElapsedTime.toString( t1 ), t1/t0 ) );
console.writeln( format( "sampleIterator | %10s | %5.2f", ElapsedTime.toString( t2 ), t2/t0 ) );
console.writeln( format( "forEachSample | %10s | %5.2f", ElapsedTime.toString( t3 ), t3/t0 ) );
console.writeln( format( "forEachMutableSample | %10s | %5.2f", ElapsedTime.toString( t4 ), t4/t0 ) );
console.writeln( format( "sampleArray | %10s | %5.2f", ElapsedTime.toString( t5 ), t5/t0 ) );
console.writeln( format( "sampleTypedArray | %10s | %5.2f", ElapsedTime.toString( t6 ), t6/t0 ) );
}
main();
Here is a sample output from this script on the machine where I'm writing this:
Code:
test | time | rel.t
---------------------+------------+------
setSample | 781.405 ms | 10.68
sampleIterator | 121.109 ms | 1.65
forEachSample | 506.530 ms | 6.92
forEachMutableSample | 423.575 ms | 5.79
sampleArray | 473.596 ms | 6.47
sampleTypedArray | 73.191 ms | 1.00
The script inverts all the pixels of a monochrome, 32-bit floating point 4 Mpx image, using the four methods enumerated above, plus the Float32Array variant of the fourth method. Then it writes a report with absolute and relative execution times (relative to the fastest routine).
As you can see, the least efficient way to read/write pixels is using the sample() and setSample() methods. Although these methods offer an intuitive way to process pixels by varying coordinates and reading/writing individual pixel samples separately, their execution times are one order of magnitude longer than the fastest techniques available. The reason for this bad performance is the amount of required interactions between JavaScript code and the underlying native C++ implementation, which prevent an optimal JIT compilation from JavaScript to machine code.
The most efficient technique is using the getSamples() and setSamples() methods with typed arrays (Float32Array in this case). This happens because typed arrays are extremely efficient, both for JIT optimization on the JavaScript side, and for direct manipulation in the native C++ implementation. Of course, the drawback of these methods is their additional memory space requirements. Basically, you need to work on a temporary duplicate of the image region that you want to process. This usually poses no practical problems, unless your script has to work with huge images under strict memory space constraints.
Using getSamples()/setSamples() with Array objects is about six times slower than the same methods used with typed arrays. This happens because Array objects are dynamic and potentially non-homogeneous containers, as seen from the native C++ implementation, requiring a higher degree of interaction with the JavaScript engine. This prevents linear access to array data, which in turn disables almost all code optimizations that make typed arrays so fast.
An excellent alternative is using sample and pixel iterators. This technique is only about a 65% less efficient than using typed arrays, but has no additional space requirements.
Finally, execution times using forEachSample() are about the same as those of getSamples()/setSamples() with Array objects, requiring no additional space.
So the bottom line is:
- For the fastest possible implementation, use getSamples()/setSamples() with typed arrays.
- For relatively good performance without additional space requirements, use image iterators.
- Avoid using the sample() and setSample() methods in production code, except for very simple cases or small images. They are great for demonstration purposes and can facilitate debugging in some cases, though.