CLImage icon indicating copy to clipboard operation
CLImage copied to clipboard

Image Conversion being too slow

Open bugbrekr opened this issue 4 years ago • 4 comments

Hey, I really love this tool. Its able to accurately convert images to terminal codes. But I have one issue - the conversion process is too slow....

Here is an example: ORIGINAL - 500x500_random.png image

Command: climage.convert("500x500_random.png", is_unicode=True, width=100)

OUTPUT - image

And this conversion took about 0.51 seconds.

My original idea was to try to make a real-time video-transmission tool using only the terminal. But the output video cannot be like 2 FPS....

So is there any way to optimize the conversion process without losing a lot of quality?

bugbrekr avatar Aug 28 '21 16:08 bugbrekr

Thanks for the feedback!

I took a look at this, and it looks like most expensive part occurs for non-truecolor colour schemes.

TL;DR: Use is_truecolor=True, is_256colour=False, if your terminal supports it.

Some cProfile stats, sorted by the total time spent in each line, for the input:

convert('image.png', is_unicode=True, width=100)
         2722518 function calls (2539177 primitive calls) in 0.762 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
181030/4540    0.210    0.000    0.627    0.000 kdtree.py:431(_search_node)
   362670    0.120    0.000    0.199    0.000 kdtree.py:382(axis_dist)
   120890    0.061    0.000    0.260    0.000 kdtree.py:396(<listcomp>)
   120890    0.058    0.000    0.336    0.000 kdtree.py:390(dist)
   540962    0.049    0.000    0.049    0.000 climage.py:15(__getitem__)
   362670    0.046    0.000    0.046    0.000 {built-in method math.pow}
   120890    0.027    0.000    0.363    0.000 kdtree.py:418(<lambda>)
   120890    0.019    0.000    0.019    0.000 {built-in method builtins.sum}
   181030    0.017    0.000    0.017    0.000 kdtree.py:172(__nonzero__)
201152/198397    0.011    0.000    0.012    0.000 {built-in method builtins.len}

Now, it's much different for this:

convert('image.png', is_unicode=True, width=100, is_truecolor=True, is_256color=False)

Note, yeah, I notice that it's kinda annoying to enable truecolour, using default values gets in the way here.

Log:

         235341 function calls (230805 primitive calls) in 0.118 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   482/80    0.007    0.000    0.019    0.000 sre_parse.py:493(_parse)
       93    0.006    0.000    0.006    0.000 {built-in method marshal.loads}
        1    0.004    0.004    0.020    0.020 climage.py:123(_toAnsi)
     5272    0.004    0.000    0.004    0.000 {method 'format' of 'str' objects}
    10000    0.004    0.000    0.009    0.000 Image.py:1423(getpixel)
  373/368    0.003    0.000    0.023    0.000 {built-in method builtins.__build_class__}
    13452    0.003    0.000    0.003    0.000 sre_parse.py:233(__next)
    10003    0.003    0.000    0.004    0.000 Image.py:814(load)
   829/76    0.003    0.000    0.007    0.000 sre_compile.py:71(_compile)
        3    0.002    0.001    0.002    0.001 {method 'decode' of 'ImagingDecoder' objects}

I benchmarked a bunch of different invocations, too:

duration = timeit.timeit(lambda: convert('image.png', is_unicode=True, width=2, is_truecolor=True, is_256color=False), number=1000)
print(f"Width=2 (unicode, truecolor) took {duration}")
duration = timeit.timeit(lambda: convert('image.png', is_unicode=True, width=100, is_truecolor=True, is_256color=False), number=1000)
print(f"Width=100 (unicode, truecolor) took {duration}")
duration = timeit.timeit(lambda: convert('image.png', is_unicode=True, width=2, is_truecolor=False, is_256color=True), number=1000)
print(f"Width=2 (unicode, 256color) took {duration}")
duration = timeit.timeit(lambda: convert('image.png', is_unicode=True, width=100, is_truecolor=False, is_256color=True), number=1000)
print(f"Width=100 (unicode, 256color) took {duration}")
duration = timeit.timeit(lambda: convert('image.png', is_unicode=False, width=2, is_truecolor=True, is_256color=False), number=1000)
print(f"Width=2 (truecolor) took {duration}")
duration = timeit.timeit(lambda: convert('image.png', is_unicode=False, width=100, is_truecolor=True, is_256color=False), number=1000)
print(f"Width=100 (truecolor) took {duration}")
duration = timeit.timeit(lambda: convert('image.png', is_unicode=False, width=2, is_truecolor=False, is_256color=True), number=1000)
print(f"Width=2 (256color) took {duration}")
duration = timeit.timeit(lambda: convert('image.png', is_unicode=False, width=100, is_truecolor=False, is_256color=True), number=1000)
print(f"Width=100 (256color) took {duration}")

Prints (this is the time each function call takes, in milliseconds):

Width=2 (unicode, truecolor) took 3.259595004012226
Width=100 (unicode, truecolor) took 13.441674665999017
Width=2 (unicode, 256color) took 3.350438164998195
Width=100 (unicode, 256color) took 314.9447129469918
Width=2 (truecolor) took 3.495372111996403
Width=100 (truecolor) took 6.761303802995826
Width=2 (256color) took 3.4730656869942322
Width=100 (256color) took 140.41951910499483

Which is confirming that the part that's mapping colours from the image to terminal colours is a bit slow. I might try and re-implement it in C++, or investigate different data-structures, because Python's just kinda slow at it. :/

pnappa avatar Sep 12 '21 08:09 pnappa

Thanks, adding is_truecolor=True, is_256color=False did increase the performance :-) I see that the colour mapping functions take the most time for computing. I hope re-implementing this in C++ optimizes and improves it.

bugbrekr avatar Sep 12 '21 11:09 bugbrekr

Haha, don't worry about closing the issue, I'm still looking to fix the performance issues for 256 bit colour, just haven't had the time quite yet :)

Thanks again for pointing it out!

pnappa avatar Dec 04 '21 23:12 pnappa

Okay, sorry. That's cool then.

Thanks :+1:

bugbrekr avatar Dec 05 '21 04:12 bugbrekr