|
a |
|
b/QuArray.py |
|
|
1 |
# DEPLOY MAC |
|
|
2 |
# sudo pyinstaller -F --windowed -p /Users/callum/callum/QuArray/scripts --add-data "/Users/callum/callum/QuArray/scripts:scripts" --add-binary "/Users/callum/Library/Application Support/pyinstaller/bincache00_py37_64bit/libopenslide.0.dylib:." --icon=scripts/icons/icon.icns QuArray.py |
|
|
3 |
# Windows |
|
|
4 |
# pyinstaller -F --windowed -p C:/Users/callum/QuArray/scripts --add-data "C:/Users/callum/QuArray/scripts;scripts" --icon=scripts/icons/icon.ico QuArray.py |
|
|
5 |
# Linux |
|
|
6 |
# pyinstaller -F -p ~/Documents/QuArray/scripts/ --add-data "scripts:scripts" QuArray.py |
|
|
7 |
|
|
|
8 |
import qdarkgraystyle |
|
|
9 |
from scripts import DABanalysis, Overlay, Cut_Application_thread |
|
|
10 |
import sys |
|
|
11 |
import os |
|
|
12 |
from PyQt5.QtCore import pyqtSlot, Qt, pyqtSignal |
|
|
13 |
from PyQt5.QtWidgets import QFileDialog |
|
|
14 |
from PyQt5 import QtWidgets, QtGui |
|
|
15 |
from PyQt5.QtWidgets import QApplication, QMainWindow |
|
|
16 |
from PyQt5.uic import loadUi |
|
|
17 |
import qimage2ndarray |
|
|
18 |
import random |
|
|
19 |
import numpy as np |
|
|
20 |
from skimage.color import rgb2hsv |
|
|
21 |
from PIL import Image |
|
|
22 |
|
|
|
23 |
|
|
|
24 |
if not hasattr(sys, "_MEIPASS"): |
|
|
25 |
sys._MEIPASS = '.' # for running locally |
|
|
26 |
|
|
|
27 |
class MyWindow(QMainWindow): |
|
|
28 |
def __init__(self): |
|
|
29 |
super(MyWindow, self).__init__() |
|
|
30 |
loadUi(sys._MEIPASS + os.sep + "scripts" + os.sep + "MainWindow_layout.ui", self) # for deployment |
|
|
31 |
self.statusBar() |
|
|
32 |
self.setWindowTitle('QuArray') |
|
|
33 |
self.setWindowOpacity(0.96) |
|
|
34 |
self.ndpi_exporter.clicked.connect(lambda: self.ndpi_export()) |
|
|
35 |
self.trainingbutton.clicked.connect(lambda: self.dabanalysis()) |
|
|
36 |
self.testingbutton.clicked.connect(lambda: self.thresh_action()) |
|
|
37 |
self.overbtn.clicked.connect(lambda: self.overlay()) |
|
|
38 |
self.actionNDPI_SVS_Export.triggered.connect(lambda: self.ndpi_export()) |
|
|
39 |
self.actionRun_Dab_Analysis.triggered.connect(lambda: self.dabanalysis()) |
|
|
40 |
self.actionSet_Thresholds.triggered.connect(lambda: self.thresh_action()) |
|
|
41 |
self.actionOverlay_Figure.triggered.connect(lambda: self.overlay()) |
|
|
42 |
self.saveimages = False |
|
|
43 |
self.checkBox.stateChanged.connect(self.clickbox) |
|
|
44 |
scene = QtWidgets.QGraphicsScene() |
|
|
45 |
self.pixmap = QtWidgets.QGraphicsPixmapItem() |
|
|
46 |
scene.addItem(self.pixmap) |
|
|
47 |
self.graphicsView.setScene(scene) |
|
|
48 |
self.show() |
|
|
49 |
|
|
|
50 |
def ndpi_export(self): # select file(s) button 2 |
|
|
51 |
self.NDPI = Cut_Application_thread.MyWindow() |
|
|
52 |
self.NDPI.show() |
|
|
53 |
|
|
|
54 |
def dabanalysis(self): # select file(s) button 1 |
|
|
55 |
self.statusupdate("Select path containing PNG files") |
|
|
56 |
self.path = QFileDialog.getExistingDirectory(parent=self, caption='Open file', |
|
|
57 |
directory="/Users/callum/Desktop") |
|
|
58 |
if self.path: |
|
|
59 |
self.analysis = DABanalysis.DabAnalysis(path=self.path, save=self.saveimages) |
|
|
60 |
self.analysis.maxcuts.connect(self.progress.setMaximum) |
|
|
61 |
self.analysis.countChanged.connect(self.onCountChanged) |
|
|
62 |
self.analysis.info.connect(self.statusupdate) |
|
|
63 |
self.analysis.figures.connect(self.showimage) |
|
|
64 |
self.analysis.activate.connect(self.activate_input) |
|
|
65 |
if hasattr(self, "newthreshold"): |
|
|
66 |
print("Using new DAB threshold - " + str(self.newthreshold)) |
|
|
67 |
self.analysis.threshold = self.newthreshold / 100 |
|
|
68 |
self.analysis.start() |
|
|
69 |
else: |
|
|
70 |
self.analysis.start() |
|
|
71 |
|
|
|
72 |
def activate_input(self, onoff): |
|
|
73 |
self.trainingbutton.setEnabled(onoff) |
|
|
74 |
self.testingbutton.setEnabled(onoff) |
|
|
75 |
self.overbtn.setEnabled(onoff) |
|
|
76 |
self.checkBox.setEnabled(onoff) |
|
|
77 |
|
|
|
78 |
def statusupdate(self, message): |
|
|
79 |
self.statusbar.showMessage(message) |
|
|
80 |
|
|
|
81 |
def clickbox(self, state): |
|
|
82 |
if state == Qt.Checked: |
|
|
83 |
self.saveimages = True |
|
|
84 |
else: |
|
|
85 |
self.saveimages = False |
|
|
86 |
|
|
|
87 |
@pyqtSlot(int) |
|
|
88 |
def onCountChanged(self, value): |
|
|
89 |
self.progress.setValue(value) |
|
|
90 |
|
|
|
91 |
def showimage(self): |
|
|
92 |
img = qimage2ndarray.array2qimage(self.analysis.current_image, normalize=True) |
|
|
93 |
img = QtGui.QPixmap(img) |
|
|
94 |
self.pixmap.setPixmap(img) |
|
|
95 |
self.graphicsView.fitInView(self.graphicsView.sceneRect(), Qt.KeepAspectRatio) |
|
|
96 |
|
|
|
97 |
def thresh_action(self): # select file(s) button 2 |
|
|
98 |
self.SW = ThresholdSelectorWindow() |
|
|
99 |
self.SW.show() |
|
|
100 |
self.SW.signal.connect(self.updatethresholds) |
|
|
101 |
|
|
|
102 |
def updatethresholds(self, value): |
|
|
103 |
self.newthreshold = value |
|
|
104 |
self.statusupdate("threshold value updated - " + str(value)) |
|
|
105 |
self.testingbutton.setStyleSheet("background-color: rgb(0,90,0)") |
|
|
106 |
|
|
|
107 |
def overlay(self): |
|
|
108 |
self.statusupdate("Select path - This is slow so only a few files if large") |
|
|
109 |
path = QFileDialog.getExistingDirectory(parent=self, caption='Open file', |
|
|
110 |
directory="/Users/callum/Desktop") |
|
|
111 |
if path: |
|
|
112 |
self.overlayfig = Overlay.Overlay(path=path) |
|
|
113 |
self.overlayfig.maxcuts.connect(self.progress.setMaximum) |
|
|
114 |
self.overlayfig.countChanged.connect(self.onCountChanged) |
|
|
115 |
self.overlayfig.info.connect(self.statusupdate) |
|
|
116 |
self.overlayfig.figures.connect(self.showimageoverlay) |
|
|
117 |
if hasattr(self, "newthreshold"): |
|
|
118 |
print("Using new DAB threshold - " + str(self.newthreshold)) |
|
|
119 |
self.overlayfig.threshold = self.newthreshold / 100 |
|
|
120 |
self.overlayfig.start() |
|
|
121 |
else: |
|
|
122 |
self.overlayfig.start() |
|
|
123 |
|
|
|
124 |
def showimageoverlay(self): |
|
|
125 |
img = qimage2ndarray.array2qimage(self.overlayfig.current_image, normalize=True) |
|
|
126 |
img = QtGui.QPixmap(img) |
|
|
127 |
self.pixmap.setPixmap(img) |
|
|
128 |
self.graphicsView.fitInView(self.graphicsView.sceneRect(), Qt.KeepAspectRatio) |
|
|
129 |
|
|
|
130 |
|
|
|
131 |
class ThresholdSelectorWindow(QtWidgets.QWidget): |
|
|
132 |
signal = pyqtSignal(int) |
|
|
133 |
|
|
|
134 |
def __init__(self): |
|
|
135 |
super(ThresholdSelectorWindow, self).__init__() |
|
|
136 |
loadUi(sys._MEIPASS + os.sep + "scripts" + os.sep + "Threshold_selector_Layout.ui", self) |
|
|
137 |
self.slider.setSingleStep(1) |
|
|
138 |
self.slider.valueChanged[int].connect(self.sliderchange) |
|
|
139 |
self.setWindowTitle('Set DAB Thresholds') |
|
|
140 |
self.setWindowOpacity(0.96) |
|
|
141 |
# scene 1 |
|
|
142 |
scene = QtWidgets.QGraphicsScene() |
|
|
143 |
self.pixmap = QtWidgets.QGraphicsPixmapItem() |
|
|
144 |
scene.addItem(self.pixmap) |
|
|
145 |
self.graphicsView.setScene(scene) |
|
|
146 |
# scene 2 |
|
|
147 |
scene_2 = QtWidgets.QGraphicsScene() |
|
|
148 |
self.pixmap_2 = QtWidgets.QGraphicsPixmapItem() |
|
|
149 |
scene_2.addItem(self.pixmap_2) |
|
|
150 |
self.graphicsView_2.setScene(scene_2) |
|
|
151 |
# scene 3 - graphicsView_zoom1 |
|
|
152 |
scene_3 = QtWidgets.QGraphicsScene() |
|
|
153 |
self.pixmap_3 = QtWidgets.QGraphicsPixmapItem() |
|
|
154 |
scene_3.addItem(self.pixmap_3) |
|
|
155 |
self.graphicsView_zoom1.setScene(scene_3) |
|
|
156 |
# scene 4 - graphicsView_zoom2 |
|
|
157 |
scene_4 = QtWidgets.QGraphicsScene() |
|
|
158 |
self.pixmap_4 = QtWidgets.QGraphicsPixmapItem() |
|
|
159 |
scene_4.addItem(self.pixmap_4) |
|
|
160 |
self.graphicsView_zoom2.setScene(scene_4) |
|
|
161 |
self.trainingbutton.clicked.connect(lambda: self.getpath()) |
|
|
162 |
self.imagebutton.clicked.connect(lambda: self.sampleimage()) |
|
|
163 |
self.togglebtn.clicked.connect(lambda: self.toggle()) |
|
|
164 |
self.applybtn.clicked.connect(lambda: self.apply()) |
|
|
165 |
|
|
|
166 |
if not hasattr(self, "path"): |
|
|
167 |
self.imagebutton.setEnabled(False) |
|
|
168 |
self.togglebtn.setEnabled(False) |
|
|
169 |
self.applybtn.setEnabled(False) |
|
|
170 |
self.slider.setVisible(False) |
|
|
171 |
self.slbl.setEnabled(False) |
|
|
172 |
self.dablbl.setEnabled(False) |
|
|
173 |
self.lbl2.setEnabled(False) |
|
|
174 |
self.show() |
|
|
175 |
|
|
|
176 |
def getpath(self): # select file(s) button 1 |
|
|
177 |
self.path = QFileDialog.getExistingDirectory(parent=self, caption='Open file path', |
|
|
178 |
directory="/Users/callum/Desktop") |
|
|
179 |
if self.path: |
|
|
180 |
self.imagebutton.setEnabled(True) |
|
|
181 |
self.togglebtn.setEnabled(True) |
|
|
182 |
self.applybtn.setEnabled(True) |
|
|
183 |
self.slider.setVisible(True) |
|
|
184 |
self.slbl.setEnabled(True) |
|
|
185 |
self.dablbl.setEnabled(True) |
|
|
186 |
self.lbl2.setEnabled(True) |
|
|
187 |
self.lbl.setText(self.path) |
|
|
188 |
self.sampleimage() # get a sample image |
|
|
189 |
|
|
|
190 |
def settext(self, text): |
|
|
191 |
self.lbl2.setText(text) |
|
|
192 |
self.lbl2.repaint() |
|
|
193 |
return |
|
|
194 |
|
|
|
195 |
@pyqtSlot() |
|
|
196 |
def sampleimage(self): |
|
|
197 |
if hasattr(self, "path"): |
|
|
198 |
files = [f for f in os.listdir(self.path) if f.endswith(".png") and not f.endswith("Overlay.png")] |
|
|
199 |
self.randpath = files[random.randint(0, len(files) - 1)] |
|
|
200 |
self.settext(self.randpath) |
|
|
201 |
self.img = np.array(Image.open(self.path + os.sep + self.randpath))[:, :, :3] |
|
|
202 |
center = int(self.img.shape[0] / 2) |
|
|
203 |
self.zoomimg = self.img[center - 256:center + 256, center - 256:center + 256, :] |
|
|
204 |
zoomimg = qimage2ndarray.array2qimage(self.zoomimg, normalize=True) |
|
|
205 |
zoomimg = QtGui.QPixmap(zoomimg) |
|
|
206 |
self.pixmap_4.setPixmap(zoomimg) |
|
|
207 |
self.graphicsView_zoom2.fitInView(self.graphicsView_zoom2.sceneRect(), Qt.KeepAspectRatio) |
|
|
208 |
self.pixmap_3.setPixmap(zoomimg) |
|
|
209 |
self.graphicsView_zoom1.fitInView(self.graphicsView_zoom1.sceneRect(), Qt.KeepAspectRatio) |
|
|
210 |
self.img = self.img[::4, ::4] # todo scale this better |
|
|
211 |
showim = qimage2ndarray.array2qimage(self.img, normalize=True) |
|
|
212 |
showim = QtGui.QPixmap(showim) |
|
|
213 |
self.pixmap_2.setPixmap(showim) |
|
|
214 |
self.graphicsView_2.fitInView(self.graphicsView_2.sceneRect(), Qt.KeepAspectRatio) |
|
|
215 |
# prep for analysis |
|
|
216 |
img_hsv = rgb2hsv(self.img) |
|
|
217 |
self.img_hue = img_hsv[:, :, 0] |
|
|
218 |
self.image_sat = img_hsv[:, :, 1] |
|
|
219 |
img_hsv_zoom = rgb2hsv(self.zoomimg) |
|
|
220 |
self.img_hue_sml = img_hsv_zoom[:, :, 0] |
|
|
221 |
self.image_sat_sml = img_hsv_zoom[:, :, 1] |
|
|
222 |
self.slider.setMaximum((self.image_sat.max()) * 100) |
|
|
223 |
self.slider.setMinimum((self.image_sat.min()) * 100) |
|
|
224 |
self.togimg = qimage2ndarray.array2qimage(self.img, normalize=True) |
|
|
225 |
self.togimg = QtGui.QPixmap(self.togimg) |
|
|
226 |
self.pixmap.setPixmap(self.togimg) |
|
|
227 |
self.graphicsView.fitInView(self.graphicsView.sceneRect(), Qt.KeepAspectRatio) |
|
|
228 |
return |
|
|
229 |
|
|
|
230 |
def resizeEvent(self, event): |
|
|
231 |
self.graphicsView.fitInView(self.graphicsView.sceneRect(), Qt.KeepAspectRatio) |
|
|
232 |
self.graphicsView_2.fitInView(self.graphicsView_2.sceneRect(), Qt.KeepAspectRatio) |
|
|
233 |
self.graphicsView_zoom1.fitInView(self.graphicsView_zoom1.sceneRect(), Qt.KeepAspectRatio) |
|
|
234 |
self.graphicsView_zoom2.fitInView(self.graphicsView_zoom2.sceneRect(), Qt.KeepAspectRatio) |
|
|
235 |
self.show() |
|
|
236 |
|
|
|
237 |
def sliderchange(self, value): |
|
|
238 |
if hasattr(self, "img"): |
|
|
239 |
self.slbl.setText(str(value)) |
|
|
240 |
# todo sort the poor code here |
|
|
241 |
# large image |
|
|
242 |
hue = np.logical_and(self.img_hue > 0.02, self.img_hue < 0.10) # BROWN PIXELS BETWEEN 0.02 and 0.10 |
|
|
243 |
img = np.logical_and(hue, self.image_sat > value / 100) |
|
|
244 |
img = qimage2ndarray.array2qimage(img, normalize=True) |
|
|
245 |
img = QtGui.QPixmap(img) |
|
|
246 |
self.pixmap.setPixmap(img) |
|
|
247 |
self.graphicsView.fitInView(self.graphicsView.sceneRect(), Qt.KeepAspectRatio) |
|
|
248 |
# small image |
|
|
249 |
hue = np.logical_and(self.img_hue_sml > 0.02, self.img_hue_sml < 0.10) # BROWN PIXELS BETWEEN 0.02 and 0.10 |
|
|
250 |
img = np.logical_and(hue, self.image_sat_sml > value / 100) |
|
|
251 |
img = qimage2ndarray.array2qimage(img, normalize=True) |
|
|
252 |
img = QtGui.QPixmap(img) |
|
|
253 |
self.pixmap_3.setPixmap(img) |
|
|
254 |
self.graphicsView_zoom1.fitInView(self.graphicsView_zoom1.sceneRect(), Qt.KeepAspectRatio) |
|
|
255 |
|
|
|
256 |
def toggle(self): |
|
|
257 |
if hasattr(self, "togimg"): |
|
|
258 |
self.pixmap.setPixmap(self.togimg) |
|
|
259 |
self.graphicsView.fitInView(self.graphicsView.sceneRect(), Qt.KeepAspectRatio) |
|
|
260 |
|
|
|
261 |
pyqtSlot() |
|
|
262 |
|
|
|
263 |
def apply(self): |
|
|
264 |
self.signal.emit(int(self.slider.value())) |
|
|
265 |
|
|
|
266 |
|
|
|
267 |
if __name__ == '__main__': |
|
|
268 |
app = QApplication(sys.argv) |
|
|
269 |
window = MyWindow() |
|
|
270 |
|
|
|
271 |
# style sheet - https://github.com/mstuttgart/qdarkgraystyle |
|
|
272 |
app.setStyleSheet(qdarkgraystyle.load_stylesheet()) |
|
|
273 |
|
|
|
274 |
sys.exit(app.exec_()) |