Python is becoming more popular with the popularity of AI (Deep Learning). I have been making a GUI application that displays, draws, and saves images in Python, but I tried to organize the code and so on. I will write the mechanism to organize my understanding. I used a library called Qt for Python (PySide2). Originally developed in C ++, Qt allows you to develop cross-platform applications that run on various operating systems such as Windows, Mac and Linux from the same source code. There are Qt for Python or PyQt to use Qt from Python, but I used Qt for Python, which is not so bound by the license of the created application. When creating a GUI application, it is easy to understand if you understand object orientation. When drawing an image, many objects appear, and it is difficult for beginners to understand the roles and relationships of each object, but it will be easier to understand if something that moves even if it is not in a beautiful shape is created. (Experience story) So, if you have an image you want to make, don't give up and try various trials and errors. I hope this article will help you at that time.
The overall picture of the app is as shown below. The screen configuration is managed by the one that has a function called Layout that automatically arranges the placed parts (Widgets) according to the size of the window.
QVBoxLayout () aligns vertically and QHBoxLayout () aligns horizontally. Use QFormLayout () when you want to make a pair like a name and its value. The usage example is as follows.
self.main_layout = QVBoxLayout()
#Set the image display area
self.graphics_view = QGraphicsView()
self.upper_layout.addWidget(self.graphics_view)
#Nest the layout at the top of the screen into the main layout
self.upper_layout = QHBoxLayout()
self.main_layout.addLayout(self.upper_layout)
Create a class MainWindow (QMainWIndow) that inherits QMainWindow as the main window. Place parts (Widgets) with various functions in this, and describe the operation when you press it. Here, in the initialization, Layout could not be set unless it was declared as self.mainWidget = QWidget (self).
The code for launching the application looks like this:
class MainWindow(QMainWindow):
def __init__(self):
#Below, various processes are described
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Declare it in the constructor (def init ()) to create a menu bar (selection at the top of the application) in the main window. To create a menu bar and create a menu called "File" in it, do the following.
class MainWindow(QMainWindow):
def __init__(self):
self.main_menu = self.menuBar()
self.file_menu = self.main_menu.addMenu('File')
If you want to create an item called "File Open" in the "File" menu, do as follows.
# Set "Original Image Open" menu
self.org_img_open_button = QAction(self.style().standardIcon(getattr(QStyle, 'SP_FileDialogStart')), 'Open Orginal Image', self)
self.org_img_open_button.setShortcut('Ctrl+O')
self.org_img_open_button.triggered.connect(self.open_org_img_dialog)
self.file_menu.addAction(self.org_img_open_button)
In the first line, it is set as QAction () that works when this item is selected, the icon is selected from the standard one, and the displayed name is'Open Orginal Image'. The second line is set so that this item can be selected with the Ctrl + O shortcut key. The third line is connected to the function that sets the behavior when this item is selected. This is explained in the next section. This item is registered in file_menu on the 4th line.
In Qt, user operations and corresponding computer reactions are performed by methods called Signal and Slot. Signal is issued when the user performs an operation such as a menu button or dragging the mouse. Then, perform the corresponding processing in the Slot defined to receive each signal. Specifically, the operation when an item on the menu bar is selected is written as follows.
#menu bar->File-> 'Original Image Open'When is selected
#Send a triggered signal. The signal is open_org_img_Connected to the dialog function.
self.org_img_open_button = QAction(self.style().standardIcon(getattr(QStyle, 'SP_FileDialogStart')), 'Open Orginal Image', self)
self.org_img_open_button.triggered.connect(self.open_org_img_dialog)
#menu bar->File-> 'Original Image Open'A function that becomes a Slot that receives the signal emitted when is selected
def open_org_img_dialog(self):
options = QFileDialog.Options()
org_img_default_path = self.app_setting["SoftwareSetting"]["file_path"]["org_img_dir"]
self.org_img_file_path, selected_filter = QFileDialog.getOpenFileName(self, 'Select original image', org_img_default_path, 'Image files(*.jpg *jpeg *.png)', options=options)
org_img_dir_path, org_img_file = os.path.split(self.org_img_file_path)
org_img_bare_name, org_img_ext = os.path.splitext(org_img_file)
self.org_img_path_label.setText(self.org_img_file_path)
When a color is selected with a mouse click from the color bar described later, the selected color is drawn as a rectangle in the drawing area for display to make it easier for the user to understand. At this time, it becomes necessary to act between the color bar and the object in the selected color drawing area. At that time, it is necessary to connect Signal and Slot through the parent main window. In that case, the class that manages the drawing items to be placed in the selected color drawing area defines its own Signal and emits the Signal with the color information of the clicked part. Specifically, the code looks like the one below.
# Class for graphics contents of tools on main window
class GraphicsSceneForTools(QGraphicsScene):
# Define custom signal
img_info = Signal(QColor)
def mousePressEvent(self, event):
# For check program action
pos = event.scenePos()
x = pos.x()
y = pos.y()
#If the cursor or pen is selected on the image editing toolbar, the color information of the clicked location(QColor)Issue a Signal with
if self.mode == 'cursor' or self.mode == 'pen':
self.pix_rgb = self.img_content.pixelColor(x, y)
self.img_info.emit(self.pix_rgb)
#Define the Slot corresponding to the Signal of the color information selected on the main window side.
class MainWindow(QMainWindow):
#Drawing item management object for color bars
self.color_bar_scene = GraphicsSceneForTools()
#Connect to the function that becomes the Slot that receives the Signal that sent the color information
self.color_bar_scene.img_info.connect(self.set_selected_color)
Several widgets are linked for image display. The relationship is as shown in the figure below. Prepare QGraphicsView, which is a drawing area object, in MainWindow, place QGraphicsScene that holds and manage drawing objects in it, and add drawings and images such as lines and circles to QGraphicsScene. To go.
The QGraphicsScene set in the main drawing area displays the pixel information of the displayed image in the status bar when the cursor tool is selected, and the layer above the image when the pen or eraser tool is selected. I will try to draw. In order to add such a function set by yourself, create a Graphics Scene that inherits QGraphic Scene as follows. By setting the parent drawing area QGraphicsView and its parent MainWindow in the initialization init function, the information obtained from each item of this GraphicsScene can be passed to the drawing area or window.
To be honest, at first I'm not sure about QGraphicsView and QGraphicsScene, but I thought it was complicated and troublesome to access and control the content! It seems that this is because the design is designed to meet the complicated demand of drawing within the visible range (drawable range) from different viewpoints even if the target content to be drawn does not change. For example, when the content to be drawn is larger than the drawing area, it may be displayed while changing the viewpoint with the scroll bar, or the 3D object may be displayed while changing the viewpoint.
class GraphicsSceneForMainView(QGraphicsScene):
def __init__(self, parent=None, window=None, mode='cursor'):
QGraphicsScene.__init__(self, parent)
# Set parent view area
self.parent = parent
# Set grand parent window
self.window = window
# Set action mode
self.mode = mode
# mouse move pixels
self.points = []
# added line items
self.line_items = []
self.lines = []
# added line's pen attribute
self.pens = []
def set_mode(self, mode):
self.mode = mode
def set_img_contents(self, img_contents):
# image data of Graphics Scene's contents
self.img_contents = img_contents
def clear_contents(self):
self.points.clear()
self.line_items.clear()
self.lines.clear()
self.pens.clear()
self.img_contents = None
def mousePressEvent(self, event):
# For check program action
pos = event.scenePos()
x = pos.x()
y = pos.y()
if self.mode == 'cursor':
# Get items on cursor
message = '(x, y)=({x}, {y}) '.format(x=int(x), y=int(y))
for img in self.img_contents:
# Get pixel value
pix_val = img.pixel(x, y)
pix_rgb = QColor(pix_val).getRgb()
message += '(R, G, B) = {RGB} '.format(RGB=pix_rgb[:3])
# show scene status on parent's widgets status bar
self.window.statusBar().showMessage(message)
Link to QGraphicsView documentation Link to QGraphicsScene documentation
To place an image in QGraphicsScene, format it as QPixmap and use QGraphicsScene.addItem (QPixmap). However, in the QPixmap format, the information of each pixel cannot be acquired or rewritten, so keep it in the QImage format and convert it to QPixmap for drawing. To create a QImage from an image file, turn it into a QPixmap, and add it to the QGraphicsScene, the code looks like this:
#self refers to MainWindow
self.scene = GraphicsSceneForMainView(self.graphics_view, self)
self.org_qimg = QImage(self.org_img_file_path)
self.org_pixmap = QPixmap.fromImage(self.org_qimg)
scene.addItem(self.org_pixmap)
To create a QImage of 8 bits (256 gradations) RGBA (A is transparency) in the sky, use the following code.
self.layer_qimg = QImage(self.org_img_width, self.org_img_height, QImage.Format_RGBA8888)
Link to QImage documentation Link to QPixmap documentation
With reference to this article, the color bar for selecting the pen color is heat map-like (smooth from low-temperature blue to high-temperature red). I made it with (change). The object that sets the color bar is partly described in the explanation of "Mechanism for receiving actions on parts", but I created a class called GraphicsSceneForTools that inherited QGraphicsScene and used it. By doing so, by clicking the mouse on it, Signal will be issued according to the position pressed from that object, and in the MainWindow (to be exact, MainWindow-> QGraphicsView-> GraphicsSceneForTools) of the parent object where the object is placed. By preparing a Slot function to be received by Signal, the selected color display area is filled with the color selected by the user from the color bar and displayed in an easy-to-understand manner. In the newly prepared class GraphicsSceneForTools (QGraphicsScene), a Signal with QColor (color information) is prepared as img_info = Signal (QColor), and it is set when the mouse is clicked in def mousePressEvent (self, event). I am trying to output a Signal signal with the color (self.pix_rgb) of the clicked coordinate position of the drawing item (color bar in this case) as self.img_info.emit (self.pix_rgb). On the MainWindow side, set_selected_color () is prepared as the Slot function on the receiving side when the GraphicsSceneForTools object issues the corresponding Signal as self.color_bar_scene.img_info.connect (self.set_selected_color). Specifically, the code is as follows.
class MainWindow(QMainWindow):
# Set color bar
self.color_bar_width = 64
self.color_bar_height = 256
self.color_bar_view = QGraphicsView()
self.color_bar_view.setFixedSize(self.color_bar_width+3, self.color_bar_height+3)
self.color_bar_scene = GraphicsSceneForTools()
#Set the color bar.
#The color change data that is the basis of the color bar is self.colormap_It is in data.
#Please refer to the source code or reference article for how to create it.
self.color_bar_img = QImage(self.color_bar_width, self.color_bar_height, QImage.Format_RGB888)
for i in range(self.color_bar_height):
# Set drawing pen for colormap
ii = round(i * (1000/256))
color = QColor(self.colormap_data[ii][0], self.colormap_data[ii][1], self.colormap_data[ii][2])
pen = QPen(color, 1, Qt.SolidLine, \
Qt.SquareCap, Qt.RoundJoin)
self.color_bar_scene.addLine(0, self.color_bar_height - i-1, self.color_bar_width, self.color_bar_height - i-1, pen=pen)
for j in range(self.color_bar_width):
self.color_bar_img.setPixelColor(j, self.color_bar_height-i-1, color)
self.color_bar_scene.set_img_content(self.color_bar_img)
self.color_bar_view.setScene(self.color_bar_scene)
# Connect signal to slot of color_bar_scene
self.color_bar_scene.img_info.connect(self.set_selected_color)
# Slot of color bar clicked for selection color
def set_selected_color(self, color):
# Delete existng image item
self.select_color_scene.removeItem(self.select_color_rect)
self.draw_color = color
brush = QBrush(self.draw_color)
self.select_color_rect = self.select_color_scene.addRect(QRect(0, 0, self.select_color_view_size, self.select_color_view_size), \
brush=brush)
self.select_color_view.setScene(self.select_color_scene)
# Class for graphics contents of tools on main window
class GraphicsSceneForTools(QGraphicsScene):
# Define custom signal
img_info = Signal(QColor)
def __init__(self, parent=None, window=None):
QGraphicsScene.__init__(self, parent)
# Set parent view area
self.parent = parent
# Set grand parent window
self.window = window
self.mode = 'cursor'
def set_mode(self, mode):
self.mode = mode
def set_img_content(self, img_content):
# image data of Graphics Scene's contents
self.img_content = img_content
def mousePressEvent(self, event):
# For check program action
pos = event.scenePos()
x = pos.x()
y = pos.y()
if self.mode == 'cursor' or self.mode == 'pen':
self.pix_rgb = self.img_content.pixelColor(x, y)
self.img_info.emit(self.pix_rgb)
With the above contents, you will be able to select a pen or eraser tool to draw. The original image is displayed, and the result of my drawing is drawn on another layer above it. Furthermore, in order to save the contents drawn by yourself, it is necessary to export the drawn contents by dragging the mouse as an image. The contents drawn by the user's mouse drag are saved as a collection of lines that are loci. Lines have start and end points and pen attributes (color, size). Therefore, the coordinates that pass on the image are calculated based on the start point and end point in order from the group of lines, and are reflected in the image for export.
#Class that manages items to be placed in the drawing area
class GraphicsSceneForMainView(QGraphicsScene):
#When a pen or eraser is selected and drawn with mouse drag
def mouseMoveEvent(self, event):
# For check program action
pos = event.scenePos()
x = pos.x()
y = pos.y()
if self.mode == 'pen' or self.mode == 'eraser':
if x >= 0 and x < self.width() and y >= 0 and y < self.height():
if len(self.points) != 0:
draw_color = self.window.draw_color
# Set transparenc value
draw_color.setAlpha(self.window.layer_alpha)
draw_size = self.window.draw_tool_size
pen = QPen(draw_color, draw_size, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
self.lines_items.append(self.addLine(QLineF(self.points[-1].x(), self.points[-1].y(), x, y), pen=pen))
#The position of the drawn line and the attributes of the pen at that time for later saving as an image(Color, size)Save
self.lines.append(self.lines_items[-1].line())
self.pens.append(pen)
self.points.append(pos)
# Main Window components
class MainWindow(QMainWindow):
#Processing to reflect the user's drawing on the image
#The content drawn by dragging the mouse is a collection of line information with a start point and an end point.
def make_layer_image(self):
for i, line in enumerate(self.scene.lines):
pen = self.scene.pens[i]
pen_size = int(pen.width())
pen_color = pen.color()
# start pixel of line
x1 = int(line.x1())
y1 = int(line.y1())
# end pixel of line
x2 = int(line.x2())
y2 = int(line.y2())
dx = int(line.dx())
dy = int(line.dy())
# When only 1pixl line
if dx <= 1 and dy <= 1:
draw_pix_x1_s = max(x1 - int(pen_size/2), 0)
draw_pix_x1_e = min(x1 + int(pen_size/2), self.org_img_width-1)
draw_pix_y1_s = max(y1 - int(pen_size/2), 0)
draw_pix_y1_e = min(y1 + int(pen_size/2), self.org_img_height-1)
# for Pen's size
for y in range(draw_pix_y1_s, draw_pix_y1_e):
for x in range(draw_pix_x1_s, draw_pix_x1_e):
self.layer_qimg.setPixelColor(x, y, pen_color)
draw_pix_x2_s = max(x2 - int(pen_size/2), 0)
draw_pix_x2_e = min(x2 + int(pen_size/2), self.org_img_width-1)
draw_pix_y2_s = max(y2 - int(pen_size/2), 0)
draw_pix_y2_e = min(y2 + int(pen_size/2), self.org_img_height-1)
# for Pen's size
for y in range(draw_pix_y2_s, draw_pix_y2_e):
for x in range(draw_pix_x2_s, draw_pix_x2_e):
self.layer_qimg.setPixelColor(x, y, pen_color)
else:
# For avoid devide by 0
if dx == 0:
for y in range(y1, y2+1):
draw_pix_y_s = y - int(pen_size/2)
draw_pix_y_e = y + int(pen_size/2)
# for Pen's size
for yy in range(draw_pix_y_s, draw_pix_y_e):
self.layer_qimg.setPixelColor(x1, yy, pen_color)
else:
grad = dy/dx
# Choose coordinates with small slope not to skip pixels
if grad >= 1.0:
for x in range(dx):
y = y1 + int(grad * x + 0.5)
draw_pix_x_s = max(x1 + x - int(pen_size/2), 0)
draw_pix_x_e = min(x1 + x + int(pen_size/2), self.org_img_width-1)
draw_pix_y_s = max(y - int(pen_size/2), 0)
draw_pix_y_e = min(y + int(pen_size/2), self.org_img_height-1)
# for Pen's size
for yy in range(draw_pix_y_s, draw_pix_y_e+1):
for xx in range(draw_pix_x_s, draw_pix_x_e+1):
self.layer_qimg.setPixelColor(xx, yy, pen_color)
else:
for y in range(dy):
x = x1 + int(1/grad * y + 0.5)
draw_pix_y_s = max(y1 + y - int(pen_size/2), 0)
draw_pix_y_e = min(y1 + y + int(pen_size/2), self.org_img_height-1)
draw_pix_x_s = max(x - int(pen_size/2), 0)
draw_pix_x_e = min(x + int(pen_size/2), self.org_img_width-1)
# for Pen's size
for yy in range(draw_pix_y_s, draw_pix_y_e+1):
for xx in range(draw_pix_x_s, draw_pix_x_e+1):
self.layer_qimg.setPixelColor(xx, yy, pen_color)
Add'Save Layer Image' to the File menu and when you select it, it will save the image drawn by the user. Specifically, the code is as follows, execute the process to create a QImage image that reflects the drawing with make_layer_image () explained above, open the file dialog for saving, and save with the entered image file name. To do.
# Main Window components
class MainWindow(QMainWindow):
def __init__(self):
# Set "Save layer image" menu
self.layer_img_save_button = QAction(self.style().standardIcon(getattr(QStyle, 'SP_FileDialogEnd')), 'Save Layer Image', self)
self.layer_img_save_button.setShortcut('Ctrl+S')
self.layer_img_save_button.triggered.connect(self.save_layer_image)
self.file_menu.addAction(self.layer_img_save_button)
# Slot function of save layer image button clicked
def save_layer_image(self):
self.make_layer_image()
layer_img_default_path = self.app_setting["SoftwareSetting"]["file_path"]["layer_img_dir"]
options = QFileDialog.Options()
file_name, selected_filete = QFileDialog.getSaveFileName(self, 'Save layer image', layer_img_default_path, \
'image files(*.png, *jpg)', options=options)
self.layer_qimg.save(file_name)
ret = QMessageBox(self, 'Success', 'layer image is saved successfully', QMessageBox.Ok)
Qt also comes with a tool called Qt Designer that arranges parts such as buttons on the screen of the application you want to create on the GUI screen. Before you get used to it, it is easy to imagine what kind of parts (Widgets) you have, so it may be easier to understand if you try to make an appearance using this.
The source code of the created application will be posted in the following location. App source code page
Recommended Posts