Friday, February 17, 2017

Python Graphical User Interface (GUI) Using PyQt5: Basic - Part 1

I am not a big fan of GUI. I do not like developing the front-end of the program. For me, personally, developing the back-end of the program is more fun. However, if you are the only developer in a company, or in my case the only data scientist in my company, then developing a GUI is a must. Why? Because nobody gives a sh*t about the back-end. What they want is something that they can interact with, something that is "beautiful", and the final result.

General overview of the whole structure of the program


Hence, I started to learn how to develop the front-end of my program eventually. Let us take the case of our previous program, the program to measure the object dimension. In case you are wondering what kind of program that is, please refer to my previous posts. You may click here for the know the purpose of the program, or click here to see the first version of the program code, or click here for the second version of the program code.

Basically, our latest program was an object-oriented programming that needed various arguments to be passed in order to work perfectly. We kept all the required arguments or parameters within a .txt file. This time, we are going to design a GUI to accommodate all the required parameters. Within the next post, we are going to assign all functions to the buttons in our GUI. Therefore, we could generate the .txt file using the GUI.

So instead of manually editing the .txt file everytime we would like to run the program, we will set all the values within a GUI. One of the upsides of using GUI is that we could ensure that the key values in our dictionary would remain the same from time to time. Just to remind that we build a dictionary to carry all the parameters.

The first thing we need to do in developing a GUI is designing the interface. In Python there are several packages to develop a GUI. However, I found that developing a GUI in Python using PyQt is amazingly fun. Herein I am using PyQt5. It is one of the builtin packages in Anaconda.

Designing process of a GUI using PyQt5

The first thing you need to do (I assume you are using Anaconda as well just like me) is to find the designer.exe. This is a GUI to build a GUI. Just double-click on the icon. The welcome screen will greet you.

Hello and welcome!

Do not get confused. You just have to focus on two parts. The one that I marked with red-rectangle and the one that I marked with orange-rectangle. First, go to the red-rectangle part. Herein you may choose the existing templates, or you may also choose to start with a blank dialog. I usually start with blank dialog. Second, you may choose the window size or dimension. In this case, I chose the VGA portrait size.

Afterwards, click Create. Now we have the blank "canvas". Go to the Widget Box, please refer to the figure below. The one that I mark with red rectangle is the Widget Box area. Herein you could find all the buttons, input widgets, display widgets, containers, etc. In order to start "drawing" (read: design) on your blank "canvas" (read: dialog), it is just simply a matter of drag and drop from the Widget Box to the blank dialog window.

A blank dialog that is ready to serve your creativity!

For example, here is my final design (figure below). You just simply have to double click on the label to change the label. The next step, you just have to focus on the Object Inspector window (red rectangle) and its property (orange rectangle). In my case, I split the GUI into several groups:
  1. directory to the folder that contains images
  2. image file extensions
  3. coin properties (diameter and its measurement unit)
  4. image properties
  5. object detection properties, wherein this group is again split into three subgroups: a) Gaussian kernel size, b) Carry Hysteresis thresholding values, and c) morphological transform iterations.
Let us go through each group and each subgroup to finalize our GUI.

Final design of the GUI.


Directory to the folder that contains the images

Explain I: directory to the folder group

Herein the idea is to let the user select the folder that contains images that they want the images to be analyzed. In order to do so, the user just have to click a button, QToolButton (3). Then the directory to the folder would be visualized in the QLineEdit (2). These two objects are grouped in one QGroupBox (1). You may limit the user access in changing the directory in the QLineEdit (2) by disabling it (see purple arrow in figure above). This way the QLineEdit (2) would not be a free-text field anymore.

Image file extensions and coin properties

Explain II: image file extension and coin properties groups

Each QCheckBox object carries each image file extension (QGroupBox (1)). In default, I set the checkbox to be checked. To do so, please refer to the purple arrow in the figure above. As for the coin properties group (QGroupBox (2)) I let the diameter value to be a free-text (enabled QLineEdit), whereas the measurement unit is in the QComboBox. Later we will the measurement unit options in this QComboBox, for example we will assign mm, cm, and inch.

