Skip to content

Commit 80d9a74

Browse files
committed
Added manual segmentation tool to SegUI
Added a new tab to SegUI to enable manual segmentation of images. The manual segmentation is saved as manual_mask.npy in the parameters directory. This is then automatically loaded if 'UI' is passed as the mask argument in ParticleAnalysis. To manually segment, users left-click and drag around the border of a particle. They then right-click within the red border. Note: do not right-click if the border is incomplete!
1 parent 0719247 commit 80d9a74

4 files changed

Lines changed: 137 additions & 43 deletions

File tree

12.4 KB
Binary file not shown.

ParticleSpy/ParticleAnalysis.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ def ParticleAnalysis(acquisition,parameters,particles=None,mask=np.zeros((1))):
5757
image = acquisition
5858
ac_types = 'Image only'
5959

60-
if mask.sum()==0:
60+
if mask == 'UI':
61+
labeled = label(np.load(inspect.getfile(process).rpartition('\\')[0]+'/Parameters/manual_mask.npy'))
62+
elif mask.sum()==0:
6163
labeled = process(image,parameters)
6264
#labels = np.unique(labeled).tolist() #some labeled number have been removed by "remove_small_holes" function
6365
else:

ParticleSpy/SegUI.py

Lines changed: 132 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@
66
"""
77

88
from PyQt5.QtWidgets import QCheckBox, QPushButton, QLabel, QMainWindow, QSpinBox
9-
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QComboBox, QTabWidget
10-
from PyQt5.QtGui import QPixmap, QImage
9+
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QComboBox, QTabWidget
10+
from PyQt5.QtWidgets import QVBoxLayout
11+
from PyQt5.QtGui import QPixmap, QImage, QColor, QPainter, QBitmap
1112
from PyQt5.QtCore import Qt
1213
import sys
1314

1415
import inspect
1516
import numpy as np
16-
from skimage.segmentation import mark_boundaries
17+
from skimage.segmentation import mark_boundaries, flood_fill
1718
from skimage.util import invert
19+
import matplotlib.pyplot as plt
20+
from PIL import Image
1821

1922
from ParticleSpy.segptcls import process
2023
from ParticleSpy.ParticleAnalysis import parameters
@@ -34,7 +37,7 @@ def __init__(self,im_hs):
3437

3538
offset = 50
3639

37-
self.layout = QVBoxLayout(self)
40+
self.layout = QHBoxLayout(self)
3841

3942
# Initialize tab screen
4043
self.tabs = QTabWidget()
@@ -47,45 +50,41 @@ def __init__(self,im_hs):
4750

4851
#self.central_widget = QWidget()
4952
#self.setCentralWidget(self.central_widget)
50-
lay = QVBoxLayout()
53+
lay = QHBoxLayout()
54+
leftlay = QVBoxLayout()
55+
rightlay = QVBoxLayout()
5156
self.tab1.setLayout(lay)
5257

5358
self.label = QLabel(self)
5459
qi = QImage(self.image.data, self.image.shape[1], self.image.shape[0], self.image.shape[1], QImage.Format_Grayscale8)
5560
pixmap = QPixmap(qi)
56-
pixmap2 = pixmap.scaled(512, 512, Qt.KeepAspectRatio)
57-
self.label.setPixmap(pixmap2)
58-
self.label.setGeometry(10,10,pixmap2.width(),pixmap2.height())
61+
self.pixmap2 = pixmap.scaled(512, 512, Qt.KeepAspectRatio)
62+
self.label.setPixmap(self.pixmap2)
63+
self.label.setGeometry(10,10,self.pixmap2.width(),self.pixmap2.height())
5964

60-
height = max((pixmap2.height()+50,300 + offset)) #300 +50
65+
height = max((self.pixmap2.height()+50,300 + offset)) #300 +50
6166

62-
self.resize(pixmap2.width()+130, height)
67+
self.resize(self.pixmap2.width()+130, height)
6368

6469
self.filt_title = QLabel(self)
6570
self.filt_title.setText('Pre-filtering options')
66-
self.filt_title.move(pixmap2.width()+20, 0)
6771

6872
self.sptxt = QLabel(self)
6973
self.sptxt.setText('Rolling ball size')
70-
self.sptxt.move(pixmap2.width()+20,20)
7174

7275
self.sp = QSpinBox(self)
7376
self.sp.setMaximum(self.image.shape[0])
7477
self.sp.valueChanged.connect(self.rollingball)
75-
self.sp.move(pixmap2.width()+20, 45)
7678

7779
self.gausstxt = QLabel(self)
7880
self.gausstxt.setText('Gaussian filter kernel size')
79-
self.gausstxt.move(pixmap2.width()+20,70)
8081

8182
self.gauss = QSpinBox(self)
8283
self.gauss.setMaximum(self.image.shape[0])
8384
self.gauss.valueChanged.connect(self.gaussian)
84-
self.gauss.move(pixmap2.width()+20, 95)
8585

8686
self.thresh_title = QLabel(self)
8787
self.thresh_title.setText('Thresholding options')
88-
self.thresh_title.move(pixmap2.width()+20, 135)
8988

9089
self.comboBox = QComboBox(self)
9190
self.comboBox.addItem("Otsu")
@@ -97,72 +96,90 @@ def __init__(self,im_hs):
9796
self.comboBox.addItem("Local")
9897
self.comboBox.addItem("Local Otsu")
9998
self.comboBox.addItem("Local+Global Otsu")
100-
self.comboBox.move(pixmap2.width()+20, 160)
10199
self.comboBox.activated[str].connect(self.threshold_choice)
102100

103101
self.localtxt = QLabel(self)
104102
self.localtxt.setText('Local filter kernel')
105-
self.localtxt.move(pixmap2.width()+20,195)
106103

107104
self.local_size = QSpinBox(self)
108105
self.local_size.setMaximum(self.image.shape[0])
109106
self.local_size.valueChanged.connect(self.local)
110-
self.local_size.move(pixmap2.width()+20, 220)
111107

112108
cb = QCheckBox('Watershed', self)
113-
cb.move(pixmap2.width()+20, 260)
114109
cb.stateChanged.connect(self.changeWatershed)
115110

116111
cb2 = QCheckBox('Invert', self)
117-
cb2.move(pixmap2.width()+20, 260 + offset /2 )
118112
cb2.stateChanged.connect(self.changeInvert)
119113

120114
self.minsizetxt = QLabel(self)
121115
self.minsizetxt.setText('Min particle size (px)')
122-
self.minsizetxt.move(pixmap2.width()+20, 280+offset)
123116

124117
self.minsizev = QSpinBox(self)
125118
self.minsizev.setMaximum(self.image.shape[0]*self.image.shape[1])
126119
self.minsizev.valueChanged.connect(self.minsize)
127-
self.minsizev.move(pixmap2.width()+20, 305+offset)
128120

129121
updateb = QPushButton('Update',self)
130-
updateb.move(pixmap2.width()+20,355+offset)
131122
updateb.clicked.connect(self.update)
132123

133124
paramsb = QPushButton('Get Params',self)
134-
paramsb.move(pixmap2.width()+20,385+offset)
135125

136126
paramsb.clicked.connect(self.return_params)
137127

138128
self.imagetxt = QLabel(self)
139129
self.imagetxt.setText('Display:')
140-
self.imagetxt.move(75, pixmap2.height()+15)
141130

142131
self.imBox = QComboBox(self)
143132
self.imBox.addItem("Image")
144133
self.imBox.addItem("Labels")
145-
self.imBox.move(pixmap2.width()/2-10, pixmap2.height()+15)
146134

147135
self.imBox.activated[str].connect(self.changeIm)
148-
149-
lay.addWidget(self.thresh_title)
150-
lay.addWidget(self.filt_title)
151-
lay.addWidget(self.label)
152-
lay.addWidget(self.comboBox)
153-
lay.addWidget(self.sp)
154-
lay.addWidget(self.sptxt)
155-
lay.addWidget(self.gauss)
156-
lay.addWidget(self.gausstxt)
157-
lay.addWidget(self.imagetxt)
158-
lay.addWidget(self.minsizev)
159-
136+
137+
leftlay.addWidget(self.label)
138+
leftlay.addWidget(self.imagetxt)
139+
leftlay.addWidget(self.imBox)
140+
141+
rightlay.addWidget(self.filt_title)
142+
rightlay.addWidget(self.sptxt)
143+
rightlay.addWidget(self.sp)
144+
rightlay.addWidget(self.gausstxt)
145+
rightlay.addWidget(self.gauss)
146+
rightlay.addStretch(1)
147+
rightlay.addWidget(self.thresh_title)
148+
rightlay.addWidget(self.comboBox)
149+
rightlay.addStretch(1)
150+
rightlay.addWidget(self.localtxt)
151+
rightlay.addWidget(self.local_size)
152+
rightlay.addStretch(1)
153+
rightlay.addWidget(cb)
154+
rightlay.addWidget(cb2)
155+
rightlay.addStretch(1)
156+
rightlay.addWidget(self.minsizetxt)
157+
rightlay.addWidget(self.minsizev)
158+
rightlay.addStretch(2)
159+
rightlay.addWidget(updateb)
160+
rightlay.addWidget(paramsb)
161+
162+
lay.addLayout(leftlay)
163+
lay.addLayout(rightlay)
160164

161165
self.layout.addWidget(self.tabs)
162166
self.setLayout(self.layout)
163167

164168
self.setCentralWidget(self.tabs)
165169

170+
#Tab 2
171+
self.canvas = Canvas(self.pixmap2)
172+
#self.canvas = Drawer(self.pixmap2)
173+
174+
self.getarrayb = QPushButton('Save Segmentation',self)
175+
self.getarrayb.clicked.connect(self.save_array)
176+
177+
tab2layout = QVBoxLayout()
178+
tab2layout.addWidget(self.canvas)
179+
tab2layout.addWidget(self.getarrayb)
180+
tab2layout.addStretch(1)
181+
self.tab2.setLayout(tab2layout)
182+
166183
self.show()
167184

168185
def getim(self,im_hs):
@@ -198,8 +215,8 @@ def changeInvert(self, state):
198215
qi = QImage(self.image.data, self.image.shape[1], self.image.shape[0], self.image.shape[1], QImage.Format_Indexed8)
199216

200217
pixmap = QPixmap(qi)
201-
pixmap2 = pixmap.scaled(512, 512, Qt.KeepAspectRatio)
202-
self.label.setPixmap(pixmap2)
218+
self.pixmap2 = pixmap.scaled(512, 512, Qt.KeepAspectRatio)
219+
self.label.setPixmap(self.pixmap2)
203220

204221
def rollingball(self):
205222
if self.sp.value() == 1:
@@ -273,7 +290,78 @@ def threshold_choice(self):
273290
self.params.segment['threshold'] = "local_otsu"
274291
if str(self.comboBox.currentText()) == "Local+Global Otsu":
275292
self.params.segment['threshold'] = "lg_otsu"
293+
294+
def save_array(self):
295+
self.canvas.savearray(self.image)
296+
297+
298+
class Canvas(QLabel):
299+
300+
def __init__(self,pixmap):
301+
super().__init__()
302+
self.setPixmap(pixmap)
303+
304+
self.last_x, self.last_y = None, None
305+
self.pen_color = QColor(255, 0, 0, 20)
306+
307+
def set_pen_color(self, c):
308+
self.pen_color = QColor(c)
309+
310+
def mousePressEvent(self, e):
311+
if e.button() == Qt.RightButton:
312+
image = self.pixmap().toImage()
313+
b = image.bits()
314+
b.setsize(512 * 512 * 4)
315+
arr = np.frombuffer(b, np.uint8).reshape((512, 512, 4))
316+
317+
arr_test = arr[:,:,0]/arr[:,:,2]
318+
319+
painted_arr = np.zeros_like(arr[:,:,0:3])
320+
painted_arr[:,:,2][arr_test!=1] = 255
321+
322+
painted_arr[:,:,2] = flood_fill(painted_arr[:,:,2],(e.y(),e.x()),255)
323+
324+
qi = QImage(painted_arr.data, painted_arr.shape[1], painted_arr.shape[0], 3*painted_arr.shape[1], QImage.Format_RGB888)
325+
pixmap = QPixmap(qi)
326+
327+
painter = QPainter(self.pixmap())
328+
painter.setOpacity(0.01)
329+
330+
painter.drawPixmap(0, 0, pixmap)
331+
painter.end()
332+
self.update()
333+
334+
self.array = painted_arr[:,:,2]
335+
336+
def mouseMoveEvent(self, e):
337+
if e.buttons() == Qt.LeftButton:
338+
if self.last_x is None: # First event.
339+
self.last_x = e.x()
340+
self.last_y = e.y()
341+
return # Ignore the first time.
342+
343+
painter = QPainter(self.pixmap())
344+
p = painter.pen()
345+
p.setWidth(4)
346+
p.setColor(self.pen_color)
347+
painter.setPen(p)
348+
painter.drawLine(self.last_x, self.last_y, e.x(), e.y())
349+
painter.end()
350+
self.update()
276351

352+
# Update the origin for next time.
353+
self.last_x = e.x()
354+
self.last_y = e.y()
355+
356+
def mouseReleaseEvent(self, e):
357+
self.last_x = None
358+
self.last_y = None
359+
360+
def savearray(self,image):
361+
resized = np.array(Image.fromarray(self.array).resize((image.shape[0],image.shape[1])))
362+
np.save('Parameters/manual_mask',resized)
363+
364+
277365
def main(haadf):
278366

279367
ex = Application(haadf)
@@ -299,6 +387,9 @@ def SegUI(image):
299387
import hyperspy.api as hs
300388
filename = "Data/JEOL HAADF Image.dm4"
301389
haadf = hs.load(filename)
390+
391+
image_out = np.zeros_like(haadf)
392+
302393
app = QApplication(sys.argv)
303394
app.aboutToQuit.connect(app.deleteLater)
304395

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
with open("README.md", "r") as fh:
44
long_description = fh.read()
55

6-
install_requires=["hyperspy"]
6+
install_requires=["hyperspy",
7+
"scikit-image>=0.15"]
78

89
try:
910
import PyQt5 # noqa

0 commit comments

Comments
 (0)