There is an application on pyqt5 that can convert an image to shades of gray, etc., but it does it very slowly.

I wonder if it can somehow speed it up?

import math from PyQt5.QtGui import QPixmap, QColor from PyQt5.QtCore import * from PyQt5.QtWidgets import QWidget, QDesktopWidget, QApplication, QLabel, QFileDialog, QPushButton, QSlider class Colors(QWidget): def __init__(self): super().__init__() self.img = '' self.label = QLabel(self.window()) self.label.resize(670, 500) self.label.move(25, 25) self.pixmap = QPixmap() self.init_ui() def init_ui(self): self.resize(1200, 900) self.center() self.setWindowTitle('Center') self.h_label = QLabel('H', self) self.h_label.move(30, 716) self.h_sld = QSlider(Qt.Horizontal, self) self.h_sld.setRange(0, 360) self.h_sld.setPageStep(1) self.h_sld.move(50, 700) self.h_sld.resize(500, 50) self.s_label = QLabel('S', self) self.s_label.move(30, 736) self.s_sld = QSlider(Qt.Horizontal, self) self.s_sld.setRange(0, 100) self.s_sld.setPageStep(1) self.s_sld.move(50, 720) self.s_sld.resize(500, 50) self.v_label = QLabel('V', self) self.v_label.move(30, 756) self.v_sld = QSlider(Qt.Horizontal, self) self.v_sld.setRange(0, 100) self.v_sld.setPageStep(1) self.v_sld.move(50, 740) self.v_sld.resize(500, 50) choose_img = QPushButton('Открыть', self) choose_img.move(700, 50) choose_img.clicked.connect(self.open_on_click) save_img = QPushButton('Сохранить', self) save_img.move(800, 50) save_img.clicked.connect(self.save_on_click) to_grey = QPushButton('RGB -> Grey', self) to_grey.move(700, 85) to_grey.clicked.connect(self.to_grey_on_click) to_red = QPushButton('RGB -> Red', self) to_red.move(800, 85) to_red.clicked.connect(self.to_red_on_click) to_green = QPushButton('RGB -> Green', self) to_green.move(700, 120) to_green.clicked.connect(self.to_green_on_click) to_blue = QPushButton('RGB -> Blue', self) to_blue.move(800, 120) to_blue.clicked.connect(self.to_blue_on_click) to_hsv = QPushButton('To HSV', self) to_hsv.move(700, 730) to_hsv.clicked.connect(self.to_hsv) def to_grey_on_click(self): img = self.pixmap.toImage() for x in range(img.width()): for y in range(img.height()): r = QColor(img.pixel(x, y)).red() g = QColor(img.pixel(x, y)).green() b = QColor(img.pixel(x, y)).blue() a = (0.2126 * r + 0.7152 * g + 0.0722 * b) img.setPixel(x, y, QColor(a, a, a).rgb()) self.label.setPixmap(QPixmap(img)) self.show() def to_red_on_click(self): img = self.pixmap.toImage() for x in range(img.width()): for y in range(img.height()): r = QColor(img.pixel(x, y)).red() img.setPixel(x, y, QColor(r, 0, 0).rgb()) self.label.setPixmap(QPixmap(img)) self.show() def to_green_on_click(self): img = self.pixmap.toImage() for x in range(img.width()): for y in range(img.height()): g = QColor(img.pixel(x, y)).green() img.setPixel(x, y, QColor(0, g, 0).rgb()) self.label.setPixmap(QPixmap(img)) self.show() def to_blue_on_click(self): img = self.pixmap.toImage() for x in range(img.width()): for y in range(img.height()): b = QColor(img.pixel(x, y)).blue() img.setPixel(x, y, QColor(0, 0, b).rgb()) self.label.setPixmap(QPixmap(img)) self.show() def open_file_name_dialog(self): options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog filename, _ = QFileDialog.getOpenFileName(self, "QFileDialog.getOpenFileName()", "", "Images (*.jpg *.jpeg *.png)", options=options) if filename: self.pixmap = QPixmap(filename).scaled(670, 500) def to_hsv(self): self.hsv() def hsv(self): img = self.pixmap.toImage() for x in range(img.width()): for y in range(img.height()): r = QColor(img.pixel(x, y)).red() g = QColor(img.pixel(x, y)).green() b = QColor(img.pixel(x, y)).blue() h, s, v = self.rgb2hsv(r, g, b) dh = self.h_sld.value() ds = self.s_sld.value() * 0.01 dv = self.v_sld.value() * 0.01 h1 = (h + dh) % 360 s1 = max(min(s + ds, 1), 0) v1 = max(min(v + dv, 1), 0) r1, g1, b1 = self.hsv2rgb(h1, s1, v1) img.setPixel(x, y, QColor(r1, g1, b1).rgb()) self.label.setPixmap(QPixmap(img)) self.show() def rgb2hsv(self, r, g, b): r, g, b = r / 255.0, g / 255.0, b / 255.0 mx = max(r, g, b) mn = min(r, g, b) df = mx - mn if mx == mn: h = 0 elif mx == r: h = (60 * ((g - b) / df) + 360) % 360 elif mx == g: h = (60 * ((b - r) / df) + 120) % 360 elif mx == b: h = (60 * ((r - g) / df) + 240) % 360 if mx == 0: s = 0 else: s = df / mx v = mx return h, s, v def hsv2rgb(self, h, s, v): h = float(h) s = float(s) v = float(v) h60 = h / 60.0 h60f = math.floor(h60) hi = int(h60f) % 6 f = h60 - h60f p = v * (1 - s) q = v * (1 - f * s) t = v * (1 - (1 - f) * s) r, g, b = 0, 0, 0 if hi == 0: r, g, b = v, t, p elif hi == 1: r, g, b = q, v, p elif hi == 2: r, g, b = p, v, t elif hi == 3: r, g, b = p, q, v elif hi == 4: r, g, b = t, p, v elif hi == 5: r, g, b = v, p, q r, g, b = int(r * 255), int(g * 255), int(b * 255) return r, g, b def open_on_click(self): self.open_file_name_dialog() self.label.setPixmap(self.pixmap) def save_on_click(self): self.save_file_dialog() def save_file_dialog(self): options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog filename, _ = QFileDialog.getSaveFileName(self, "QFileDialog.getSaveFileName()", "", "Images (*.jpg *.jpeg *.png)", options=options) if filename: self.label.pixmap().save(filename) def center(self): qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) if __name__ == '__main__': import sys app = QApplication(sys.argv) ex = Colors() ex .show() sys.exit(app.exec_()) 
  • Yes, looping over pixels in a script is very slow, unless it is javascript. How to speed up - write a C module that will process images, or use a PIL. - user239133
  • one
    PIL and numpy here will save you. - Igor Igoryanych September