Image properties

Explain III: image properties group

As you may recall, I suggested to resize the image in order to get less expensive computational load. The suggested or default width of the image is stored in the QSpinBox (1) spinBoxResize object. In case user would like to rotate a bit the image due to any reasons, they can set the rotational degree in the QDoubleSpinBox (2). Lastly, do not forget to set up the property of each SpinBox in the property editor window.

Object detection properties

Explain IV: object detection properties group

The same process is repeated to design the object detection properties group. But herein I would like to emphasize the property for spinBoxGaussian (QSpinBox (1)). Since the value of the Gaussian kernel size needs to be in positive value AND odd, thus, you need to change the singleStep to 2 with the minimum at 1 and the default value at 5. As for the rest, I just used the same objects as I have already described previously.

As for the push buttons (OK and Close), I just leave them be. And for the final touch of our GUI design, I made some changes in the QDialog. Herein the QDialog, I made some changes to the maximum and minimum width as well as heights of the window. I made the values in Width and Height same across the minimumSize, maximumSize, and geometry. Therefore, our GUI window could not be resized.
Explain V: final touch in QDialog

Now we are done with the design! Congrats! Save the project and you are good to go to the next step. Hope that you feel that it is a simple process to design the front-end of our program. One little note from me, please make sure that you name each object in the right manner. If you just randomly name the objects, then when we start assigning the function to each object, you will get confused. Hence, in order to avoid the confusion in the next step, please make sure your objects' names are readable.

Before I end my post, one little step that we need to take before starting the assignments, is that to convert the .ui to .py. Here is how to do it. (1) You need to navigate to the folder that contains the .ui file. In my case I have to navigate to the following directory D:\Private\BLOG\2017\02. Feb\week3 since my Measure_Object_Dimension_UI.ui is stored inside the folder. Then (2) type the command as shown in the figure below. And done!
Converting .ui to .py

UPDATE:

If you installed the Anaconda as an administrator, then your pyuic5.bat file will not be stored in the AppData as shown above. It will be in the following directory, C:\Program Files\Anaconda3\Library\bin. And there is a bug in the pyuic5.bat file. If you type the following command in the command prompt (after you navigate to the folder that contains your .ui file), pyuic5 -x <filename.ui> -o <filename.py>, then probably you would end up with the following error message, 'C:/Program' is not recognized as an internal or external command operable program or batch file. Well, do not worry. It is just a minor bug. You just have to open the pyuic5.bat file in the notepad, and then surround the C:/Program Files/Anaconda3\python.exe with double-quotes. It would look like this:

1
@"C:/Program Files/Anaconda3\python.exe" -m PyQt5.uic.pyuic %1 %2 %3 %4 %5 %6 %7 %8 %9

END OF UPDATE

If you open the resulted file (mine is GUI.py), it is just another python script that we are already familiar with. Here is mine.
The resulted python file after the conversion

The full code for my GUI is as follows.


