Show HN: Bild – Simple image processing in Go with parallel processing support
github.com
github.com
Even though I might not use this package as I don't have any Go project in my pipeline, reading the code was a nice learning experience and just demystified a few things about image processing for me.
Go might not be one of my favorite languages, but its readability is just great (or maybe this codebase isn't idiomatic Go?).
Part of that is because there aren't any 'higher level' functions so you are forced to write everything out as explicit loops. Often a bit tedious but it is definitely easier to see what exactly is happening when reading the code.
Side rant on Haskel: I really wanted to get into it, but when you see function signatures like this:
add :: Integer -> Integer -> Integer
You have to wonder what pipe they are smoking. I'm sure someone is going to tell me there is an obscure compsci reason why it couldn't have been this: add :: Integer, Integer -> Integer
But that is just stupid, sorry.Since you're touting the simplicity of Go, I can describe a reason why the function signatures is written like that: a,b,c->d would be a special case of a->b->c->d (multi-arity function without currying, I guess?), so it's not needed.
If it was written as
add :: Integer, Integer -> Integer
that would imply that it was a single function taking 2 arguments and that if you called it with only 1 argument it would fail instead of returning you a curried add function (a bit like partial application but not exactly).
I'm sure someone is going to tell me there is an obscure compsci reason why it couldn't have been this: [...]
This "obscure" reason is actually one of the core features of the language and one of the reasons it's called Haskell. A thing that you are going to use all the time writing Haskell. The reason is called currying: https://en.m.wikipedia.org/wiki/Currying add :: (Integer, Integer) -> Integer
but then you couldn't write add 1
and use it as a function. And that would be just stupid, sorry.So something like:
add 1 _
or whatever (I'm not Haskeller so hard for me to judge what exact syntax) would do.And then
add _ 1
would be easily possible too. add _ (minus 1 _)
mean? Not the same as let f = minus 1 _
in add _ f
and things would get very weird very fast.But I will definitely post some benchmarking results soon, as they are crucial for the optimization stage.
By the way, any suggestions/contributions to the project are more than welcome!
All of the blur/ functions could see big improvements with some simple changes:
* Use separable kernels whenever possible (this reduces a O(K^2) evaluation to 2K evaluations)
Some filters (like your box filter) can be done efficiently as a linear combination of IID filters (i.e. implicitly computing a pair of running sums, subtracting one from the other as you go).
* The convolve/ package could use some cache blocking, and should definitely have the conditionals on the inner-loop removed.
Unfortunately, these changes will likely make your code a bit more difficult to read.
This is one of the reasons Halide has become so popular for image-processing (if you're interested in high performance image processing without sacrificing maintainability, Halide is definitely worth looking into!)
Halide looks very interesting, I found the "Halide Talk" video on their website to be a great primer on their methods.
This is very clear code though and great for learning.
The goal of the library is ease of use and development instead of being the fastest implementation (and then loosing too much readability). That being said, it is not slow but there's still room for improvement, which will be addressed during the optimisation and bug fixing stage.
I'm not sure if it's the same as xv, to be honest I haven't worked with it before. But it looks interesting, thanks for the link!
You will usually see it like this:
parallel.Line(height, func(start, end int) {
for y := start; y < end; y++ {
for x := 0; x < w; x++ {
pos:= y*img.Stride + x*4
// ...
}
}
})
parallel.Line is a function that takes a length int and a fn func(start, end int) and it splits the provided length into segments, then it dispatches the provided fn with each segment range to a new goroutine. The number of segments is defined by the number of available logical CPUs.So basically each dispatched fn is iterating over its assigned range of the Pix []uint8 and the image is split into chunks by height. Notice that only the y-axis loop is assigned a partial range and we iterate over the x-axis first, this is because we want to move sequentially in the slice as much as possible.
During the optimization stage I would like to benchmark against a tiled segmentation instead of a line one, but the current version only implements the latter.