|
a |
|
b/scripts/Cut_Application_thread.py |
|
|
1 |
from PyQt5.uic import loadUi |
|
|
2 |
from PyQt5.QtCore import Qt, QRectF, QThread, QObject, pyqtSignal, pyqtSlot |
|
|
3 |
from PyQt5.QtGui import QColor, QFont, QImage, QPainter, QPixmap, QPen, QBrush |
|
|
4 |
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsItem, QFileDialog, QWidget, QGraphicsPixmapItem |
|
|
5 |
from openslide import OpenSlide |
|
|
6 |
import qimage2ndarray |
|
|
7 |
import numpy as np |
|
|
8 |
import os |
|
|
9 |
from openpyxl import load_workbook |
|
|
10 |
from openpyxl.utils.cell import coordinate_from_string, column_index_from_string |
|
|
11 |
import json |
|
|
12 |
import sys |
|
|
13 |
from skimage.filters import threshold_otsu, threshold_li, threshold_mean, threshold_triangle, gaussian |
|
|
14 |
from skimage.color import rgb2gray |
|
|
15 |
from skimage.morphology import closing, square, remove_small_objects, label |
|
|
16 |
from skimage.measure import regionprops |
|
|
17 |
from skimage.transform import resize |
|
|
18 |
from PIL import Image |
|
|
19 |
|
|
|
20 |
if not hasattr(sys, "_MEIPASS"): |
|
|
21 |
sys._MEIPASS = '.' # for running locally |
|
|
22 |
|
|
|
23 |
# setup the Graphics scene to detect clicks |
|
|
24 |
class GraphicsScene(QGraphicsScene): |
|
|
25 |
def __init__(self, parent=None): |
|
|
26 |
QGraphicsScene.__init__(self, parent) |
|
|
27 |
self.coords = [] |
|
|
28 |
self.circles = [] |
|
|
29 |
self.rectsandtext = [] |
|
|
30 |
self.parent = MyWindow |
|
|
31 |
|
|
|
32 |
def reset(self): |
|
|
33 |
[self.circles[i].setVisible(False) for i in range(len(self.circles))] |
|
|
34 |
self.coords = [] |
|
|
35 |
self.circles = [] |
|
|
36 |
self.rectsandtext = [] |
|
|
37 |
|
|
|
38 |
def elipse_adder(self, x, y): |
|
|
39 |
pen = QPen(QColor(69, 130, 201, 200)) # QColor(128, 128, 255, 128) |
|
|
40 |
pen.setWidthF(10) # border width |
|
|
41 |
brush = QBrush(Qt.transparent) |
|
|
42 |
elipse = self.addEllipse(x - 25, y - 25, 50, 50, pen, brush) |
|
|
43 |
elipse.setAcceptDrops(True) |
|
|
44 |
elipse.setCursor(Qt.OpenHandCursor) |
|
|
45 |
elipse.setFlag(QGraphicsItem.ItemIsSelectable, True) |
|
|
46 |
elipse.setFlag(QGraphicsItem.ItemIsMovable, True) |
|
|
47 |
elipse.setFlag(QGraphicsItem.ItemIsFocusable, True) |
|
|
48 |
elipse.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) |
|
|
49 |
elipse.setAcceptHoverEvents(True) |
|
|
50 |
self.circles.append(elipse) |
|
|
51 |
|
|
|
52 |
def mouseDoubleClickEvent(self, event): |
|
|
53 |
x = event.scenePos().x() |
|
|
54 |
y = event.scenePos().y() |
|
|
55 |
btn = event.button() |
|
|
56 |
if btn == 1: # left click |
|
|
57 |
self.coords.append((x, y)) |
|
|
58 |
self.elipse_adder(x, y) |
|
|
59 |
|
|
|
60 |
def keyPressEvent(self, e): # hit space to remove point |
|
|
61 |
if e.key() == Qt.Key_Space: |
|
|
62 |
if len(self.circles) >= 1: |
|
|
63 |
self.circles[-1].setVisible(False) |
|
|
64 |
self.circles = self.circles[:-1] |
|
|
65 |
self.coords = self.coords[:-1] |
|
|
66 |
|
|
|
67 |
def sortCentroid(self, centroid): |
|
|
68 |
scent = sorted(centroid, key=lambda x: x[0]) |
|
|
69 |
sortList = [] |
|
|
70 |
a = 0 |
|
|
71 |
comLength = 0 |
|
|
72 |
for length in self.rowcount: |
|
|
73 |
comLength = comLength + length |
|
|
74 |
sortList.extend((sorted(scent[a:comLength], key=lambda k: [k[1]]))) |
|
|
75 |
a = a + length |
|
|
76 |
return sortList |
|
|
77 |
|
|
|
78 |
def overlay_cores(self, core_diameter, scale_index, cores, autopilot=False): # removed - centroid, image, cores |
|
|
79 |
if len(self.rectsandtext) >= 1: |
|
|
80 |
[i.setVisible(False) for i in self.rectsandtext] |
|
|
81 |
self.rectsandtext = [] |
|
|
82 |
pen = QPen(QColor(69, 130, 201, 240)) |
|
|
83 |
pen.setWidthF(6) # border width |
|
|
84 |
brush = QBrush(QColor(215, 230, 248, 160)) # square fill |
|
|
85 |
if autopilot: |
|
|
86 |
self.centroid = self.coords |
|
|
87 |
self.centroid = self.sortCentroid(self.centroid) |
|
|
88 |
[self.elipse_adder(y, x) for (x, y) in self.centroid] |
|
|
89 |
self.coords = [(y, x) for (x, y) in self.centroid] |
|
|
90 |
else: |
|
|
91 |
self.centroid = [(y, x) for (x, y) in self.coords] |
|
|
92 |
self.centroid = [(self.centroid[i][0]+self.circles[i].scenePos().y(), self.centroid[i][1]+self.circles[i].scenePos().x()) for i in range(len(self.circles))] |
|
|
93 |
diameter = core_diameter / scale_index |
|
|
94 |
a = 0 |
|
|
95 |
for y, x in self.centroid: |
|
|
96 |
try: |
|
|
97 |
rect = self.addRect((x - (diameter / 2)), (y - (diameter / 2)), diameter, diameter, pen, brush) |
|
|
98 |
text = self.addText(cores[a]) # label |
|
|
99 |
self.rectsandtext.append(rect) |
|
|
100 |
self.rectsandtext.append(text) |
|
|
101 |
text.setPos(x, y) |
|
|
102 |
text.setDefaultTextColor(QColor(35, 57, 82, 200)) |
|
|
103 |
font = QFont() |
|
|
104 |
font.setPointSize(80) |
|
|
105 |
text.setFont(font) |
|
|
106 |
a = a + 1 |
|
|
107 |
except IndexError as e: |
|
|
108 |
self.centroid.pop(a) |
|
|
109 |
print("index error", self.centroid[a]) |
|
|
110 |
continue |
|
|
111 |
|
|
|
112 |
def save(self, output, name): |
|
|
113 |
# Get region of scene to capture from somewhere. |
|
|
114 |
area = self.sceneRect().size().toSize() |
|
|
115 |
image = QImage(area, QImage.Format_RGB888) |
|
|
116 |
painter = QPainter(image) |
|
|
117 |
self.render(painter, target=QRectF(image.rect()), source=self.sceneRect()) |
|
|
118 |
painter.end() |
|
|
119 |
image.save(output + os.sep + name + "_overlay" + ".tiff") |
|
|
120 |
|
|
|
121 |
|
|
|
122 |
class MyWindow(QWidget): |
|
|
123 |
def __init__(self): |
|
|
124 |
super(MyWindow, self).__init__() |
|
|
125 |
loadUi(sys._MEIPASS + os.sep + "scripts" + os.sep + "Cut_Application_thread_layout.ui", self) # deployment |
|
|
126 |
self.setWindowTitle('QuArray') |
|
|
127 |
self.tabWidget.setStyleSheet("QTabWidget::pane {margin: 0px,0px,0px,0px; padding: 0px}") |
|
|
128 |
self.excel_layout = self.excel_btn.isChecked() |
|
|
129 |
self.excel_btn.toggled.connect(self.excel) |
|
|
130 |
self.load_ndpi.clicked.connect(lambda: self.loadndpi()) |
|
|
131 |
self.load_excel.clicked.connect(lambda: self.read_excel()) |
|
|
132 |
self.overlay.clicked.connect(lambda: self.overlaystart()) |
|
|
133 |
self.export_2.clicked.connect(lambda: self.export_images()) |
|
|
134 |
# self.export_again.clicked.connect(lambda: self.export_images(meta_only=True)) |
|
|
135 |
self.current_image = None |
|
|
136 |
# threshold buttons |
|
|
137 |
self.gausianval = 0 |
|
|
138 |
self.thresholdval = None |
|
|
139 |
self.otsu.clicked.connect(lambda: [self.threshold("otsu"), self.reset_sliders()]) |
|
|
140 |
self.threshmean.clicked.connect(lambda: [self.threshold("mean"), self.reset_sliders()]) |
|
|
141 |
self.threshtriangle.clicked.connect(lambda: [self.threshold("triangle"), self.reset_sliders()]) |
|
|
142 |
self.threshli.clicked.connect(lambda: [self.threshold("li"), self.reset_sliders()]) |
|
|
143 |
self.toggleorigional.clicked.connect(lambda: [self.threshold("origional"), self.reset_sliders()]) |
|
|
144 |
self.gausslider.setMaximum(5) |
|
|
145 |
self.gausslider.setValue(0) |
|
|
146 |
self.gausslider.valueChanged.connect(lambda: self.gauslineEdit.setText(str(self.gausslider.value()))) # change |
|
|
147 |
self.gausslider.sliderReleased.connect(self.gaus) |
|
|
148 |
self.closingslider.setMaximum(50) |
|
|
149 |
self.closingslider.setValue(0) |
|
|
150 |
self.closingslider.valueChanged.connect(lambda: self.closelineEdit.setText(str(self.closingslider.value()))) |
|
|
151 |
self.closingslider.sliderReleased.connect(self.closing) |
|
|
152 |
self.removesmallobjects.clicked.connect(self.removesmall) |
|
|
153 |
self.current_augments = {"threshold": False, "gausian": False, "closing": False, "overlay_applied": False, |
|
|
154 |
"manual_overlay": False} |
|
|
155 |
self.pathology = None |
|
|
156 |
self.excelpath = False |
|
|
157 |
|
|
|
158 |
self.init_scene() |
|
|
159 |
self.show() |
|
|
160 |
|
|
|
161 |
def overlaystart(self, autopilot=False, coords=None): |
|
|
162 |
""" |
|
|
163 |
starts overlay |
|
|
164 |
:param autopilot: if the coords need to be asigned outside of the click function |
|
|
165 |
:param coords: the external coords to be applied in autopilot |
|
|
166 |
""" |
|
|
167 |
try: |
|
|
168 |
self.core_diameter = int(self.diamiterLineEdit.text().strip()) |
|
|
169 |
except: |
|
|
170 |
self.core_diameter = 6000 |
|
|
171 |
self.diamiterLineEdit.setText('6000') |
|
|
172 |
self.info("core diamiter must be an integer - reset to 6000") |
|
|
173 |
pass |
|
|
174 |
if autopilot: |
|
|
175 |
self.scene.coords = coords |
|
|
176 |
self.scene.overlay_cores(self.core_diameter, self.scale_index, self.cores, autopilot=True) |
|
|
177 |
else: |
|
|
178 |
self.scene.overlay_cores(self.core_diameter, self.scale_index, self.cores) |
|
|
179 |
if self.overlaySave.isChecked(): |
|
|
180 |
self.info(f"Overlay saved to - {self.output}") |
|
|
181 |
self.scene.save(self.output, self.name) |
|
|
182 |
self.current_augments["overlay_applied"] = True |
|
|
183 |
self.activate([self.export_2], action=True) |
|
|
184 |
|
|
|
185 |
def excel(self, x): |
|
|
186 |
self.excel_layout = x |
|
|
187 |
if self.excelpath: |
|
|
188 |
self.read_excel() |
|
|
189 |
|
|
|
190 |
def init_scene(self): |
|
|
191 |
self.scene = GraphicsScene(self) |
|
|
192 |
self.graphicsView.setScene(self.scene) |
|
|
193 |
self.pixmap = QGraphicsPixmapItem() |
|
|
194 |
self.scene.addItem(self.pixmap) |
|
|
195 |
|
|
|
196 |
def show_info(self, text): |
|
|
197 |
self.metadata.setText(text) |
|
|
198 |
|
|
|
199 |
def activate(self, names, action=True): |
|
|
200 |
for i in names: |
|
|
201 |
i.setEnabled(action) |
|
|
202 |
|
|
|
203 |
def loadndpi(self): |
|
|
204 |
self.init_scene() |
|
|
205 |
formats = '*.ndpi*;;*.svs*;;*.tif*;;*.scn*;;*.mrxs*;;*.tiff*;;*.svslide*;;*.bif*' |
|
|
206 |
self.path, _ = QFileDialog.getOpenFileName(parent=self, caption='Open file', |
|
|
207 |
directory="/Users/callum/Desktop/", filter=formats) |
|
|
208 |
if self.path: |
|
|
209 |
self.output = os.path.splitext(self.path)[0] + '_split' |
|
|
210 |
if not os.path.exists(self.output): # make output directory |
|
|
211 |
os.mkdir(self.output) |
|
|
212 |
|
|
|
213 |
self.name = os.path.split(self.output)[-1] |
|
|
214 |
self.nameLineEdit.setText(self.name) |
|
|
215 |
self.load_ndpi.setStyleSheet("background-color: rgb(0,90,0)") |
|
|
216 |
try: |
|
|
217 |
self.image = OpenSlide(self.path) |
|
|
218 |
except Exception as e: |
|
|
219 |
self.loadndpi() |
|
|
220 |
print(self.path + ' read to memory') |
|
|
221 |
print(' slide format = ' + str(OpenSlide.detect_format(self.path))) |
|
|
222 |
if str(OpenSlide.detect_format(self.path)) == "aperio": |
|
|
223 |
try: |
|
|
224 |
print(' Magnification = ' + str(self.image.properties['openslide.objective-power'])) # TODO |
|
|
225 |
print(' Date = ' + str(self.image.properties['aperio.Date'])) |
|
|
226 |
print(' dimensions = ' + str(self.image.dimensions)) |
|
|
227 |
print(' level_downsamples = ' + str(self.image.level_downsamples)) |
|
|
228 |
except KeyError: |
|
|
229 |
pass |
|
|
230 |
if str(OpenSlide.detect_format(self.path)) == "hamamatsu": |
|
|
231 |
try: |
|
|
232 |
self.formatLineEdit.setText("Hamamatsu") |
|
|
233 |
self.scanDateLineEdit.setText(str(self.image.properties['tiff.DateTime'][:10])) |
|
|
234 |
self.dimensionsLineEdit.setText(str(self.image.dimensions)) |
|
|
235 |
self.magnificationLineEdit.setText(str(self.image.properties['hamamatsu.SourceLens'])) |
|
|
236 |
self.show_info(f"""Magnification = {str(self.image.properties['hamamatsu.SourceLens'])} |
|
|
237 |
Date = {str(self.image.properties['tiff.DateTime'])}\ndimensions = {str(self.image.dimensions)} |
|
|
238 |
level_downsamples = {str(self.image.level_downsamples)}""") |
|
|
239 |
print(' Magnification = ' + str(self.image.properties['hamamatsu.SourceLens'])) |
|
|
240 |
print(' Date = ' + str(self.image.properties['tiff.DateTime'])) |
|
|
241 |
print(' dimensions = ' + str(self.image.dimensions)) |
|
|
242 |
print(' level_downsamples = ' + str(self.image.level_downsamples)) |
|
|
243 |
self.macro_image = self.image.associated_images['macro'] |
|
|
244 |
except KeyError: |
|
|
245 |
pass |
|
|
246 |
self.overview_level_width = 3000 |
|
|
247 |
self.activate([self.nameLabel, self.nameLineEdit, self.formatLabel, self.formatLineEdit, |
|
|
248 |
self.magnificationLabel, self.magnificationLineEdit, self.scanDateLabel, |
|
|
249 |
self.scanDateLineEdit, self.dimensionsLabel, self.dimensionsLineEdit, self.overlayLevelLabel, |
|
|
250 |
self.overlayLevelLineEdit, self.graphicsView, self.overlaySave, self.groupBox_2, |
|
|
251 |
self.removesmallobjects]) |
|
|
252 |
self.get_overview() |
|
|
253 |
if os.path.exists(os.path.splitext(self.path)[0] + '.xlsx'): |
|
|
254 |
self.excelpath = os.path.splitext(self.path)[0] + '.xlsx' |
|
|
255 |
self.read_excel() |
|
|
256 |
else: |
|
|
257 |
self.excelpath = False |
|
|
258 |
|
|
|
259 |
def get_overview(self): |
|
|
260 |
self.width_height = [ |
|
|
261 |
(int(self.image.properties[f'openslide.level[{i}].width']), |
|
|
262 |
int(self.image.properties[f'openslide.level[{i}].height'])) for i |
|
|
263 |
in range(int(self.image.properties['openslide.level-count']))] |
|
|
264 |
width = [self.width_height[i][0] for i in range(len(self.width_height))] |
|
|
265 |
self.lvl = np.where(width == self.find_nearest(width, self.overview_level_width))[0][0] |
|
|
266 |
self.overlayLevelLineEdit.setText(str(self.lvl)) |
|
|
267 |
self.scale_index = self.width_height[0][0] / self.width_height[self.lvl][0] |
|
|
268 |
self.overview = np.array(self.image.read_region(location=(0, 0), level=self.lvl, |
|
|
269 |
size=self.width_height[self.lvl])) |
|
|
270 |
self.showimage(image=self.overview) |
|
|
271 |
|
|
|
272 |
def find_nearest(self, array, value): |
|
|
273 |
# https://stackoverflow.com/questions/2566412/find-nearest-value-in-numpy-array |
|
|
274 |
array = np.asarray(array) |
|
|
275 |
idx = (np.abs(array - value)).argmin() |
|
|
276 |
return array[idx] |
|
|
277 |
|
|
|
278 |
def showimage(self, image): |
|
|
279 |
self.current_image = image |
|
|
280 |
img = qimage2ndarray.array2qimage(image, normalize=True) |
|
|
281 |
img = QPixmap(img) |
|
|
282 |
self.pixmap.setPixmap(img) |
|
|
283 |
self.graphicsView.fitInView(self.graphicsView.sceneRect(), Qt.KeepAspectRatio) |
|
|
284 |
|
|
|
285 |
def reset_sliders(self): |
|
|
286 |
self.gausslider.setValue(0) |
|
|
287 |
self.closingslider.setValue(0) |
|
|
288 |
|
|
|
289 |
def threshold(self, threshold_name): |
|
|
290 |
if self.scene.circles: |
|
|
291 |
self.scene.reset() |
|
|
292 |
if self.current_augments['overlay_applied']: |
|
|
293 |
del self.scene.coords |
|
|
294 |
self.init_scene() |
|
|
295 |
self.read_excel() |
|
|
296 |
self.current_augments['overlay_applied'] = False |
|
|
297 |
if threshold_name == "origional": |
|
|
298 |
self.showimage(self.overview) |
|
|
299 |
self.thresholdval = None |
|
|
300 |
self.current_augments["threshold"] = False |
|
|
301 |
return |
|
|
302 |
self.thresholdval = threshold_name |
|
|
303 |
|
|
|
304 |
im = rgb2gray(self.overview) |
|
|
305 |
if threshold_name == "otsu": |
|
|
306 |
threshold = threshold_otsu(im) |
|
|
307 |
if threshold_name == "li": |
|
|
308 |
threshold = threshold_li(im) |
|
|
309 |
if threshold_name == "mean": |
|
|
310 |
threshold = threshold_mean(im) |
|
|
311 |
if threshold_name == "triangle": |
|
|
312 |
threshold = threshold_triangle(im) |
|
|
313 |
self.current_augments["threshold"] = threshold_name |
|
|
314 |
self.current_image = im < threshold |
|
|
315 |
self.showimage(self.current_image) |
|
|
316 |
|
|
|
317 |
def gaus(self): |
|
|
318 |
self.gauslineEdit.setText(str(self.gausslider.value())) |
|
|
319 |
if self.current_image.ndim > 2: |
|
|
320 |
filtered = gaussian(self.overview, sigma=self.gausslider.value()) |
|
|
321 |
else: |
|
|
322 |
self.threshold(self.current_augments["threshold"]) |
|
|
323 |
filtered = gaussian(self.current_image, sigma=self.gausslider.value()) |
|
|
324 |
self.showimage(filtered) |
|
|
325 |
|
|
|
326 |
def closing(self): |
|
|
327 |
self.closelineEdit.setText(str(self.closingslider.value())) |
|
|
328 |
if self.current_augments["threshold"]: |
|
|
329 |
self.threshold(self.current_augments["threshold"]) |
|
|
330 |
if self.closingslider.value() > 0: |
|
|
331 |
if self.current_augments["gausian"]: |
|
|
332 |
self.current_image = gaussian(self.current_image, sigma=self.gausslider.value()) |
|
|
333 |
closed = closing(self.current_image, square(self.closingslider.value())) |
|
|
334 |
closed = closed > 0 |
|
|
335 |
self.showimage(closed) |
|
|
336 |
|
|
|
337 |
def removesmall(self): |
|
|
338 |
""" |
|
|
339 |
the function name is misleading as this function now applies remove small objects - then |
|
|
340 |
overlays the cores and saves the image |
|
|
341 |
""" |
|
|
342 |
if not self.current_augments['threshold']: |
|
|
343 |
return |
|
|
344 |
if self.current_augments['overlay_applied']: |
|
|
345 |
self.init_scene() |
|
|
346 |
self.read_excel() |
|
|
347 |
labeled_image = label(self.current_image) |
|
|
348 |
try: |
|
|
349 |
min = int(self.smallobs_text.text()) |
|
|
350 |
except ValueError as e: |
|
|
351 |
self.smallobs_text.setText("6000") |
|
|
352 |
min = 6000 |
|
|
353 |
self.info("remove small value must be an integer - reset to 6000") |
|
|
354 |
labeled_image = remove_small_objects(labeled_image, min_size=min) |
|
|
355 |
self.showimage(labeled_image) |
|
|
356 |
labels = regionprops(labeled_image) |
|
|
357 |
centroid = [r.centroid for r in labels] |
|
|
358 |
self.overlaystart(autopilot=True, coords=centroid) |
|
|
359 |
self.current_augments['overlay_applied'] = True |
|
|
360 |
|
|
|
361 |
def read_excel(self): |
|
|
362 |
if not self.excelpath: |
|
|
363 |
self.excelpath, _ = QFileDialog.getOpenFileName(parent=self, caption='Open file', |
|
|
364 |
directory="/Users/callum/Desktop", filter="*.xlsx*") |
|
|
365 |
self.activate([self.numberOfCoresLabel, self.numberOfCoresLineEdit, self.diameterLabel, self.diamiterLineEdit, |
|
|
366 |
self.overlay, self.progressBar, self.excel_btn, self.overlaySave, self.tabWidget]) |
|
|
367 |
if self.excelpath: |
|
|
368 |
self.load_excel.setStyleSheet("background-color: rgb(0,90,0)") |
|
|
369 |
excelname = self.excelpath |
|
|
370 |
wb = load_workbook(excelname) |
|
|
371 |
ws = wb.worksheets[0] |
|
|
372 |
cores = [] |
|
|
373 |
values = [] |
|
|
374 |
rowcount = [] |
|
|
375 |
self.rowcol = [] |
|
|
376 |
if not self.excel_layout: |
|
|
377 |
for row in ws.iter_rows(): |
|
|
378 |
for cell in row: |
|
|
379 |
values.append(cell.value) |
|
|
380 |
if cell.value == 1: |
|
|
381 |
cores.append((str(chr(int(cell.row) + 64))) + str(int(ord(cell.column_letter)) - 64)) |
|
|
382 |
else: |
|
|
383 |
cores = [] |
|
|
384 |
for col in ws.iter_cols(): |
|
|
385 |
for cell in col: |
|
|
386 |
if cell.value == 1: |
|
|
387 |
cores.append(cell.coordinate) # get core names if contain a 1 |
|
|
388 |
cores.sort(key=lambda x: x[1:]) |
|
|
389 |
for row in ws.iter_rows(): |
|
|
390 |
for cell in row: |
|
|
391 |
values.append(cell.value) |
|
|
392 |
values = np.array_split(values, ws.max_row) |
|
|
393 |
for row in values: |
|
|
394 |
rowcount.append(np.count_nonzero(row)) # EXCEL END |
|
|
395 |
self.numberOfCoresLineEdit.setText(str(len(cores))) |
|
|
396 |
self.cores = cores |
|
|
397 |
self.values = values |
|
|
398 |
self.rowcount = rowcount |
|
|
399 |
self.arrayshape = (ws.max_row, ws.max_column) |
|
|
400 |
self.scene.rowcount = rowcount |
|
|
401 |
# check for pathology file |
|
|
402 |
if len(wb.sheetnames) > 1: |
|
|
403 |
ws = wb.worksheets[1] # pathology tab (hopefully) |
|
|
404 |
self.pathology = [ws[i].value for i in self.cores] |
|
|
405 |
|
|
|
406 |
def info(self, text): |
|
|
407 |
self.label.setText(text) |
|
|
408 |
|
|
|
409 |
def export_images(self, meta_only=False): |
|
|
410 |
if not self.scene.centroid: |
|
|
411 |
self.info("must overlay some selected core - double click on image") |
|
|
412 |
return |
|
|
413 |
self.progressBar.setMaximum(len(self.scene.centroid)) |
|
|
414 |
self.activate([self.nameLabel, self.nameLineEdit, self.formatLabel, self.formatLineEdit, |
|
|
415 |
self.magnificationLabel, self.magnificationLineEdit, self.scanDateLabel, |
|
|
416 |
self.scanDateLineEdit, self.dimensionsLabel, self.dimensionsLineEdit, self.overlayLevelLabel, |
|
|
417 |
self.overlayLevelLineEdit, self.graphicsView, self.numberOfCoresLabel, |
|
|
418 |
self.numberOfCoresLineEdit, self.diameterLabel, self.diamiterLineEdit, |
|
|
419 |
self.export_2, self.overlay, self.excel_btn, self.load_ndpi, self.load_excel, self.overlaySave, |
|
|
420 |
self.tabWidget], action=False) |
|
|
421 |
try: |
|
|
422 |
resolution = int(self.resolution_edit.text()) |
|
|
423 |
print("new resolution applied") |
|
|
424 |
except ValueError as E: |
|
|
425 |
resolution = 0 |
|
|
426 |
|
|
|
427 |
self.export = Export(image=self.image, centroid=self.scene.centroid, cores=self.cores, |
|
|
428 |
scale_index=self.scale_index, |
|
|
429 |
core_diameter=self.core_diameter, output=self.output, name=self.name, lvl=self.lvl, |
|
|
430 |
path=self.path, arrayshape=self.arrayshape, pathology=self.pathology, resolution=resolution, window=self, |
|
|
431 |
meta_only=meta_only) |
|
|
432 |
|
|
|
433 |
self.thread = QThread() |
|
|
434 |
self.export.info.connect(self.info) |
|
|
435 |
self.export.done.connect(self.complete) |
|
|
436 |
self.export.countChanged.connect(self.progressBar.setValue) |
|
|
437 |
self.export.moveToThread(self.thread) |
|
|
438 |
self.thread.started.connect(self.export.run) |
|
|
439 |
self.thread.start() |
|
|
440 |
|
|
|
441 |
def complete(self): |
|
|
442 |
print('done') |
|
|
443 |
self.activate([self.nameLabel, self.nameLineEdit, self.formatLabel, self.formatLineEdit, |
|
|
444 |
self.magnificationLabel, self.magnificationLineEdit, self.scanDateLabel, |
|
|
445 |
self.scanDateLineEdit, self.dimensionsLabel, self.dimensionsLineEdit, self.overlayLevelLabel, |
|
|
446 |
self.overlayLevelLineEdit, self.graphicsView, self.numberOfCoresLabel, |
|
|
447 |
self.numberOfCoresLineEdit, self.diameterLabel, self.diamiterLineEdit, |
|
|
448 |
self.export_2, self.overlay, self.excel_btn, self.load_ndpi, self.load_excel, self.overlaySave], |
|
|
449 |
action=True) |
|
|
450 |
|
|
|
451 |
|
|
|
452 |
class Export(QObject): |
|
|
453 |
info = pyqtSignal(str) |
|
|
454 |
countChanged = pyqtSignal(int) |
|
|
455 |
figures = pyqtSignal() |
|
|
456 |
done = pyqtSignal(bool) |
|
|
457 |
writemeta = pyqtSignal(bool) |
|
|
458 |
|
|
|
459 |
def __init__(self, image, centroid, cores, scale_index, core_diameter, output, name, lvl, path, arrayshape, |
|
|
460 |
pathology, resolution, window, meta_only=False): |
|
|
461 |
super().__init__() |
|
|
462 |
self.image = image |
|
|
463 |
self.centroid = centroid |
|
|
464 |
self.cores = cores |
|
|
465 |
self.scale_index = scale_index |
|
|
466 |
self.core_diameter = core_diameter |
|
|
467 |
self.output = output |
|
|
468 |
self.name = name |
|
|
469 |
self.lvl = lvl |
|
|
470 |
self.path = path |
|
|
471 |
self.arrayshape = arrayshape |
|
|
472 |
self.pathology = pathology |
|
|
473 |
self.resolution = resolution |
|
|
474 |
self.meta_only = meta_only |
|
|
475 |
self.win = window |
|
|
476 |
|
|
|
477 |
@pyqtSlot() |
|
|
478 |
def run(self): |
|
|
479 |
if self.meta_only: |
|
|
480 |
self.json_write() |
|
|
481 |
self.wsifigure(higher_resolution=False, pathology=self.pathology) |
|
|
482 |
else: |
|
|
483 |
self.export_images(self.centroid, self.cores) |
|
|
484 |
|
|
|
485 |
def export_images(self, centroid, cores): |
|
|
486 |
infostr = [] |
|
|
487 |
self.scaledcent = [(y * self.scale_index, x * self.scale_index) for (x, y) in centroid] # rotate xy openslide |
|
|
488 |
self.scaledcent = [(int(x - (self.core_diameter / 2)), int(y - (self.core_diameter / 2))) for (x, y) in self.scaledcent] |
|
|
489 |
self.json_write() |
|
|
490 |
self.wsifigure(higher_resolution=False, pathology=self.pathology) |
|
|
491 |
w_h = (self.core_diameter, self.core_diameter) |
|
|
492 |
self.lvl = 0 |
|
|
493 |
for i in range(len(self.scaledcent)): |
|
|
494 |
if not self.win.isVisible(): |
|
|
495 |
print("need to kill here - main window closed") |
|
|
496 |
break |
|
|
497 |
infostr.append("Exporting " + self.name + "_" + cores[i] + ".png") |
|
|
498 |
print("Exporting " + self.name + "_" + cores[i] + ".png") |
|
|
499 |
self.info.emit('\n'.join(infostr)) |
|
|
500 |
core = self.image.read_region(location=self.scaledcent[i], level=self.lvl, size=w_h) |
|
|
501 |
core.save(self.output + os.sep + self.name + "_" + cores[i] + ".png") |
|
|
502 |
infostr.append("Saved " + self.name + "_" + cores[i] + ".png") |
|
|
503 |
self.info.emit('\n'.join(infostr)) |
|
|
504 |
self.countChanged.emit(i + 1) |
|
|
505 |
|
|
|
506 |
infostr.append("All files exported with JSON metadata") |
|
|
507 |
self.info.emit('\n'.join(infostr)) |
|
|
508 |
print('\n'.join(infostr)) |
|
|
509 |
self.done.emit(True) |
|
|
510 |
|
|
|
511 |
def json_write(self): |
|
|
512 |
jsondata = {"path": self.path, "coordinates": self.scaledcent, "cores": self.cores, |
|
|
513 |
"diameter": self.core_diameter, "scale_index": self.scale_index, "lowlevel": int(self.lvl), |
|
|
514 |
"arrayshape": self.arrayshape} |
|
|
515 |
self.info.emit('Saving ' + self.output + os.sep + self.name + '_metadata.json') |
|
|
516 |
with open(self.output + os.sep + self.name + '_metadata.json', "w") as write_file: |
|
|
517 |
json.dump(jsondata, write_file) |
|
|
518 |
|
|
|
519 |
def wsifigure(self, higher_resolution=False, pathology=None): |
|
|
520 |
""" |
|
|
521 |
TODO: need to link this better to the actual script - rather than the meta file before its created |
|
|
522 |
todo: have removed button export_again for function later |
|
|
523 |
takes the metadata from the json |
|
|
524 |
makes a fig of the locations on the tissue array and saves it |
|
|
525 |
higher resolution = int start at 1 and move up to improve resolution - will slow code |
|
|
526 |
""" |
|
|
527 |
higher_resolution = self.resolution |
|
|
528 |
|
|
|
529 |
def overly_path(im, overlay, pathology): |
|
|
530 |
if pathology == "N": |
|
|
531 |
colour = [0, 1, 0] # green |
|
|
532 |
else: |
|
|
533 |
colour = [1, 0, 0] # red |
|
|
534 |
im[overlay == 0] = colour |
|
|
535 |
return im |
|
|
536 |
|
|
|
537 |
jsonpath = self.output + os.sep + self.name + '_metadata.json' |
|
|
538 |
with open(jsonpath) as json_file: |
|
|
539 |
data = json.load(json_file) |
|
|
540 |
image = OpenSlide(data['path']) |
|
|
541 |
if higher_resolution: |
|
|
542 |
lvl = data['lowlevel'] - int(higher_resolution) |
|
|
543 |
else: |
|
|
544 |
lvl = data['lowlevel'] |
|
|
545 |
scale_index = image.level_downsamples[lvl] |
|
|
546 |
diameter = int(data["diameter"] / scale_index) |
|
|
547 |
arrayshape = data['arrayshape'] |
|
|
548 |
arrayshape = [(a * diameter) + 1 for a in arrayshape] |
|
|
549 |
arrayshape.append(3) |
|
|
550 |
figarray = np.ones(arrayshape) |
|
|
551 |
cindex = [coordinate_from_string(i) for i in data['cores']] # returns ('A',4) |
|
|
552 |
cindex = [(b, column_index_from_string(a)) for (a, b) in cindex] # returns index tuples for each core |
|
|
553 |
maskcoord = [[y * diameter - diameter if y > 1 else y for y in x] for x in cindex] |
|
|
554 |
|
|
|
555 |
if pathology: |
|
|
556 |
overlay = np.load(sys._MEIPASS + os.sep + "scripts" + os.sep + "outline.npy") |
|
|
557 |
overlay = resize(overlay, (diameter, diameter)) |
|
|
558 |
|
|
|
559 |
for i in range(len(data["coordinates"])): # iterate through coordinates pulling out images from wsi |
|
|
560 |
im = image.read_region(location=data["coordinates"][i], level=lvl, size=(diameter, diameter)) |
|
|
561 |
im = np.array(im)[:, :, :3] |
|
|
562 |
if pathology: |
|
|
563 |
try: |
|
|
564 |
im = overly_path(im, overlay, pathology[i]) |
|
|
565 |
except: |
|
|
566 |
continue |
|
|
567 |
figarray[maskcoord[i][0]:maskcoord[i][0] + diameter, maskcoord[i][1]:maskcoord[i][1] + diameter, :] = im |
|
|
568 |
figarray[figarray == [1, 1, 1]] = 255 # make background white |
|
|
569 |
savepath = self.output + os.sep + self.name + '_layoutfig.tiff' |
|
|
570 |
Image.fromarray(figarray.astype(np.uint8)).save(savepath) |