Switch to side-by-side view

--- a
+++ b/scripts/Cut_Application_thread.py
@@ -0,0 +1,570 @@
+from PyQt5.uic import loadUi
+from PyQt5.QtCore import Qt, QRectF, QThread, QObject, pyqtSignal, pyqtSlot
+from PyQt5.QtGui import QColor, QFont, QImage, QPainter, QPixmap, QPen, QBrush
+from PyQt5.QtWidgets import QGraphicsScene, QGraphicsItem, QFileDialog, QWidget, QGraphicsPixmapItem
+from openslide import OpenSlide
+import qimage2ndarray
+import numpy as np
+import os
+from openpyxl import load_workbook
+from openpyxl.utils.cell import coordinate_from_string, column_index_from_string
+import json
+import sys
+from skimage.filters import threshold_otsu, threshold_li, threshold_mean, threshold_triangle, gaussian
+from skimage.color import rgb2gray
+from skimage.morphology import closing, square, remove_small_objects, label
+from skimage.measure import regionprops
+from skimage.transform import resize
+from PIL import Image
+
+if not hasattr(sys, "_MEIPASS"):
+    sys._MEIPASS = '.'  # for running locally
+
+# setup the Graphics scene to detect clicks
+class GraphicsScene(QGraphicsScene):
+    def __init__(self, parent=None):
+        QGraphicsScene.__init__(self, parent)
+        self.coords = []
+        self.circles = []
+        self.rectsandtext = []
+        self.parent = MyWindow
+
+    def reset(self):
+        [self.circles[i].setVisible(False) for i in range(len(self.circles))]
+        self.coords = []
+        self.circles = []
+        self.rectsandtext = []
+
+    def elipse_adder(self, x, y):
+        pen = QPen(QColor(69, 130, 201, 200))  # QColor(128, 128, 255, 128)
+        pen.setWidthF(10)  # border width
+        brush = QBrush(Qt.transparent)
+        elipse = self.addEllipse(x - 25, y - 25, 50, 50, pen, brush)
+        elipse.setAcceptDrops(True)
+        elipse.setCursor(Qt.OpenHandCursor)
+        elipse.setFlag(QGraphicsItem.ItemIsSelectable, True)
+        elipse.setFlag(QGraphicsItem.ItemIsMovable, True)
+        elipse.setFlag(QGraphicsItem.ItemIsFocusable, True)
+        elipse.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
+        elipse.setAcceptHoverEvents(True)
+        self.circles.append(elipse)
+
+    def mouseDoubleClickEvent(self, event):
+        x = event.scenePos().x()
+        y = event.scenePos().y()
+        btn = event.button()
+        if btn == 1:  # left click
+            self.coords.append((x, y))
+            self.elipse_adder(x, y)
+
+    def keyPressEvent(self, e): # hit space to remove point
+        if e.key() == Qt.Key_Space:
+            if len(self.circles) >= 1:
+                self.circles[-1].setVisible(False)
+                self.circles = self.circles[:-1]
+                self.coords = self.coords[:-1]
+
+    def sortCentroid(self, centroid):
+        scent = sorted(centroid, key=lambda x: x[0])
+        sortList = []
+        a = 0
+        comLength = 0
+        for length in self.rowcount:
+            comLength = comLength + length
+            sortList.extend((sorted(scent[a:comLength], key=lambda k: [k[1]])))
+            a = a + length
+        return sortList
+
+    def overlay_cores(self, core_diameter, scale_index, cores, autopilot=False):  # removed - centroid, image, cores
+        if len(self.rectsandtext) >= 1:
+            [i.setVisible(False) for i in self.rectsandtext]
+            self.rectsandtext = []
+        pen = QPen(QColor(69, 130, 201, 240))
+        pen.setWidthF(6)  # border width
+        brush = QBrush(QColor(215, 230, 248, 160))  # square fill
+        if autopilot:
+            self.centroid = self.coords
+            self.centroid = self.sortCentroid(self.centroid)
+            [self.elipse_adder(y, x) for (x, y) in self.centroid]
+            self.coords = [(y, x) for (x, y) in self.centroid]
+        else:
+            self.centroid = [(y, x) for (x, y) in self.coords]
+            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))]
+        diameter = core_diameter / scale_index
+        a = 0
+        for y, x in self.centroid:
+            try:
+                rect = self.addRect((x - (diameter / 2)), (y - (diameter / 2)), diameter, diameter, pen, brush)
+                text = self.addText(cores[a])  # label
+                self.rectsandtext.append(rect)
+                self.rectsandtext.append(text)
+                text.setPos(x, y)
+                text.setDefaultTextColor(QColor(35, 57, 82, 200))
+                font = QFont()
+                font.setPointSize(80)
+                text.setFont(font)
+                a = a + 1
+            except IndexError as e:
+                self.centroid.pop(a)
+                print("index error", self.centroid[a])
+                continue
+
+    def save(self, output, name):
+        # Get region of scene to capture from somewhere.
+        area = self.sceneRect().size().toSize()
+        image = QImage(area, QImage.Format_RGB888)
+        painter = QPainter(image)
+        self.render(painter, target=QRectF(image.rect()), source=self.sceneRect())
+        painter.end()
+        image.save(output + os.sep + name + "_overlay" + ".tiff")
+
+
+class MyWindow(QWidget):
+    def __init__(self):
+        super(MyWindow, self).__init__()
+        loadUi(sys._MEIPASS + os.sep + "scripts" + os.sep + "Cut_Application_thread_layout.ui", self)  # deployment
+        self.setWindowTitle('QuArray')
+        self.tabWidget.setStyleSheet("QTabWidget::pane {margin: 0px,0px,0px,0px; padding: 0px}")
+        self.excel_layout = self.excel_btn.isChecked()
+        self.excel_btn.toggled.connect(self.excel)
+        self.load_ndpi.clicked.connect(lambda: self.loadndpi())
+        self.load_excel.clicked.connect(lambda: self.read_excel())
+        self.overlay.clicked.connect(lambda: self.overlaystart())
+        self.export_2.clicked.connect(lambda: self.export_images())
+        # self.export_again.clicked.connect(lambda: self.export_images(meta_only=True))
+        self.current_image = None
+        # threshold buttons
+        self.gausianval = 0
+        self.thresholdval = None
+        self.otsu.clicked.connect(lambda: [self.threshold("otsu"), self.reset_sliders()])
+        self.threshmean.clicked.connect(lambda: [self.threshold("mean"), self.reset_sliders()])
+        self.threshtriangle.clicked.connect(lambda: [self.threshold("triangle"), self.reset_sliders()])
+        self.threshli.clicked.connect(lambda: [self.threshold("li"), self.reset_sliders()])
+        self.toggleorigional.clicked.connect(lambda: [self.threshold("origional"), self.reset_sliders()])
+        self.gausslider.setMaximum(5)
+        self.gausslider.setValue(0)
+        self.gausslider.valueChanged.connect(lambda: self.gauslineEdit.setText(str(self.gausslider.value())))  # change
+        self.gausslider.sliderReleased.connect(self.gaus)
+        self.closingslider.setMaximum(50)
+        self.closingslider.setValue(0)
+        self.closingslider.valueChanged.connect(lambda: self.closelineEdit.setText(str(self.closingslider.value())))
+        self.closingslider.sliderReleased.connect(self.closing)
+        self.removesmallobjects.clicked.connect(self.removesmall)
+        self.current_augments = {"threshold": False, "gausian": False, "closing": False, "overlay_applied": False,
+                                 "manual_overlay": False}
+        self.pathology = None
+        self.excelpath = False
+
+        self.init_scene()
+        self.show()
+
+    def overlaystart(self, autopilot=False, coords=None):
+        """
+        starts overlay
+        :param autopilot: if the coords need to be asigned outside of the click function
+        :param coords: the external coords to be applied in autopilot
+        """
+        try:
+            self.core_diameter = int(self.diamiterLineEdit.text().strip())
+        except:
+            self.core_diameter = 6000
+            self.diamiterLineEdit.setText('6000')
+            self.info("core diamiter must be an integer - reset to 6000")
+            pass
+        if autopilot:
+            self.scene.coords = coords
+            self.scene.overlay_cores(self.core_diameter, self.scale_index, self.cores, autopilot=True)
+        else:
+            self.scene.overlay_cores(self.core_diameter, self.scale_index, self.cores)
+        if self.overlaySave.isChecked():
+            self.info(f"Overlay saved to - {self.output}")
+            self.scene.save(self.output, self.name)
+        self.current_augments["overlay_applied"] = True
+        self.activate([self.export_2], action=True)
+
+    def excel(self, x):
+        self.excel_layout = x
+        if self.excelpath:
+            self.read_excel()
+
+    def init_scene(self):
+        self.scene = GraphicsScene(self)
+        self.graphicsView.setScene(self.scene)
+        self.pixmap = QGraphicsPixmapItem()
+        self.scene.addItem(self.pixmap)
+
+    def show_info(self, text):
+        self.metadata.setText(text)
+
+    def activate(self, names, action=True):
+        for i in names:
+            i.setEnabled(action)
+
+    def loadndpi(self):
+        self.init_scene()
+        formats = '*.ndpi*;;*.svs*;;*.tif*;;*.scn*;;*.mrxs*;;*.tiff*;;*.svslide*;;*.bif*'
+        self.path, _ = QFileDialog.getOpenFileName(parent=self, caption='Open file',
+                                                   directory="/Users/callum/Desktop/", filter=formats)
+        if self.path:
+            self.output = os.path.splitext(self.path)[0] + '_split'
+            if not os.path.exists(self.output):  # make output directory
+                os.mkdir(self.output)
+
+            self.name = os.path.split(self.output)[-1]
+            self.nameLineEdit.setText(self.name)
+            self.load_ndpi.setStyleSheet("background-color: rgb(0,90,0)")
+            try:
+                self.image = OpenSlide(self.path)
+            except Exception as e:
+                self.loadndpi()
+            print(self.path + ' read to memory')
+            print('    slide format = ' + str(OpenSlide.detect_format(self.path)))
+            if str(OpenSlide.detect_format(self.path)) == "aperio":
+                try:
+                    print('    Magnification = ' + str(self.image.properties['openslide.objective-power']))  # TODO
+                    print('    Date = ' + str(self.image.properties['aperio.Date']))
+                    print('    dimensions = ' + str(self.image.dimensions))
+                    print('    level_downsamples = ' + str(self.image.level_downsamples))
+                except KeyError:
+                    pass
+            if str(OpenSlide.detect_format(self.path)) == "hamamatsu":
+                try:
+                    self.formatLineEdit.setText("Hamamatsu")
+                    self.scanDateLineEdit.setText(str(self.image.properties['tiff.DateTime'][:10]))
+                    self.dimensionsLineEdit.setText(str(self.image.dimensions))
+                    self.magnificationLineEdit.setText(str(self.image.properties['hamamatsu.SourceLens']))
+                    self.show_info(f"""Magnification = {str(self.image.properties['hamamatsu.SourceLens'])}
+Date = {str(self.image.properties['tiff.DateTime'])}\ndimensions = {str(self.image.dimensions)}
+level_downsamples = {str(self.image.level_downsamples)}""")
+                    print('    Magnification = ' + str(self.image.properties['hamamatsu.SourceLens']))
+                    print('    Date = ' + str(self.image.properties['tiff.DateTime']))
+                    print('    dimensions = ' + str(self.image.dimensions))
+                    print('    level_downsamples = ' + str(self.image.level_downsamples))
+                    self.macro_image = self.image.associated_images['macro']
+                except KeyError:
+                    pass
+            self.overview_level_width = 3000
+            self.activate([self.nameLabel, self.nameLineEdit, self.formatLabel, self.formatLineEdit,
+                           self.magnificationLabel, self.magnificationLineEdit, self.scanDateLabel,
+                           self.scanDateLineEdit, self.dimensionsLabel, self.dimensionsLineEdit, self.overlayLevelLabel,
+                           self.overlayLevelLineEdit, self.graphicsView, self.overlaySave, self.groupBox_2,
+                           self.removesmallobjects])
+            self.get_overview()
+            if os.path.exists(os.path.splitext(self.path)[0] + '.xlsx'):
+                self.excelpath = os.path.splitext(self.path)[0] + '.xlsx'
+                self.read_excel()
+            else:
+                self.excelpath = False
+
+    def get_overview(self):
+        self.width_height = [
+            (int(self.image.properties[f'openslide.level[{i}].width']),
+             int(self.image.properties[f'openslide.level[{i}].height'])) for i
+            in range(int(self.image.properties['openslide.level-count']))]
+        width = [self.width_height[i][0] for i in range(len(self.width_height))]
+        self.lvl = np.where(width == self.find_nearest(width, self.overview_level_width))[0][0]
+        self.overlayLevelLineEdit.setText(str(self.lvl))
+        self.scale_index = self.width_height[0][0] / self.width_height[self.lvl][0]
+        self.overview = np.array(self.image.read_region(location=(0, 0), level=self.lvl,
+                                                        size=self.width_height[self.lvl]))
+        self.showimage(image=self.overview)
+
+    def find_nearest(self, array, value):
+        # https://stackoverflow.com/questions/2566412/find-nearest-value-in-numpy-array
+        array = np.asarray(array)
+        idx = (np.abs(array - value)).argmin()
+        return array[idx]
+
+    def showimage(self, image):
+        self.current_image = image
+        img = qimage2ndarray.array2qimage(image, normalize=True)
+        img = QPixmap(img)
+        self.pixmap.setPixmap(img)
+        self.graphicsView.fitInView(self.graphicsView.sceneRect(), Qt.KeepAspectRatio)
+
+    def reset_sliders(self):
+        self.gausslider.setValue(0)
+        self.closingslider.setValue(0)
+
+    def threshold(self, threshold_name):
+        if self.scene.circles:
+            self.scene.reset()
+        if self.current_augments['overlay_applied']:
+            del self.scene.coords
+            self.init_scene()
+            self.read_excel()
+            self.current_augments['overlay_applied'] = False
+        if threshold_name == "origional":
+            self.showimage(self.overview)
+            self.thresholdval = None
+            self.current_augments["threshold"] = False
+            return
+        self.thresholdval = threshold_name
+
+        im = rgb2gray(self.overview)
+        if threshold_name == "otsu":
+            threshold = threshold_otsu(im)
+        if threshold_name == "li":
+            threshold = threshold_li(im)
+        if threshold_name == "mean":
+            threshold = threshold_mean(im)
+        if threshold_name == "triangle":
+            threshold = threshold_triangle(im)
+        self.current_augments["threshold"] = threshold_name
+        self.current_image = im < threshold
+        self.showimage(self.current_image)
+
+    def gaus(self):
+        self.gauslineEdit.setText(str(self.gausslider.value()))
+        if self.current_image.ndim > 2:
+            filtered = gaussian(self.overview, sigma=self.gausslider.value())
+        else:
+            self.threshold(self.current_augments["threshold"])
+            filtered = gaussian(self.current_image, sigma=self.gausslider.value())
+        self.showimage(filtered)
+
+    def closing(self):
+        self.closelineEdit.setText(str(self.closingslider.value()))
+        if self.current_augments["threshold"]:
+            self.threshold(self.current_augments["threshold"])
+            if self.closingslider.value() > 0:
+                if self.current_augments["gausian"]:
+                    self.current_image = gaussian(self.current_image, sigma=self.gausslider.value())
+                closed = closing(self.current_image, square(self.closingslider.value()))
+                closed = closed > 0
+                self.showimage(closed)
+
+    def removesmall(self):
+        """
+        the function name is misleading as this function now applies remove small objects - then
+        overlays the cores and saves the image
+        """
+        if not self.current_augments['threshold']:
+            return
+        if self.current_augments['overlay_applied']:
+            self.init_scene()
+            self.read_excel()
+        labeled_image = label(self.current_image)
+        try:
+            min = int(self.smallobs_text.text())
+        except ValueError as e:
+            self.smallobs_text.setText("6000")
+            min = 6000
+            self.info("remove small value must be an integer - reset to 6000")
+        labeled_image = remove_small_objects(labeled_image, min_size=min)
+        self.showimage(labeled_image)
+        labels = regionprops(labeled_image)
+        centroid = [r.centroid for r in labels]
+        self.overlaystart(autopilot=True, coords=centroid)
+        self.current_augments['overlay_applied'] = True
+
+    def read_excel(self):
+        if not self.excelpath:
+            self.excelpath, _ = QFileDialog.getOpenFileName(parent=self, caption='Open file',
+                                                            directory="/Users/callum/Desktop", filter="*.xlsx*")
+        self.activate([self.numberOfCoresLabel, self.numberOfCoresLineEdit, self.diameterLabel, self.diamiterLineEdit,
+                       self.overlay, self.progressBar, self.excel_btn, self.overlaySave, self.tabWidget])
+        if self.excelpath:
+            self.load_excel.setStyleSheet("background-color: rgb(0,90,0)")
+            excelname = self.excelpath
+            wb = load_workbook(excelname)
+            ws = wb.worksheets[0]
+            cores = []
+            values = []
+            rowcount = []
+            self.rowcol = []
+            if not self.excel_layout:
+                for row in ws.iter_rows():
+                    for cell in row:
+                        values.append(cell.value)
+                        if cell.value == 1:
+                            cores.append((str(chr(int(cell.row) + 64))) + str(int(ord(cell.column_letter)) - 64))
+            else:
+                cores = []
+                for col in ws.iter_cols():
+                    for cell in col:
+                        if cell.value == 1:
+                            cores.append(cell.coordinate)  # get core names if contain a 1
+                cores.sort(key=lambda x: x[1:])
+                for row in ws.iter_rows():
+                    for cell in row:
+                        values.append(cell.value)
+            values = np.array_split(values, ws.max_row)
+            for row in values:
+                rowcount.append(np.count_nonzero(row))  # EXCEL END
+            self.numberOfCoresLineEdit.setText(str(len(cores)))
+            self.cores = cores
+            self.values = values
+            self.rowcount = rowcount
+            self.arrayshape = (ws.max_row, ws.max_column)
+            self.scene.rowcount = rowcount
+            # check for pathology file
+            if len(wb.sheetnames) > 1:
+                ws = wb.worksheets[1]  # pathology tab (hopefully)
+                self.pathology = [ws[i].value for i in self.cores]
+
+    def info(self, text):
+        self.label.setText(text)
+
+    def export_images(self, meta_only=False):
+        if not self.scene.centroid:
+            self.info("must overlay some selected core - double click on image")
+            return
+        self.progressBar.setMaximum(len(self.scene.centroid))
+        self.activate([self.nameLabel, self.nameLineEdit, self.formatLabel, self.formatLineEdit,
+                       self.magnificationLabel, self.magnificationLineEdit, self.scanDateLabel,
+                       self.scanDateLineEdit, self.dimensionsLabel, self.dimensionsLineEdit, self.overlayLevelLabel,
+                       self.overlayLevelLineEdit, self.graphicsView, self.numberOfCoresLabel,
+                       self.numberOfCoresLineEdit, self.diameterLabel, self.diamiterLineEdit,
+                       self.export_2, self.overlay, self.excel_btn, self.load_ndpi, self.load_excel, self.overlaySave,
+                       self.tabWidget], action=False)
+        try:
+            resolution = int(self.resolution_edit.text())
+            print("new resolution applied")
+        except ValueError as E:
+            resolution = 0
+
+        self.export = Export(image=self.image, centroid=self.scene.centroid, cores=self.cores,
+                             scale_index=self.scale_index,
+                             core_diameter=self.core_diameter, output=self.output, name=self.name, lvl=self.lvl,
+                            path=self.path, arrayshape=self.arrayshape, pathology=self.pathology, resolution=resolution, window=self,
+                             meta_only=meta_only)
+
+        self.thread = QThread()
+        self.export.info.connect(self.info)
+        self.export.done.connect(self.complete)
+        self.export.countChanged.connect(self.progressBar.setValue)
+        self.export.moveToThread(self.thread)
+        self.thread.started.connect(self.export.run)
+        self.thread.start()
+
+    def complete(self):
+        print('done')
+        self.activate([self.nameLabel, self.nameLineEdit, self.formatLabel, self.formatLineEdit,
+                       self.magnificationLabel, self.magnificationLineEdit, self.scanDateLabel,
+                       self.scanDateLineEdit, self.dimensionsLabel, self.dimensionsLineEdit, self.overlayLevelLabel,
+                       self.overlayLevelLineEdit, self.graphicsView, self.numberOfCoresLabel,
+                       self.numberOfCoresLineEdit, self.diameterLabel, self.diamiterLineEdit,
+                       self.export_2, self.overlay, self.excel_btn, self.load_ndpi, self.load_excel, self.overlaySave],
+                      action=True)
+
+
+class Export(QObject):
+    info = pyqtSignal(str)
+    countChanged = pyqtSignal(int)
+    figures = pyqtSignal()
+    done = pyqtSignal(bool)
+    writemeta = pyqtSignal(bool)
+
+    def __init__(self, image, centroid, cores, scale_index, core_diameter, output, name, lvl, path, arrayshape,
+                 pathology, resolution, window, meta_only=False):
+        super().__init__()
+        self.image = image
+        self.centroid = centroid
+        self.cores = cores
+        self.scale_index = scale_index
+        self.core_diameter = core_diameter
+        self.output = output
+        self.name = name
+        self.lvl = lvl
+        self.path = path
+        self.arrayshape = arrayshape
+        self.pathology = pathology
+        self.resolution = resolution
+        self.meta_only = meta_only
+        self.win = window
+
+    @pyqtSlot()
+    def run(self):
+        if self.meta_only:
+            self.json_write()
+            self.wsifigure(higher_resolution=False, pathology=self.pathology)
+        else:
+            self.export_images(self.centroid, self.cores)
+
+    def export_images(self, centroid, cores):
+        infostr = []
+        self.scaledcent = [(y * self.scale_index, x * self.scale_index) for (x, y) in centroid]  # rotate xy openslide
+        self.scaledcent = [(int(x - (self.core_diameter / 2)), int(y - (self.core_diameter / 2))) for (x, y) in self.scaledcent]
+        self.json_write()
+        self.wsifigure(higher_resolution=False, pathology=self.pathology)
+        w_h = (self.core_diameter, self.core_diameter)
+        self.lvl = 0
+        for i in range(len(self.scaledcent)):
+            if not self.win.isVisible():
+                print("need to kill here - main window closed")
+                break
+            infostr.append("Exporting " + self.name + "_" + cores[i] + ".png")
+            print("Exporting " + self.name + "_" + cores[i] + ".png")
+            self.info.emit('\n'.join(infostr))
+            core = self.image.read_region(location=self.scaledcent[i], level=self.lvl, size=w_h)
+            core.save(self.output + os.sep + self.name + "_" + cores[i] + ".png")
+            infostr.append("Saved " + self.name + "_" + cores[i] + ".png")
+            self.info.emit('\n'.join(infostr))
+            self.countChanged.emit(i + 1)
+
+        infostr.append("All files exported with JSON metadata")
+        self.info.emit('\n'.join(infostr))
+        print('\n'.join(infostr))
+        self.done.emit(True)
+
+    def json_write(self):
+        jsondata = {"path": self.path, "coordinates": self.scaledcent, "cores": self.cores,
+                    "diameter": self.core_diameter, "scale_index": self.scale_index, "lowlevel": int(self.lvl),
+                    "arrayshape": self.arrayshape}
+        self.info.emit('Saving ' + self.output + os.sep + self.name + '_metadata.json')
+        with open(self.output + os.sep + self.name + '_metadata.json', "w") as write_file:
+            json.dump(jsondata, write_file)
+
+    def wsifigure(self, higher_resolution=False, pathology=None):
+        """
+        TODO: need to link this better to the actual script - rather than the meta file before its created
+        todo: have removed button export_again for function later
+        takes the metadata from the json
+        makes a fig of the locations on the tissue array and saves it
+        higher resolution = int start at 1 and move up to improve resolution - will slow code
+        """
+        higher_resolution = self.resolution
+
+        def overly_path(im, overlay, pathology):
+            if pathology == "N":
+                colour = [0, 1, 0]  # green
+            else:
+                colour = [1, 0, 0]  # red
+            im[overlay == 0] = colour
+            return im
+
+        jsonpath = self.output + os.sep + self.name + '_metadata.json'
+        with open(jsonpath) as json_file:
+            data = json.load(json_file)
+            image = OpenSlide(data['path'])
+            if higher_resolution:
+                lvl = data['lowlevel'] - int(higher_resolution)
+            else:
+                lvl = data['lowlevel']
+            scale_index = image.level_downsamples[lvl]
+            diameter = int(data["diameter"] / scale_index)
+            arrayshape = data['arrayshape']
+            arrayshape = [(a * diameter) + 1 for a in arrayshape]
+            arrayshape.append(3)
+            figarray = np.ones(arrayshape)
+            cindex = [coordinate_from_string(i) for i in data['cores']]  # returns ('A',4)
+            cindex = [(b, column_index_from_string(a)) for (a, b) in cindex]  # returns index tuples for each core
+            maskcoord = [[y * diameter - diameter if y > 1 else y for y in x] for x in cindex]
+
+            if pathology:
+                overlay = np.load(sys._MEIPASS + os.sep + "scripts" + os.sep + "outline.npy")
+                overlay = resize(overlay, (diameter, diameter))
+
+            for i in range(len(data["coordinates"])):  # iterate through coordinates pulling out images from wsi
+                im = image.read_region(location=data["coordinates"][i], level=lvl, size=(diameter, diameter))
+                im = np.array(im)[:, :, :3]
+                if pathology:
+                    try:
+                        im = overly_path(im, overlay, pathology[i])
+                    except:
+                        continue
+                figarray[maskcoord[i][0]:maskcoord[i][0] + diameter, maskcoord[i][1]:maskcoord[i][1] + diameter, :] = im
+            figarray[figarray == [1, 1, 1]] = 255  # make background white
+            savepath = self.output + os.sep + self.name + '_layoutfig.tiff'
+            Image.fromarray(figarray.astype(np.uint8)).save(savepath)