Imgdiff: Faster than the fastest pixel-by-pixel image difference tool
github.com
github.com
This problem is perfect to exploit capabilities of modern CPUs, and neither of the projects does so. At least this one iterates over the y coordinates in the outer loop.
Nothing about what either tool does is noteworthy.
Here's some Python code (using OpenCV & numpy) that is roughly equivalent to OP's imgdiff. It is single-threaded but it'll be vectorised and it's 3x faster on my 5-year-old 2-core laptop (even counting the Python overhead):
import sys, cv2, numpy
f1 = cv2.imread(sys.argv[1])
f1 = cv2.cvtColor(f1, cv2.COLOR_BGR2GRAY)
f2 = cv2.imread(sys.argv[2])
f2 = cv2.cvtColor(f2, cv2.COLOR_BGR2GRAY)
threshold = 0.1
absdiff = cv2.absdiff(f1, f2)
_, thresholded = cv2.threshold(
absdiff, int(threshold * 255),
255, cv2.THRESH_BINARY)
cv2.imwrite("output.png", thresholded)
print("Different pixels: %s" % numpy.count_nonzero(thresholded))
Benchmark: $ /usr/bin/time python test.py water-4k.png water-4k-2.png
Different pixels: 89153
1.39user 0.34system 0:01.33elapsed 129%CPU
$ /usr/bin/time ./imgdiff water-4k.png water-4k-2.png output.png
Failure! Images are different.
Different pixels: 89142
Command exited with non-zero status 1
8.85user 0.10system 0:04.02elapsed 222%CPU
(Edit: The Python code is a simplified version extracted from https://github.com/stb-tester/stb-tester -- we use it for regression testing of GUI screenshots.)This C code isn't doing exactly the same thing; here it's calculating a global similarity measure by calculating sum-of-squared-differences and comparing the end sum against the specified threshold, instead of comparing each pixel-wise diff against the threshold.
It is straightforward C code but it's vectorised by the compiler: https://godbolt.org/z/-3uO1z
Excluding the overhead of reading & decoding the PNGs, it takes 80ms for the same 8400x4725 images as my parent comment.
Benchmark:
$ git clone git@github.com:stb-tester/stb-tester.git
$ cd stb-tester
$ make
$ ipython
>>> import _stbt.sqdiff, cv2, timeit
>>> f1 = cv2.imread("water-4k.png")
>>> f2 = cv2.imread("water-4k-2.png")
>>> timeit.timeit(lambda: _stbt.sqdiff._sqdiff_c(f1, f2), number=1)
0.081
[1] The Python code in my parent comment will allocate memory (the size of the entire image) for each intermediate calculation like the colourspace conversion, the absolute differences, etc.In my experience, the only drawback of the Python API is the fact that it cannot utilize the multithreading module (probably does not release the GIL for long calls).
Good point about OpenCV's in-place operations. Sometimes it's tricky/impossible to do that in numpy if you need to implement something that OpenCV doesn't provide. For example the C code that I linked in my previous comment, we wrote as an optimization of OpenCV's `matchTemplate` when the inputs meet a specific condition (that both input images are the same size). In C we do the multiplications as we iterate over the images and we maintain a rolling sum in a single variable. In numpy you can't really do this, you have to multiply the whole array and then sum the result.
For 720p images our C implementation[1] was 100x faster than our numpy implementation[2], and 10x faster than numba[3].
[1]: https://github.com/stb-tester/stb-tester/blob/v32/_stbt/sqdi...
[2]: https://github.com/stb-tester/stb-tester/pull/566/files#diff...
[3]: https://github.com/stb-tester/stb-tester/pull/566/files#diff...
> I believe many (most?) opencv & numpy operations release the GIL.
Any idea how I can determine this? I am prototyping a real time machine vision application targeting 2x720p@240fps and I want to avoid writing any C++ for as long as possible.
(see https://docs.python.org/3/c-api/init.html#releasing-the-gil-... in the Python C API manual).
That's only called from the ERRWRAP2 macro here: https://github.com/opencv/opencv/blob/4.5.0/modules/python/s...
That macro, in turn, is called from gen_template_func_body in gen2.py: https://github.com/opencv/opencv/blob/4.5.0/modules/python/s...
And that seems to be a code generator that is generating the bindings for all the OpenCV functions: https://github.com/opencv/opencv/blob/4.5.0/modules/python/s...
As to testing this, on Linux I'd run `atop` to see if your program is using all available CPUs.
Doesn't seem like there's anything special here? It's roughly what any novice would build if asked to diff two images in Go?
Which, sure, gets the job done faster than the single-thread `odiff` on a single image, but is quite irrelevant for a tool marketed for tests/CI where many images are likely to be compared in parallel already (and maybe a single core is even available).
Yep, uses as many goroutines as the number of CPUs, that's about it. Saying it's 3x faster or whatever in "benchmarks" without even saying what CPU is used in the benchmarks is just sketchy.
$ time ./imgdiff cypress.png cypress.png
Success! Images are equal.
3.22user 0.10system 0:00.50elapsed 661%CPU
(0avgtext+0avgdata 478524maxresident)k
0inputs+0outputs (0major+48339minor)pagefaults 0swaps
$ time gm compare -highlight-style assign -highlight-color purple -file diff.png cypress.png cypress.png
1.99user 0.21system 0:02.21elapsed 99%CPU
(0avgtext+0avgdata 874948maxresident)k
0inputs+5240outputs (0major+217173minor)pagefaults 0swaps
$ time gm compare -metric mse cypress.png cypress.png
Image Difference (MeanSquaredError):
Normalized Absolute
============ ==========
Red: 0.0000000000 0.0
Green: 0.0000000000 0.0
Blue: 0.0000000000 0.0
Opacity: 0.0000000000 0.0
Total: 0.0000000000 0.0
0.94user 0.14system 0:00.67elapsed 161%CPU
(0avgtext+0avgdata 585832maxresident)k
0inputs+0outputs (0major+144881minor)pagefaults 0swapsTurns out it seems to be something people care about.
I should just start writing the CLI tools I think of, without dismissing their utility.
There's also bound to be some vector compare trick as well which falls back to pixel-by-pixel only for differences.
I think there are quite a few simple tricks you could apply here to get a bit more performance... Starting with sacrificing some memory and decoding a chunk of image at a time rather than calling .At(x,y) every time. And maybe improving local cache by dividing the image in `cpu` parts rather than having the work interleaved.
I would guess that the time of getting the data out to the GPU and back, is about the same as just calculating the difference on the CPU.
So, the quickest solution is probably to use the CPUs vector operations, and read ahead into the registers so you can exhaust the memory bandwidth fully.
The actual file transfer is comparable to the GPU doing a memcpy. (You could also use pinned host memory if the GPU program is just doing a one-off read.)
GPU processing for simple operations really shines if your data is already on the GPU and your code is hot. Otherwise, it's more useful if the process is significantly complex enough to make the overhead of a cold startup insignificant.
https://github.com/n7olkachev/imgdiff/blob/master/pkg/imgdif...
https://github.com/n7olkachev/imgdiff/blob/master/pkg/yiq/de...
https://github.com/python-needle/needle
Depending on your needs, detecting duplicates can also be useful – especially if you’re using some kind of CV tool which quickly assesses similarity and you want a second check to confirm that the highly similarity matches aren’t hitting some edge case failure mode.
One challenge is that you sometimes want to allow minor differences - think things like text aliasing or slight differences in the pixel values produced by different compression/color handling/etc. implementations - so there are some neat tools like pdiff which try to emphasize differences which the human visual system is sensitive to:
I've put a few simple examples in Node.js here: https://github.com/umaar/learn-browser-testing/tree/master/3... (just as learning material)
It was achieved by looping over every RGB pixel from the PNG rendered image of each URL.
https://github.com/sammcj/urldiff
Not exactly something I'm proud of, but the pixel loop solution did make me laugh at the time.
I am really used to just being able to do go build from the root directory of the repository.
Decoding the PNGs should be most of the work if written properly.
Edit: Yes, this is more reasonable: https://news.ycombinator.com/item?id=25405595
Basically diffing took too long ( I was diffing to remove duplicate frames from a virtualized browser to browser screencast ), so after trying to various options (hashing, diffs, etc) I just went with a simple check of a basically random but fixed set of pixels, which worked really well. I don't have data on the actual false positives/ negatives but for the thing important in my case, perceptual difference and not sending the same frame twice (to save bandwidth and speed) it worked great, and was "constant time".
The "code" is here: https://github.com/c9fe/ViewFinder/blob/84620bd87abb32b2f2c3...
And a demo of the project is: https://demo.browsergap.dosyago.com (if you check it on Safari mobile you need to plant your clicks about half a finger under where you think they should go, some bug!)
I'd like to know some idea of how this posted fast diff works. I checked around the repo and couldn't figure it out, but it seems to be splitting across multiple cores, but still looping over each pixel (but divided by the number of cores). I thought it might be doing something like checking 64-bit blocks somehow, but seems not.
Edit on the above: it should say "fixed set of bytes" not "fixed set of pixels"
What makes this fast?
https://github.com/n7olkachev/imgdiff/blob/master/pkg/imgdif...