2

I need my screenshot function to be as fast as possible, and now every call to the function takes about 0.2sec.

This is the function:

def get_screenshot(self, width, height):
    image = self.screen_capture.grab(self.monitor)
    image = Image.frombuffer('RGB', image.size, image.bgra, 'raw', 'BGRX')
    image = image.resize((int(width), int(height)), Image.BICUBIC) # Resize to the size of 0.8 from original picture
    image = np.array(image)
    image = np.swapaxes(image, 0, 1)
    # This code below supposed to replace each black color ([0,0,0]) to the color of [0,0,1]
    # r1,g1,b1 = [0,0,0] and r2,g2,b2 = [0,0,1]
    red, green, blue = image[:, :, 0], image[:, :, 1], image[:, :, 2]
    mask = (red == r1) & (green == g1) & (blue == b1)
    image[:, :, :3][mask] = [r2, g2, b2]
    return image

Do you notice any changes that I can do to make the function faster?

Edit: Some details that I forgot to mention:

  1. My screen dimensions are 1920*1080

  2. This function is a part of a live stream project that I am currently working on. The solution that Carlo has suggested below is not appropriate in this case because the remote computer will not be synchronized with our computer screen.

Muhammad Dyas Yaskur
  • 6,914
  • 10
  • 48
  • 73
Adi Koch
  • 91
  • 9
  • You can move the `image = self.screen_capture.grab(self.monitor)` to another function that add the result to a global list of your program. After that create another function that elaborate the image later and remove the object from the list. You can do this with multi-threading. – Carlo Zanocco Mar 02 '19 at 17:01
  • OK tnx, i will try it out. Someone has another idea for speed up the function? – Adi Koch Mar 02 '19 at 17:05
  • I have answered to the question with my idea – Carlo Zanocco Mar 02 '19 at 17:12

2 Answers2

3

As your code is incomplete, I can only guess what might help, so here are a few thoughts...

I started with a 1200x1200 image, because I don't know how big yours is, and reduced it by a factor of 0.8x to 960x960 because of a comment in your code.

My ideas for speeding it up are based on either using a different interpolation method, or using OpenCV which is highly optimised SIMD code. Either, or both, may be appropriate, but as I don't know what your images look like, only you can say.

So, here we go, first with PIL resize() and different interpolation methods:

# Open image with PIL
i = Image.open('start.png').convert('RGB')

In [91]: %timeit s = i.resize((960,960), Image.BICUBIC)                                             
16.2 ms ± 28 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [92]: %timeit s = i.resize((960,960), Image.BILINEAR)                                            
10.9 ms ± 87.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [93]: %timeit s = i.resize((960,960), Image.NEAREST)                                             
440 µs ± 10.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

So, BILINEAR is 1.5x faster than BICUBIC and the real winner here is NEAREST at 32x faster.

Now, converting to a Numpy array (as you are doing anyway) and using the highly optimised OpenCV SIMD code to resize:

# Now make into Numpy array for OpenCV methods
n = np.array(i)

In [100]: %timeit s = cv2.resize(n, (960,960), interpolation = cv2.INTER_CUBIC)                     
806 µs ± 9.81 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

In [101]: %timeit s = cv2.resize(n, (960,960), interpolation = cv2.INTER_LINEAR)                    
3.69 ms ± 29 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [102]: %timeit s = cv2.resize(n, (960,960), interpolation = cv2.INTER_AREA)                      
12.3 ms ± 136 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [103]: %timeit s = cv2.resize(n, (960,960), interpolation = cv2.INTER_NEAREST)                   
692 µs ± 448 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)

And the winner here looks like INTER_CUBIC which is 20x faster than PIL's resize().

Please try them all and see what works for you! Just remove the Python magic %timeit at the start of the line and run what's left.

Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
  • Thanks for your answer. The Image.NEAREST interpolation method was the fastest of all the options but the quality of the image was bad. cv2.INTER_CUBIC had the same quality as Image.BICUBIC and it was 2x faster. So i picked INTER_CUBIC that was overall the best, in terms of quality and time. I have reduced the time of the function in about 0.02sec. In livestream every milisecond is important :) Do you see more changes that i can make? – Adi Koch Mar 02 '19 at 19:32
  • Cool. As your code is still incomplete, I have no idea where your `screen_capture.grab()` comes from so I don't know how to optimise it. It seems as though you are dragging an unnecessary alpha channel around which is 33% increase in useless data - maybe you can discard that sooner? – Mark Setchell Mar 02 '19 at 19:38
  • screen_capture is an MSS object. MSS is the fastest library the i have found to take a screenshot. I don't see when i can discard the alpha channel sooner. – Adi Koch Mar 02 '19 at 19:55
  • Sorry, I don't know MSS at all. – Mark Setchell Mar 02 '19 at 20:35
1

This is just an example of what I mean. If it solve the problem let me know.

You can create two different thread. One that take the screenshot, the other that elaborate the screen later. Both add the result to a list. This improve the speed of the get_screenshot function. But for elaborate it you need the time that is required for the function execution.

import threading
#import your stuff

class your_class(object):
        def __init__(self):

                self.images = list()
                self.elaborated_images = list()

                threading.Thread(name="Take_Screen", target=self.get_screenshot, args=(width, height))
                threading.Thread(name="Elaborate_Screen", target=self.elaborate_screenshot)

        def get_screenshot(self, width, height):
                while True:
                        images.append(self.screen_capture.grab(self.monitor))

        def elaborate_screenshot(self):
                while True:
                        image = self.images[0]
                        image = Image.frombuffer('RGB', image.size, image.bgra, 'raw', 'BGRX')
                        image = image.resize((int(width), int(height)), Image.BICUBIC) # Resize to the size of 0.8 from original picture
                        image = np.array(image)
                        image = np.swapaxes(image, 0, 1)

                        # This code below supposed to replace each black color ([0,0,0]) to the color of [0,0,1]
                        # r1,g1,b1 = [0,0,0] and r2,g2,b2 = [0,0,1]

                        red, green, blue = image[:, :, 0], image[:, :, 1], image[:, :, 2]
                        mask = (red == r1) & (green == g1) & (blue == b1)
                        image[:, :, :3][mask] = [r2, g2, b2]

                        del self.images[0]

                        self.elaborated_images.append(image)

your_class()

Because I don't have your full code I can't build it the best as possible.

Carlo Zanocco
  • 1,967
  • 4
  • 18
  • 31
  • Thank you for your code, but i think there is a problem to your solution. The code that i have added is a part of livestream project that I am doing, and if i use your solution, the remote computer will not be synchronised with the screen of our computer. In my original solution i get a screenshot, send it, get approve from the remote computer and then continues to the next screenshot. – Adi Koch Mar 02 '19 at 18:49
  • Nicely done, sir! Worthy of an upvote from everybody. – Mark Setchell Mar 02 '19 at 18:55
  • @AdiKoch Maybe add this detail to the question. Could be useful for other people – Carlo Zanocco Mar 02 '19 at 19:34