1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'Measure_Object_Dimension_UI.ui'
#
# Created by: PyQt5 UI code generator 5.6
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_Dialog(object):
    def setupUi(self, Dialog):
        Dialog.setObjectName("Dialog")
        Dialog.resize(261, 462)
        Dialog.setMinimumSize(QtCore.QSize(261, 462))
        Dialog.setMaximumSize(QtCore.QSize(261, 462))
        self.groupBoxDirectoryImage = QtWidgets.QGroupBox(Dialog)
        self.groupBoxDirectoryImage.setGeometry(QtCore.QRect(10, 10, 241, 51))
        self.groupBoxDirectoryImage.setObjectName("groupBoxDirectoryImage")
        self.lineEditDirectoryImage = QtWidgets.QLineEdit(self.groupBoxDirectoryImage)
        self.lineEditDirectoryImage.setEnabled(False)
        self.lineEditDirectoryImage.setGeometry(QtCore.QRect(10, 20, 191, 20))
        self.lineEditDirectoryImage.setClearButtonEnabled(False)
        self.lineEditDirectoryImage.setObjectName("lineEditDirectoryImage")
        self.toolButtonDirectoryImage = QtWidgets.QToolButton(self.groupBoxDirectoryImage)
        self.toolButtonDirectoryImage.setGeometry(QtCore.QRect(210, 20, 25, 19))
        self.toolButtonDirectoryImage.setObjectName("toolButtonDirectoryImage")
        self.groupBoxCoin = QtWidgets.QGroupBox(Dialog)
        self.groupBoxCoin.setGeometry(QtCore.QRect(150, 70, 101, 91))
        self.groupBoxCoin.setObjectName("groupBoxCoin")
        self.lineEditCoin = QtWidgets.QLineEdit(self.groupBoxCoin)
        self.lineEditCoin.setGeometry(QtCore.QRect(10, 20, 81, 20))
        self.lineEditCoin.setObjectName("lineEditCoin")
        self.comboBoxCoin = QtWidgets.QComboBox(self.groupBoxCoin)
        self.comboBoxCoin.setGeometry(QtCore.QRect(10, 50, 81, 22))
        self.comboBoxCoin.setObjectName("comboBoxCoin")
        self.groupBoxFileExtension = QtWidgets.QGroupBox(Dialog)
        self.groupBoxFileExtension.setGeometry(QtCore.QRect(10, 70, 131, 91))
        self.groupBoxFileExtension.setObjectName("groupBoxFileExtension")
        self.checkBoxJPPG = QtWidgets.QCheckBox(self.groupBoxFileExtension)
        self.checkBoxJPPG.setGeometry(QtCore.QRect(10, 20, 81, 17))
        self.checkBoxJPPG.setChecked(True)
        self.checkBoxJPPG.setObjectName("checkBoxJPPG")
        self.checkBoxPNG = QtWidgets.QCheckBox(self.groupBoxFileExtension)
        self.checkBoxPNG.setGeometry(QtCore.QRect(10, 40, 70, 17))
        self.checkBoxPNG.setChecked(True)
        self.checkBoxPNG.setObjectName("checkBoxPNG")
        self.checkBoxBMP = QtWidgets.QCheckBox(self.groupBoxFileExtension)
        self.checkBoxBMP.setGeometry(QtCore.QRect(10, 60, 70, 17))
        self.checkBoxBMP.setChecked(True)
        self.checkBoxBMP.setObjectName("checkBoxBMP")
        self.checkBoxJEPG = QtWidgets.QCheckBox(self.groupBoxFileExtension)
        self.checkBoxJEPG.setGeometry(QtCore.QRect(80, 20, 81, 17))
        self.checkBoxJEPG.setChecked(True)
        self.checkBoxJEPG.setObjectName("checkBoxJEPG")
        self.checkBoxTIF = QtWidgets.QCheckBox(self.groupBoxFileExtension)
        self.checkBoxTIF.setGeometry(QtCore.QRect(80, 40, 81, 17))
        self.checkBoxTIF.setChecked(True)
        self.checkBoxTIF.setObjectName("checkBoxTIF")
        self.checkBoxTIFF = QtWidgets.QCheckBox(self.groupBoxFileExtension)
        self.checkBoxTIFF.setGeometry(QtCore.QRect(80, 60, 81, 17))
        self.checkBoxTIFF.setChecked(True)
        self.checkBoxTIFF.setObjectName("checkBoxTIFF")
        self.groupBoxImageProperties = QtWidgets.QGroupBox(Dialog)
        self.groupBoxImageProperties.setGeometry(QtCore.QRect(10, 170, 241, 80))
        self.groupBoxImageProperties.setObjectName("groupBoxImageProperties")
        self.labelResize = QtWidgets.QLabel(self.groupBoxImageProperties)
        self.labelResize.setGeometry(QtCore.QRect(10, 21, 47, 20))
        self.labelResize.setObjectName("labelResize")
        self.labelPixels = QtWidgets.QLabel(self.groupBoxImageProperties)
        self.labelPixels.setGeometry(QtCore.QRect(190, 20, 31, 21))
        self.labelPixels.setObjectName("labelPixels")
        self.labelRotate = QtWidgets.QLabel(self.groupBoxImageProperties)
        self.labelRotate.setGeometry(QtCore.QRect(10, 51, 47, 21))
        self.labelRotate.setObjectName("labelRotate")
        self.doubleSpinBoxRotate = QtWidgets.QDoubleSpinBox(self.groupBoxImageProperties)
        self.doubleSpinBoxRotate.setGeometry(QtCore.QRect(60, 50, 121, 22))
        self.doubleSpinBoxRotate.setSingleStep(0.1)
        self.doubleSpinBoxRotate.setObjectName("doubleSpinBoxRotate")
        self.labelDegree = QtWidgets.QLabel(self.groupBoxImageProperties)
        self.labelDegree.setGeometry(QtCore.QRect(190, 50, 41, 21))
        self.labelDegree.setObjectName("labelDegree")
        self.spinBoxResize = QtWidgets.QSpinBox(self.groupBoxImageProperties)
        self.spinBoxResize.setGeometry(QtCore.QRect(60, 20, 121, 22))
        self.spinBoxResize.setMinimum(500)
        self.spinBoxResize.setMaximum(99999)
        self.spinBoxResize.setSingleStep(20)
        self.spinBoxResize.setProperty("value", 700)
        self.spinBoxResize.setObjectName("spinBoxResize")
        self.groupBoxObjectDetect = QtWidgets.QGroupBox(Dialog)
        self.groupBoxObjectDetect.setGeometry(QtCore.QRect(10, 260, 241, 161))
        self.groupBoxObjectDetect.setObjectName("groupBoxObjectDetect")
        self.labelGaussian = QtWidgets.QLabel(self.groupBoxObjectDetect)
        self.labelGaussian.setGeometry(QtCore.QRect(10, 21, 101, 20))
        self.labelGaussian.setObjectName("labelGaussian")
        self.spinBoxGaussian = QtWidgets.QSpinBox(self.groupBoxObjectDetect)
        self.spinBoxGaussian.setGeometry(QtCore.QRect(120, 20, 71, 22))
        self.spinBoxGaussian.setMinimum(1)
        self.spinBoxGaussian.setSingleStep(2)
        self.spinBoxGaussian.setProperty("value", 5)
        self.spinBoxGaussian.setObjectName("spinBoxGaussian")
        self.labelGaussianPixels = QtWidgets.QLabel(self.groupBoxObjectDetect)
        self.labelGaussianPixels.setGeometry(QtCore.QRect(200, 21, 31, 20))
        self.labelGaussianPixels.setObjectName("labelGaussianPixels")
        self.groupBoxCanny = QtWidgets.QGroupBox(self.groupBoxObjectDetect)
        self.groupBoxCanny.setGeometry(QtCore.QRect(10, 50, 221, 71))
        self.groupBoxCanny.setObjectName("groupBoxCanny")
        self.lineEditCannyMin = QtWidgets.QLineEdit(self.groupBoxCanny)
        self.lineEditCannyMin.setGeometry(QtCore.QRect(10, 40, 91, 20))
        self.lineEditCannyMin.setObjectName("lineEditCannyMin")
        self.lineEditCannyMax = QtWidgets.QLineEdit(self.groupBoxCanny)
        self.lineEditCannyMax.setGeometry(QtCore.QRect(120, 40, 91, 20))
        self.lineEditCannyMax.setObjectName("lineEditCannyMax")
        self.labelCannyMin = QtWidgets.QLabel(self.groupBoxCanny)
        self.labelCannyMin.setGeometry(QtCore.QRect(10, 20, 91, 16))
        self.labelCannyMin.setObjectName("labelCannyMin")
        self.labelCannyMax = QtWidgets.QLabel(self.groupBoxCanny)
        self.labelCannyMax.setGeometry(QtCore.QRect(120, 20, 91, 16))
        self.labelCannyMax.setObjectName("labelCannyMax")
        self.labelGaussianIterations = QtWidgets.QLabel(self.groupBoxObjectDetect)
        self.labelGaussianIterations.setGeometry(QtCore.QRect(10, 130, 171, 20))
        self.labelGaussianIterations.setObjectName("labelGaussianIterations")
        self.spinBoxGaussianInterations = QtWidgets.QSpinBox(self.groupBoxObjectDetect)
        self.spinBoxGaussianInterations.setGeometry(QtCore.QRect(180, 130, 51, 22))
        self.spinBoxGaussianInterations.setMinimum(1)
        self.spinBoxGaussianInterations.setObjectName("spinBoxGaussianInterations")
        self.pushButtonOK = QtWidgets.QPushButton(Dialog)
        self.pushButtonOK.setGeometry(QtCore.QRect(20, 430, 101, 23))
        self.pushButtonOK.setObjectName("pushButtonOK")
        self.pushButtonClose = QtWidgets.QPushButton(Dialog)
        self.pushButtonClose.setGeometry(QtCore.QRect(140, 430, 101, 23))
        self.pushButtonClose.setObjectName("pushButtonClose")

        self.retranslateUi(Dialog)
        QtCore.QMetaObject.connectSlotsByName(Dialog)

    def retranslateUi(self, Dialog):
        _translate = QtCore.QCoreApplication.translate
        Dialog.setWindowTitle(_translate("Dialog", "Measure Object Dimension GUI v0.1"))
        self.groupBoxDirectoryImage.setTitle(_translate("Dialog", "Directory to the folder contains images"))
        self.toolButtonDirectoryImage.setText(_translate("Dialog", "..."))
        self.groupBoxCoin.setTitle(_translate("Dialog", "Coin diameter"))
        self.groupBoxFileExtension.setTitle(_translate("Dialog", "Image fiile extension"))
        self.checkBoxJPPG.setText(_translate("Dialog", "JPG"))
        self.checkBoxPNG.setText(_translate("Dialog", "PNG"))
        self.checkBoxBMP.setText(_translate("Dialog", "BMP"))
        self.checkBoxJEPG.setText(_translate("Dialog", "JPEG"))
        self.checkBoxTIF.setText(_translate("Dialog", "TIF"))
        self.checkBoxTIFF.setText(_translate("Dialog", "TIF"))
        self.groupBoxImageProperties.setTitle(_translate("Dialog", "Image properties"))
        self.labelResize.setText(_translate("Dialog", "Resize to"))
        self.labelPixels.setText(_translate("Dialog", "pixels"))
        self.labelRotate.setText(_translate("Dialog", "Rotate"))
        self.labelDegree.setText(_translate("Dialog", "degree"))
        self.groupBoxObjectDetect.setTitle(_translate("Dialog", "Object detection properties"))
        self.labelGaussian.setText(_translate("Dialog", "Gaussian Kernel Size"))
        self.labelGaussianPixels.setText(_translate("Dialog", "pixels"))
        self.groupBoxCanny.setTitle(_translate("Dialog", "Canny Hysteresis Thresholding"))
        self.labelCannyMin.setText(_translate("Dialog", "Minimum"))
        self.labelCannyMax.setText(_translate("Dialog", "Maximum"))
        self.labelGaussianIterations.setText(_translate("Dialog", "Morphological transform iterations"))
        self.pushButtonOK.setText(_translate("Dialog", "OK"))
        self.pushButtonClose.setText(_translate("Dialog", "Close"))


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    Dialog = QtWidgets.QDialog()
    ui = Ui_Dialog()
    ui.setupUi(Dialog)
    Dialog.show()
    sys.exit(app.exec_())

Looks complicated? Well yeah if you need to write the code line by line. However, since we are using the PyQT designer, then it is not that complicated. That is all folks for now. As always, thanks for reading. If you any query or comments, please do leave them down below in the comment section. My next post will be how to assign every function to every button in our GUI, thus, we could produce the same .txt file as before. Cheers!

No comments:

Post a Comment