During last few days I decide to go further with Java 8 stream, lambda. As I currently work on differents projects set to Java 5 compatibility I does not have a lot of "material" for experiment.
Learn
So I began, as usual with google to find out example and/or tutorial.
I mainly found cases of studies starting with simple list, applying mapping, filtering and finally reducing.
Those examples are very usefull and well explain and were helpfull as starting point :
Apply
Now it time to try ourself. I do not want to work with case of studies beacause I want to think about about the solution and find myself how solve with stream a given problem. And, I want to make something (I hope) usefull (almost for me...).
So which example ? A long time a ago, I worked on image processing,
segmentation,
morphology transformation,
skeletonisation,
hough transform, on color (TLS, RVB), gray level or binary images.
An image is a good client for my streaming example :
- could be usefull
- could be enough big to see parallel effect
- you could easily imagine how to apply a structuring element on an image stream
Imagine the stream
As I want to apply a structuring element 3x3, 5x5, nxn I think about :
- walking on a stream of vectors building from original image. Those vectors has the same dimension as the structuring element.
- iterate on an Integer Stream of (height x width) of image for limit, that allow to build the target image
I give up the first solution beacause :
- the memory foot print is four time the image size for a 3x3 kernel. 3 for stream + 1 for target.
- the time for stream building is in excess and depend of the kernel size.
Stream coding
To do the second solution I have to :
- iterate on a range [0 , heigh * width]
- for each calculate the value resulting of kernel application
- put it in the the resulting image.
I found to two way to implement this :
IntStream.range(0, (heigh * width)-1).forEach(n->{applyKernel(n)})
or
Stream.iterate(0, n -> n+1).limit(heigh * width) - 1).forEach(n-> {applyKernel(n)})
The applyKernel(int n) method is for the target pixel resulting of kernel application ( for the pixel n)
To find the right way to implement, I saw result time for the same image, the same number of time in the same machine, excluding the worst 2 cases for each. The winner is the first solution with 880 ms compare with 1000ms.
Why? I think It's because IntStream is provide for this purpose exactly -ie providing an int range. Unlike Stream.iterate() with (plus) limit() is done to generate a stream by applying the given function on the seed. It's something that is more sophisticated. Another point could be that the limit method produce another stream(). So at the end I think there is more "mechanics" in the second solution that implie less speed.
Processing image
In this example to process image I
- load it as a BufferedImage
- convert it to gray (byte)
- filter it with an octagonal
- convert it to binary relying on DataBufferByte
- 2 closing (dilate x 2 then erode x 2)
- then edge detection
Load image
File imageFile = ...
BufferedImage img = ImageIO.read(imageFile);
Convert to byte gray
BufferedImage grayImg= new BufferedImage(widht, heigh, BufferedImage.TYPE_BYTE_GRAY);
BufferedImageOp gsOp = new ColorConvertOp(
imageToConvert.getColorModel().getColorSpace(),
grayImg.getColorModel().getColorSpace(),null);
gsOp.filter(imageToConvert, grayImg);
Filter with Octagon kernel
It is the application of this kernel
|1,1,1|
|1,1,1| divide by 9
|1,1,1|
It means that the resulting pixel is the average of itself and the 8 pixels around it.
To do that I use the
byte[] imageAsByteArray = ((DataBufferByte)image.getRaster().getDataBuffer()).getData();
Convert to binary
It means iterate on gray scale byte[] and set resulting pixel to 0 or 255 depending of the imput value.
If the imput value is more than a threshold the valur will be 255 else 0.
Closing
for erosion and dilatation I use a CROSS kernel
|false,true,false|
|true ,true, true|
|false,true,false|
For dilatation I apply this on binary image (255 is true, 0 is false) if the kernel application (logical OR) on the given image contexte return true then the result is 255 , 0 otherwhise.
For erosion this is the same kernel but the application use a logical AND.
The closing operator effect is filling small hole in image. The hole size fill up depend of the kernel size an the number of succesive erosion, dilatation.
Edge detection
On binary image I apply the laplacian square using multiplication.
|0, 1,0|
|1,-4, 1|
|0, 1,0|
Stream and parallel
Now it's time to test parallel feature. I red a lot of bad thing about parallel but it seems that all those "problems" always appear in concurrent enviromnent. See :
The big deal is that stream allows nows to acheive those easily/instantly without need to use Thread or other framework, just add .parallel() and you got it.
How to use it in my example. I just do like that :
IntStream.range(0, (heigh * width)-1).parallel().forEach(n->{applyKernel(n)})
If you want to control the number of thread use (
see here for more informations) :
ForkJoinPool forkJoinPool = new forkJoinPool(4);
...
forkJoinPool.submit(() ->
IntStream.range(0, (heigh * width)-1).parallel().forEach(n->{applyKernel(n)})
).get()
At the end the time is cut by 2 going from 880 ms to 430 ms to does erosion.
So nice and easy. So is there any problems around there ?
Well as I understand problem could come from :
Conclusion
I have now a better understanding on Java stream pro and cons. Enough to think about Stream the next time I face an analoguoud context.