What I want to do is to loop over an image pixel by pixel using each pixel value to draw a circle in another corresponding image. My approach is as follows:
it = np.nditer(pixels, flags=['multi_index']) while not it.finished: y, x = it.multi_index color = it[0] it.iternext() center = (x*20 + 10, y*20 + 10) # corresponding circle center cv2.circle(circles, center, int(8 * color/255), 255, -1)
Looping this way is somewhat slow. I tried adding the @njit decorator of numba, but apparently it has problems with opencv.
Input images are 32×32 pixels They map to output images that are 32×32 circles each circle is drawn inside a 20×20 pixels square That is, the output image is 640×640 pixels
A single image takes around 100ms to be transformed to circles, and I was hoping to lower that to 30ms or lower
Any recommendations?
Advertisement
Answer
When:
Dealing with drawings
The number of possible options does not exceed a common sense value (in this case: 256)
Speed is important (I guess that’s always the case)
There’s no other restriction preventing this approach
the best way would be to “cache” the drawings (draw them upfront (or on demand depending on the needed overhead) in another array), and when the drawing should normally take place, simply take the appropriate drawing from the cache and place it in the target area (as @ChristophRackwitz stated in one of the comments), which is a very fast NumPy operation (compared to drawing).
As a side note, this is a generic method not necessarily limited to drawings.
But the results you claim you’re getting: ~100 ms per one 32×32 image (to a 640×640 circles one), didn’t make any sense to me (as OpenCV is also fast, and 1024 circles shouldn’t be such a big deal), so I created a program to convince myself.
code00.py:
#!/usr/bin/env python import itertools as its import sys import time import cv2 import numpy as np def draw_img_orig(arr_in, arr_out): factor = round(arr_out.shape[0] / arr_in.shape[0]) factor_2 = factor // 2 it = np.nditer(arr_in, flags=["multi_index"]) while not it.finished: y, x = it.multi_index color = it[0] it.iternext() center = (x * factor + factor_2, y * factor + factor_2) # corresponding circle center cv2.circle(arr_out, center, int(8 * color / 255), 255, -1) def draw_img_regular_iter(arr_in, arr_out): factor = round(arr_out.shape[0] / arr_in.shape[0]) factor_2 = factor // 2 for row_idx, row in enumerate(arr_in): for col_idx, col in enumerate(row): cv2.circle(arr_out, (col_idx * factor + factor_2, row_idx * factor + factor_2), int(8 * col / 255), 255, -1) def draw_img_cache(arr_in, arr_out, cache): factor = round(arr_out.shape[0] / arr_in.shape[0]) it = np.nditer(arr_in, flags=["multi_index"]) while not it.finished: y, x = it.multi_index yf = y * factor xf = x *factor arr_out[yf: yf + factor, xf: xf + factor] = cache[it[0]] it.iternext() def generate_input_images(shape, count, dtype=np.uint8): return np.random.randint(256, size=(count,) + shape, dtype=dtype) def generate_circles(shape, dtype=np.uint8, count=256, rad_func=lambda arg: int(8 * arg / 255), color=255): ret = np.zeros((count,) + shape, dtype=dtype) cy = shape[0] // 2 cx = shape[1] // 2 for idx, arr in enumerate(ret): cv2.circle(arr, (cx, cy), rad_func(idx), color, -1) return ret def test_draw(imgs_in, img_out, count, draw_func, *draw_func_args): print("nTesting {:s}".format(draw_func.__name__)) start = time.time() for i, e in enumerate(its.cycle(range(imgs_in.shape[0]))): draw_func(imgs_in[e], img_out, *draw_func_args) if i >= count: break print("Took {:.3f} seconds ({:d} images)".format(time.time() - start, count)) def test_speed(shape_in, shape_out, dtype=np.uint8): imgs_in = generate_input_images(shape_in, 50, dtype=dtype) #print(imgs_in.shape, imgs_in) img_out = np.zeros(shape_out, dtype=dtype) circles = generate_circles((shape_out[0] // shape_in[0], shape_out[1] // shape_in[1])) count = 250 funcs_data = ( (draw_img_orig,), (draw_img_regular_iter,), (draw_img_cache, circles), ) for func_data in funcs_data: test_draw(imgs_in, img_out, count, func_data[0], *func_data[1:]) def test_accuracy(shape_in, shape_out, dtype=np.uint8): img_in = np.arange(np.product(shape_in), dtype=dtype).reshape(shape_in) circles = generate_circles((shape_out[0] // shape_in[0], shape_out[1] // shape_in[1])) funcs_data = ( (draw_img_orig, "orig.png"), (draw_img_regular_iter, "regit.png"), (draw_img_cache, "cache.png", circles), ) imgs_out = [np.zeros(shape_out, dtype=dtype) for _ in funcs_data] for idx, func_data in enumerate(funcs_data): func_data[0](img_in, imgs_out[idx], *func_data[2:]) cv2.imwrite(func_data[1], imgs_out[idx]) for idx, img in enumerate(imgs_out[1:], start=1): if not np.array_equal(img, imgs_out[0]): print("Image index different: {:d}".format(idx)) def main(*argv): dt = np.uint8 shape_in = (32, 32) factor_io = 20 shape_out = tuple(i * factor_io for i in shape_in) test_speed(shape_in, shape_out, dtype=dt) test_accuracy(shape_in, shape_out, dtype=dt) if __name__ == "__main__": print("Python {:s} {:03d}bit on {:s}n".format(" ".join(elem.strip() for elem in sys.version.split("n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform)) rc = main(*sys.argv[1:]) print("nDone.n") sys.exit(rc)
Notes:
Besides your implementation that uses np.nditer (which I placed in a function called draw_img_orig), I created 2 more:
One that iterates the input array Pythonicly (draw_img_regular_iter)
One that uses cached circles, and also iterates via np.nditer (draw_img_cache)
In terms of tests, there are 2 of them – each being performed on every of the 3 (above) approaches:
Speed: measure the time took to process a number of images
Accuracy: measure the output for a 32×32 input containing the interval [0, 255] (4 times)
Output:
[cfati@CFATI-5510-0:e:WorkDevStackOverflowq071818080]> sopr.bat ### Set shorter prompt to better fit when pasted in StackOverflow (or other) pages ### [prompt]> dir /b code00.py [prompt]> "e:WorkDevVEnvspy_pc064_03.09_test0Scriptspython.exe" code00.py Python 3.9.9 (tags/v3.9.9:ccb0e6a, Nov 15 2021, 18:08:50) [MSC v.1929 64 bit (AMD64)] 064bit on win32 Testing draw_img_orig Took 0.908 seconds (250 images) Testing draw_img_regular_iter Took 1.061 seconds (250 images) Testing draw_img_cache Took 0.426 seconds (250 images) Done. [prompt]> [prompt]> dir /b cache.png code00.py orig.png regit.png
Above there are the speed test results: as seen, your approach took a bit less than a second for 250 images!!! So I was right, I don’t know where your slowness comes from, but it’s not from here (maybe you got the measurements wrong?).
The regular method is a bit slower, while the cached one is ~2X faster.
I ran the code on my laptop:
- Win 10 pc064
- CPU: Intel i7 6820HQ @ 2.70GHz (fairly old)
- GPU: not relevant, as I didn’t notice any spikes during execution
Regarding the accuracy test, all (3) output arrays are identical (there’s no message saying otherwise), here’s one saved image: