-
Notifications
You must be signed in to change notification settings - Fork 0
/
TheEggCounter.py
executable file
·335 lines (272 loc) · 13.9 KB
/
TheEggCounter.py
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
#!/usr/bin/env python3
# TheEggCounter.py
#
# A tiny application suited for counting eggs for the estimation
# of DEPM (Daily Egg Production Method) fecundity parameter
# (But you could count many other things on a picture...)
#
# Copyright 2014,2020 Jorge Tornero Nunez http://imasdemase.com
#
# This file is part of TheEggCounter, V1.0
#
# TheEggCounter is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# TheEggCounter is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with TheEggCounter. If not, see <http://www.gnu.org/licenses/>.
import sys, os
from PyQt5 import QtGui, QtCore, QtWidgets
from counter_gui import Ui_OnImageCounter as CounterGUI
from aboutdialog import Ui_aboutDialog as AboutDialog
class ItemCounter(QtWidgets.QMainWindow):
"""
This class provides a Main Window with a graphic view
where an image with items to be counted is placed and
counting such items can be performed using the mouse
"""
# Defines the color for marker and its outline
markerPen = QtGui.QPen(QtGui.QColor(QtCore.Qt.black))
markerBrush = QtGui.QBrush(QtGui.QColor(QtCore.Qt.red))
textPen = QtGui.QPen(QtGui.QColor(QtCore.Qt.yellow))
def __init__(self, screenWidth = 1280, screenHeight = 1024):
QtWidgets.QMainWindow.__init__(self)
self.setGeometry(0, 0, screenWidth, screenHeight)
self.countingWindow = CounterGUI()
self.translateUi()
self.countingWindow.setupUi(self)
self.resize(screenWidth,screenHeight)
self.scene = QtWidgets.QGraphicsScene()
self.countingWindow.imageView.setScene(self.scene)
# Reimplementation of keypress event management
self.keyPressEvent = self.manageCounterKeyboardEvent
self.setItemCount(0)
self.countingWindow.finishButton.setEnabled(False)
self.countingWindow.resetCounterButton.setEnabled(False)
self.connectSignals()
def translateUi(self):
"""
This function localizates GUI and related messages
"""
# We get the running dir and the locale
self.running_dir = os.path.dirname(__file__)
locale = QtCore.QLocale().name()
# Generation of paths for localization files, must be inside
# i18n folder and their name should be like:
# theeggcounter_[LOCALE].qm
# counter_gui_[LOCALE].qm
# aboutdialog_[LOCALE].qm
# WHere [LOCALE] is the name of the locale following BCP47 guidelines
# You can create your own translations by creating new files with
# QtLinguist.
mainTranslatorPath = os.path.join(\
self.running_dir, 'i18n', 'theeggcounter_{0}.qm'.format(locale))
UiTranslatorPath = os.path.join(\
self.running_dir, 'i18n', 'counter_gui_{0}.qm'.format(locale))
aboutTranslatorPath = os.path.join(\
self.running_dir, 'i18n', 'aboutdialog_{0}.qm'.format(locale))
# Localization
self.mainTranslator = QtCore.QTranslator()
self.UiTranslator = QtCore.QTranslator()
self.aboutTranslator = QtCore.QTranslator()
self.mainTranslator.load(mainTranslatorPath)
self.UiTranslator.load(UiTranslatorPath)
self.aboutTranslator.load(aboutTranslatorPath)
QtCore.QCoreApplication.installTranslator(self.mainTranslator)
QtCore.QCoreApplication.installTranslator(self.UiTranslator)
QtCore.QCoreApplication.installTranslator(self.aboutTranslator)
def connectSignals(self):
"""
Connects GUI buttons as well as menu actions with their event handlers
"""
self.countingWindow.loadImageButton.clicked.connect(self.loadImage)
self.countingWindow.resetCounterButton.clicked.connect(self.resetCounter)
self.countingWindow.finishButton.clicked.connect(self.finishCount)
self.countingWindow.actionExit.triggered.connect(self.exitCounter)
self.countingWindow.actionAbout.triggered.connect(self.about)
def loadImage(self):
"""
Loads an image from disk and displays it
"""
filenameDialog = QtWidgets.QFileDialog.getOpenFileName(None,
self.tr("Select an image to load"),
options=QtWidgets.QFileDialog.DontUseNativeDialog)
self.filename = filenameDialog[0]
pixmap = QtGui.QPixmap(self.filename)
pixmap = pixmap.scaledToHeight(\
self.countingWindow.imageView.height(),QtCore.Qt.SmoothTransformation)
self.pixmap = QtWidgets.QGraphicsPixmapItem(pixmap)
self.scene.addItem(self.pixmap)
#self.pixmap = QtWidgets.QGraphicsPixmapItem(pixmap, scene = self.scene)
# Reimplementation of the mousePressEvent of the pixmap
self.pixmap.mousePressEvent = self.manageCounterMouseEvent
# We calculate the width of the pixmap and subsequently, the step
# for scrolling the view. The view shift is calculated so visibility
# of all the markers is guaranteed while scrolling the view
self.pixmapWidth = pixmap.width()
self.viewShift = self.pixmapWidth / ((self.pixmapWidth\
/ self.countingWindow.imageView.width()) + 1)
# Sets the scrollbar to the leftmost part of the image
self.countingWindow.imageView.horizontalScrollBar().setValue(0)
# Once loaded the file, we enable the Finish Count button
self.countingWindow.finishButton.setEnabled(True)
self.countingWindow.resetCounterButton.setEnabled(True)
def resetCounter(self):
"""
This function just resets the counter, so we
remove only markers and keep the image. This is done
by removing only those items with type() equal to 4,
which means that is an ellipse
"""
self.setItemCount(0)
for item in self.scene.items():
if item.type() == 4:
self.scene.removeItem(item)
def manageCounterMouseEvent(self, event):
"""
This function manages the mouse events over the image:
Left button adds a marker and increases the count.
Right button removes a marker and decreases the count
Left + Ctrl moves the view to the right
Right + Ctrl moves the view to the left
"""
keyboardModifier = event.modifiers()
pressedMouseButton = event.button()
# We must get the position of the scroll bar and.
#viewShift = self.pixmapWidth/((self.pixmapWidth/self.countingWindow.imageView.width())+1)
viewPosition = self.countingWindow.imageView.horizontalScrollBar().value()
# Left mouse click management
if pressedMouseButton == QtCore.Qt.LeftButton:
if keyboardModifier == QtCore.Qt.NoModifier:
# Adds a marker in the position of the click
# taking into account the diameter of the marker
posx = event.pos().x()
posy = event.pos().y()
self.scene.addEllipse(posx-3.5, posy-3.5, 7, 7, self.markerPen, self.markerBrush)
self.setItemCount(self.itemCount + 1)
elif keyboardModifier == QtCore.Qt.ControlModifier:
# Moves view to the right
self.countingWindow.imageView.horizontalScrollBar().setValue(viewPosition - self.viewShift)
# Right mouse click management
elif pressedMouseButton == QtCore.Qt.RightButton:
if keyboardModifier == QtCore.Qt.NoModifier:
# Removes the marker under the cursor
posx = event.pos().x()
posy = event.pos().y()
itemToDelete = self.scene.itemAt(posx,posy,QtGui.QTransform())
if itemToDelete.type() == 4: # Means is an ellipse (marker)
self.scene.removeItem(itemToDelete)
self.setItemCount(self.itemCount - 1)
elif keyboardModifier == QtCore.Qt.ControlModifier:
# Moves view to the left
self.countingWindow.imageView.horizontalScrollBar().setValue(viewPosition + self.viewShift)
def manageCounterKeyboardEvent(self, event):
"""
This function handles the keyboard events of the dialog
"""
keyPressed = event.key()
keyboardModifier = event.modifiers()
# Shorcut for loading images
if (keyPressed == QtCore.Qt.Key_L and keyboardModifier == QtCore.Qt.ControlModifier):
self.loadImage()
# Shorcut for finishing the count
elif (keyPressed == QtCore.Qt.Key_F and keyboardModifier == QtCore.Qt.ControlModifier and self.countingWindow.finishButton.isEnabled() == True):
self.finishCount()
# Shorcut for resetting the count
elif (keyPressed == QtCore.Qt.Key_R and keyboardModifier == QtCore.Qt.ControlModifier and self.countingWindow.resetCounterButton.isEnabled() == True):
self.resetCounter()
def finishCount(self):
"""
This function ask for confirmation when the user has finished counting
and wants to mark the image file as counted.
It adds the suffix _cntd to the file and also inserts the number of items
counted in the top right corner of the image.
Additionally, it completely resets the view removing the markers and the pixmap
from it.
"""
# We get some info from the image file
fileInfo = QtCore.QFileInfo(self.filename)
suffix = fileInfo.completeSuffix()
basename = fileInfo.baseName()
newFilename = basename + '_ctnd.' + suffix
filePath = fileInfo.absolutePath()
destinationFile = filePath + '/' + newFilename
# Firstly we ask for confirmation
confirmation = QtWidgets.QMessageBox(None)
confirmation.setWindowTitle(self.tr("Confirmation Dialog"))
confirmation.setText(self.tr(u"<center>This action will:<ol><li>Remove all markers</li><li>Unload the image</li><li>Insert the number of counted items in the image</li><li>Create file %s</li></ol><br><b>Do you want to proceed with the above?</b><center>") %newFilename)
btn1 = confirmation.addButton(self.tr(u"Accept"), QtWidgets.QMessageBox.YesRole)
if confirmation.exec_() == 0: # User agrees
# We will load again the image file, because we scaled it
# when first loaded
pixmap = QtGui.QPixmap(self.filename)
# We create a painter on the pixmap to be able to
# draw on it
painter = QtGui.QPainter(pixmap)
painter.setPen(self.textPen)
font = QtGui.QFont("Arial", 30)
painter.setFont(font)
pixmapWidth = pixmap.width()
countText = self.tr(u"Count: %s") %self.getItemCount()
fm = QtGui.QFontMetrics(font)
countTextHeight = fm.boundingRect(countText).height()
painter.drawText(QtCore.QPoint(pixmapWidth / 2, countTextHeight + 5), countText )
# End the painter is mandatory
painter.end()
# Now saves the filename appending the count marker
pixmap.save(destinationFile)
# Removing items and pixmap froom scene
for item in self.scene.items():
self.scene.removeItem(item)
# Finally we set the count to zero and disable the finish count button
# as well as the reset count button
self.setItemCount(0)
self.countingWindow.finishButton.setEnabled(False)
self.countingWindow.resetCounterButton.setEnabled(False)
def setItemCount(self, count):
"""
Sets the count to a integer value and displays its value
"""
self.itemCount = count
text = self.tr(u"Counted items: %s") %count
self.countingWindow.counterLabel.setText(text)
def getItemCount(self):
"""
Returns the number of items counted
"""
return self.itemCount
def about(self):
"""
Shows an about dialog with license and some simple help
"""
dialog = QtWidgets.QDialog()
aboutDialog = AboutDialog()
aboutDialog.setupUi(dialog)
dialog.exec_()
def exitCounter(self):
"""
Function to exit the dialog, asking for confirmation
"""
confirmation = QtWidgets.QMessageBox(None)
confirmation.setWindowTitle(self.tr("Exiting TheEggCounter"))
confirmation.setText(self.tr("""<center>Are you sure you want to exit?</b>
</center>"""))
btn1 = confirmation.addButton(self.tr("Accept"), QtWidgets.QMessageBox.YesRole)
btn2 = confirmation.addButton(self.tr("No, thanks"), QtWidgets.QMessageBox.YesRole)
if confirmation.exec_() == 0:
self.close()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
# We get the screen size to set the size of the counter
primaryScreen = app.desktop().primaryScreen()
screenWidth = app.desktop().screenGeometry(primaryScreen).width()
screenHeight = app.desktop().screenGeometry(primaryScreen).height()
itemCounter = ItemCounter(screenWidth, screenHeight)
itemCounter.show()
sys.exit(app.exec_())