1 answer 1

First, measure the execution time of methods with nested loops. Make sure that they are the bottleneck in your code (this is likely, but should be checked). At the same time, it will let you know how much better (if better) other solutions are.

For example, here is the slow implementation of the application, which shows the image specified in the command line and converts it to shades of gray by clicking the mouse, using nested loops from the code in question:

 #!/usr/bin/env python3 """Usage: to-grey <image>""" import sys from PyQt5.Qt import QApplication, QColor, QIcon, QPixmap, QSize, QToolButton def on_click(): img = w.icon().pixmap(qsize).toImage() # XXX slow, don't do it for x in range(img.width()): for y in range(img.height()): r = QColor(img.pixel(x, y)).red() g = QColor(img.pixel(x, y)).green() b = QColor(img.pixel(x, y)).blue() a = (0.2126 * r + 0.7152 * g + 0.0722 * b) img.setPixel(x, y, QColor(a, a, a).rgb()) w.setIcon(QIcon(QPixmap(img))) app = QApplication(sys.argv) if len(app.arguments()) != 2: sys.exit(__doc__) w = QToolButton() w.setIcon(QIcon(app.arguments()[1])) qsize = QSize(300, 300) w.setIconSize(qsize) w.clicked.connect(on_click) # center w.adjustSize() # update w.rect() now w.move(app.desktop().screen().rect().center() - w.rect().center()) w.show() sys.exit(app.exec_()) 

Example:

 $ python3 to-gray.py ~/Pictures/example.jpg 

To find out which calls take the most time, you can run the script with the profiler:

 $ python -mprofile --sort cumtime to-gray.py ~/Pictures/example.jpg 

There are no surprises, on on_click() takes a considerable time. If you wish, you can measure the performance line by line:

 $ pip install pprofile $ pprofile --exclude-syspath to-gray.py ~/Pictures/example.jpg 

To improve performance, you can replace nested loops with vector operations on the numpy array.

You can create a numpy array using a buffer from a QImage, then changes to the numpy array will automatically be reflected in the QImage:

 #!/usr/bin/env python3 """Usage: to-grey <image>""" import sys import numpy as np # $ pip install numpy from PyQt5.Qt import QApplication, QIcon, QImage, QPixmap, QSize, QToolButton def on_click(): # r = arr[..., 0] # g = arr[..., 1] # b = arr[..., 2] # a = (0.2126 * r + 0.7152 * g + 0.0722 * b) a = arr @ [0.2126, 0.7152, 0.0722] # np.dot # arr[..., 0] = a # arr[..., 1] = a # arr[..., 2] = a arr[...] = a[..., np.newaxis] w.setIcon(QIcon(QPixmap(qimage))) app = QApplication(sys.argv) if len(app.arguments()) != 2: sys.exit(__doc__) w = QToolButton() qimage = QImage(app.arguments()[1]).convertToFormat(QImage.Format_RGB888) w.setIcon(QIcon(QPixmap(qimage))) width, height = qimage.width(), qimage.height() w.setIconSize(QSize(width, height)) ptr = qimage.bits() ptr.setsize(qimage.byteCount()) arr = np.ndarray((height, width, 3), dtype=np.uint8, buffer=ptr) # share data w.clicked.connect(on_click) # center w.adjustSize() # update w.rect() now w.move(app.desktop().screen().rect().center() - w.rect().center()) w.show() sys.exit(app.exec_()) 

QPixmap uses its internal buffer, so after changing the array, re-QPixmap is created in on_click() . This requires an order of magnitude less time than for example the np.dot operation in on_click() .

It is convenient to check the correctness of converting images and their performance separately from the GUI code:

 def rgb2gray_rgb(arr): a = arr @ [0.2126, 0.7152, 0.0722] arr[...] = a[..., np.newaxis] 

See if existing packages, such as scikit-image, offer this feature.

For cycles that are difficult to turn into vector operations, you can use cython, an example .