class QTSeedEditor(QDialog): """ DICOM viewer. """ @staticmethod def get_line(mode='h'): line = QFrame() if mode == 'h': line.setFrameStyle(QFrame.HLine) elif mode == 'v': line.setFrameStyle(QFrame.VLine) line.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) return line def initUI(self, shape, vscale, height=600, mode='seed'): """ Initialize UI. Parameters ---------- shape : (int, int, int) Shape of data matrix. vscale : (float, float, float) Voxel scaling. height : int Maximal slice height in pixels. mode : str Editor mode. """ # picture grid = height / float(shape[1] * vscale[1]) mgrid = (grid * vscale[0], grid * vscale[1]) self.slice_box = SliceBox(shape[:-1], mgrid, mode) self.slice_box.setScrollFun(self.scrollSlices) self.connect(self.slice_box, SIGNAL('focus_slider'), self.focusSliceSlider) # sliders self.allow_select_slice = True self.n_slices = shape[2] self.slider = QSlider(Qt.Vertical) self.slider.label = QLabel() self.slider.label.setText('Slice: %d / %d' % (self.actual_slice, self.n_slices)) self.slider.setRange(1, self.n_slices) self.slider.valueChanged.connect(self.sliderSelectSlice) self.slider.setValue(self.actual_slice) self.slider_cw = {} self.slider_cw['c'] = QSlider(Qt.Horizontal) self.slider_cw['c'].valueChanged.connect(self.changeC) self.slider_cw['c'].label = QLabel() self.slider_cw['w'] = QSlider(Qt.Horizontal) self.slider_cw['w'].valueChanged.connect(self.changeW) self.slider_cw['w'].label = QLabel() self.view_label = QLabel('View size: %d x %d' % self.img_aview.shape[:-1]) self.voxel_label = QLabel('Voxel size [mm]:\n %.2f x %.2f x %.2f'\ % tuple(self.voxel_size[np.array(self.act_transposition)])) combo_view_options = VIEW_TABLE.keys() combo_view = QComboBox(self) combo_view.activated[str].connect(self.setView) combo_view.addItems(combo_view_options) # buttons self.btn_quit = QPushButton("Return", self) self.btn_quit.clicked.connect(self.quit) combo_dmask = QComboBox(self) combo_dmask.activated.connect(self.changeMask) self.mask_points_tab, aux = self.init_draw_mask(DRAW_MASK, mgrid) for icon, label in aux: combo_dmask.addItem(icon, label) self.slice_box.setMaskPoints(self.mask_points_tab[combo_dmask.currentIndex()]) self.status_bar = QStatusBar() vopts = [] vmenu = [] appmenu = [] if mode == 'seed' and self.mode_fun is not None: btn_recalc = QPushButton("Recalculate", self) btn_recalc.clicked.connect(self.recalculate) appmenu.append(QLabel('<b>Segmentation mode</b><br><br><br>' + 'Select the region of interest<br>' + 'using the mouse buttons:<br><br>' + ' <i>left</i> - inner region<br>' + ' <i>right</i> - outer region<br><br>')) appmenu.append(btn_recalc) appmenu.append(QLabel()) self.volume_label = QLabel('Volume:\n unknown') appmenu.append(self.volume_label) # Set middle pencil as default (M. Jirik) combo_dmask.setCurrentIndex(1) self.slice_box.setMaskPoints( self.mask_points_tab[combo_dmask.currentIndex()]) # -----mjirik---end------ if mode == 'seed' or mode == 'crop'\ or mode == 'mask' or mode == 'draw': btn_del = QPushButton("Delete Seeds", self) btn_del.clicked.connect(self.deleteSliceSeeds) vmenu.append(None) vmenu.append(btn_del) combo_contour_options = ['fill', 'contours', 'off'] combo_contour = QComboBox(self) combo_contour.activated[str].connect(self.changeContourMode) combo_contour.addItems(combo_contour_options) self.changeContourMode(combo_contour_options[combo_contour.currentIndex()]) vopts.append(QLabel('Selection mode:')) vopts.append(combo_contour) if mode == 'mask': btn_recalc_mask = QPushButton("Recalculate mask", self) btn_recalc_mask.clicked.connect(self.updateMaskRegion_btn) btn_all = QPushButton("Select all", self) btn_all.clicked.connect(self.maskSelectAll) btn_reset = QPushButton("Reset selection", self) btn_reset.clicked.connect(self.resetSelection) btn_reset_seads = QPushButton("Reset seads", self) btn_reset_seads.clicked.connect(self.resetSeads) btn_add = QPushButton("Add selection", self) btn_add.clicked.connect(self.maskAddSelection) btn_rem = QPushButton("Remove selection", self) btn_rem.clicked.connect(self.maskRemoveSelection) btn_mask = QPushButton("Mask region", self) btn_mask.clicked.connect(self.maskRegion) appmenu.append(QLabel('<b>Mask mode</b><br><br><br>' + 'Select the region to mask<br>' + 'using the left mouse button<br><br>')) appmenu.append(self.get_line('h')) appmenu.append(btn_recalc_mask) appmenu.append(btn_all) appmenu.append(btn_reset) appmenu.append(btn_reset_seads) appmenu.append(self.get_line('h')) appmenu.append(btn_add) appmenu.append(btn_rem) appmenu.append(self.get_line('h')) appmenu.append(btn_mask) appmenu.append(self.get_line('h')) self.mask_qhull = None if mode == 'crop': btn_crop = QPushButton("Crop", self) btn_crop.clicked.connect(self.crop) appmenu.append(QLabel('<b>Crop mode</b><br><br><br>' + 'Select the crop region<br>' + 'using the left mouse button<br><br>')) appmenu.append(btn_crop) if mode == 'draw': appmenu.append(QLabel('<b>Manual segmentation<br> mode</b><br><br><br>' + 'Mark the region of interest<br>' + 'using the mouse buttons:<br><br>' + ' <i>left</i> - draw<br>' + ' <i>right</i> - erase<br>' + ' <i>middle</i> - vol. erase<br><br>')) btn_reset = QPushButton("Reset", self) btn_reset.clicked.connect(self.resetSliceDraw) vmenu.append(None) vmenu.append(btn_reset) combo_erase_options = ['inside', 'outside'] combo_erase = QComboBox(self) combo_erase.activated[str].connect(self.changeEraseMode) combo_erase.addItems(combo_erase_options) self.changeEraseMode(combo_erase_options[combo_erase.currentIndex()]) vopts.append(QLabel('Volume erase mode:')) vopts.append(combo_erase) hbox = QHBoxLayout() vbox = QVBoxLayout() vbox_left = QVBoxLayout() vbox_app = QVBoxLayout() hbox.addWidget(self.slice_box) hbox.addWidget(self.slider) vbox_left.addWidget(self.slider.label) vbox_left.addWidget(self.view_label) vbox_left.addWidget(self.voxel_label) vbox_left.addWidget(QLabel()) vbox_left.addWidget(QLabel('View plane:')) vbox_left.addWidget(combo_view) vbox_left.addWidget(self.get_line()) vbox_left.addWidget(self.slider_cw['c'].label) vbox_left.addWidget(self.slider_cw['c']) vbox_left.addWidget(self.slider_cw['w'].label) vbox_left.addWidget(self.slider_cw['w']) vbox_left.addWidget(self.get_line()) vbox_left.addWidget(QLabel('Drawing mask:')) vbox_left.addWidget(combo_dmask) for ii in vopts: vbox_left.addWidget(ii) for ii in vmenu: if ii is None: vbox_left.addStretch(1) else: vbox_left.addWidget(ii) for ii in appmenu: if ii is None: vbox_app.addStretch(1) else: vbox_app.addWidget(ii) vbox_app.addStretch(1) vbox_app.addWidget(self.btn_quit) hbox.addLayout(vbox_left) hbox.addWidget(self.get_line('v')) hbox.addLayout(vbox_app) vbox.addLayout(hbox) vbox.addWidget(self.status_bar) self.my_layout = vbox self.setLayout(vbox) self.setWindowTitle('Segmentation Editor') self.show() def __init__(self, img, viewPositions=None, seeds=None, contours=None, mode='seed', modeFun=None, voxelSize=[1,1,1], volume_unit='mm3'): """ Initiate Editor Parameters ---------- img : array DICOM data matrix. actualSlice : int Index of actual slice. seeds : array Seeds, user defined regions of interest. contours : array Computed segmentation. mode : str Editor modes: 'seed' - seed editor 'crop' - manual crop 'draw' - drawing 'mask' - mask region modeFun : fun Mode function invoked by user button. voxelSize : tuple of float voxel size [mm] volume_unit : allow select output volume in mililiters or mm3 [mm, ml] """ QDialog.__init__(self) self.mode = mode self.mode_fun = modeFun self.actual_view = 'axial' self.act_transposition = VIEW_TABLE[self.actual_view] self.img = img self.img_aview = self.img.transpose(self.act_transposition) self.volume_unit = volume_unit self.last_view_position = {} for jj, ii in enumerate(VIEW_TABLE.iterkeys()): if viewPositions is None: viewpos = img.shape[VIEW_TABLE[ii][-1]] / 2 else: viewpos = viewPositions[jj] self.last_view_position[ii] =\ img.shape[VIEW_TABLE[ii][-1]] - viewpos - 1 self.actual_slice = self.last_view_position[self.actual_view] # set contours self.contours = contours if self.contours is None: self.contours_aview = None else: self.contours_aview = self.contours.transpose(self.act_transposition) # masked data - has information about which data were removed # 1 == enabled, 0 == deleted # How to return: # editorDialog.exec_() # masked_data = editorDialog.masked self.masked = np.ones(self.img.shape, np.int8) self.voxel_size = np.squeeze(np.asarray(voxelSize)) self.voxel_scale = self.voxel_size / float(np.min(self.voxel_size)) self.voxel_volume = np.prod(voxelSize) # set seeds if seeds is None: self.seeds = np.zeros(self.img.shape, np.int8) else: self.seeds = seeds self.seeds_aview = self.seeds.transpose(self.act_transposition) self.seeds_modified = False self.initUI(self.img_aview.shape, self.voxel_scale[np.array(self.act_transposition)], 600, mode) if mode == 'draw': self.seeds_orig = self.seeds.copy() self.slice_box.setEraseFun(self.eraseVolume) # set view window values C/W lb = np.min(img) self.img_min_val = lb ub = np.max(img) dul = np.double(ub) - np.double(lb) self.cw_range = {'c': [lb, ub], 'w': [1, dul]} self.slider_cw['c'].setRange(lb, ub) self.slider_cw['w'].setRange(1, dul) self.changeC(lb + dul / 2) self.changeW(dul) self.offset = np.zeros((3,), dtype=np.int16) def showStatus(self, msg): self.status_bar.showMessage(QString(msg)) QApplication.processEvents() def init_draw_mask(self, draw_mask, grid): mask_points = [] mask_iconlabel = [] for mask, label in draw_mask: w, h = mask.shape xx, yy = mask.nonzero() mask_points.append((xx - w/2, yy - h/2)) img = QImage(w, h, QImage.Format_ARGB32) img.fill(qRgba(255, 255, 255, 0)) for ii in range(xx.shape[0]): img.setPixel(xx[ii], yy[ii], qRgba(0, 0, 0, 255)) img = img.scaled(QSize(w * grid[0], h * grid[1])) icon = QIcon(QPixmap.fromImage(img)) mask_iconlabel.append((icon, label)) return mask_points, mask_iconlabel def saveSliceSeeds(self): aux = self.slice_box.getSliceSeeds() if aux is not None: self.seeds_aview[...,self.actual_slice] = aux self.seeds_modified = True else: self.seeds_modified = False def updateMaskRegion_btn(self): self.saveSliceSeeds() self.updateMaskRegion() def updateMaskRegion(self): crp = self.getCropBounds(return_nzs=True) if crp is not None: off, cri, nzs = crp if nzs[0].shape[0] <=5: self.showStatus("Not enough points (need >= 5)!") else: points = np.transpose(nzs) hull = Delaunay(points) X, Y, Z = np.mgrid[cri[0], cri[1], cri[2]] grid = np.vstack([X.ravel(), Y.ravel(), Z.ravel()]).T simplex = hull.find_simplex(grid) fill = grid[simplex >=0,:] fill = (fill[:,0], fill[:,1], fill[:,2]) if self.contours is None or self.contours_old is None: self.contours = np.zeros(self.img.shape, np.int8) self.contours_old = self.contours.copy() else: self.contours[self.contours != 2] = 0 self.contours[fill] = 1 self.contours_aview = self.contours.transpose(self.act_transposition) self.selectSlice(self.actual_slice) def maskRegion(self): self.masked[self.contours == 0] = 0 self.img[self.contours != 2] = self.img_min_val self.contours.fill(0) self.contours_old = self.contours.copy() self.seeds.fill(0) self.selectSlice(self.actual_slice) def maskAddSelection(self): self.updateMaskRegion() if self.contours is None: return self.contours[self.contours == 1] = 2 self.contours_old = self.contours.copy() self.seeds.fill(0) self.selectSlice(self.actual_slice) def maskRemoveSelection(self): self.updateMaskRegion() if self.contours is None: return self.contours[self.contours == 1] = 0 self.contours_old = self.contours.copy() self.seeds.fill(0) self.selectSlice(self.actual_slice) def maskSelectAll(self): self.updateMaskRegion() self.seeds[0][0][0] = 1 self.seeds[0][0][-1] = 1 self.seeds[0][-1][0] = 1 self.seeds[0][-1][-1] = 1 self.seeds[-1][0][0] = 1 self.seeds[-1][0][-1] = 1 self.seeds[-1][-1][0] = 1 self.seeds[-1][-1][-1] = 1 self.updateMaskRegion() self.selectSlice(self.actual_slice) def resetSelection(self): self.updateMaskRegion() if self.contours is None: return self.contours.fill(0) self.contours_old = self.contours.copy() self.seeds.fill(0) self.selectSlice(self.actual_slice) def resetSeads(self): self.seeds.fill(0) if self.contours is not None: self.contours = self.contours_old.copy() self.contours_aview = self.contours.transpose(self.act_transposition) self.updateMaskRegion() self.selectSlice(self.actual_slice) def updateCropBounds(self): crp = self.getCropBounds() if crp is not None: _, cri = crp self.contours = np.zeros(self.img.shape, np.int8) self.contours[cri].fill(1) self.contours_aview = self.contours.transpose(self.act_transposition) def focusSliceSlider(self): self.slider.setFocus(True) def sliderSelectSlice(self, value): self.selectSlice(self.n_slices - value) def scrollSlices(self, inc): if abs(inc) > 0: new = self.actual_slice + inc self.selectSlice(new) def selectSlice(self, value, force=False): if not(self.allow_select_slice): return if (value < 0) or (value >= self.n_slices): return if (value != self.actual_slice) or force: self.saveSliceSeeds() if self.seeds_modified: if self.mode == 'crop': self.updateCropBounds() elif self.mode == 'mask': self.updateMaskRegion() if self.contours is None: contours = None else: contours = self.contours_aview[...,value] slider_val = self.n_slices - value self.slider.setValue(slider_val) self.slider.label.setText('Slice: %d / %d' % (slider_val, self.n_slices)) self.slice_box.setSlice(self.img_aview[...,value], self.seeds_aview[...,value], contours) self.actual_slice = value def getSeeds(self): return self.seeds def getImg(self): return self.img def getOffset(self): return self.offset * self.voxel_size def getSeedsVal(self, label): return self.img[self.seeds==label] def getContours(self): return self.contours def setContours(self, contours): self.contours = contours self.contours_aview = self.contours.transpose(self.act_transposition) self.selectSlice(self.actual_slice) def changeCW(self, value, key): rg = self.cw_range[key] if (value < rg[0]) or (value > rg[1]): return if (value != self.slice_box.getCW()[key]): self.slider_cw[key].setValue(value) self.slider_cw[key].label.setText('%s: %d' % (key.upper(), value)) self.slice_box.setCW(value, key) self.slice_box.updateSliceCW(self.img_aview[...,self.actual_slice]) def changeC(self, value): self.changeCW(value, 'c') def changeW(self, value): self.changeCW(value, 'w') def setView(self, value): self.last_view_position[self.actual_view] = self.actual_slice # save seeds self.saveSliceSeeds() if self.seeds_modified: if self.mode == 'crop': self.updateCropBounds() elif self.mode == 'mask': self.updateMaskRegion() key = str(value) self.actual_view = key self.actual_slice = self.last_view_position[key] self.act_transposition = VIEW_TABLE[key] self.img_aview = self.img.transpose(self.act_transposition) self.seeds_aview = self.seeds.transpose(self.act_transposition) if self.contours is not None: self.contours_aview = self.contours.transpose(self.act_transposition) contours = self.contours_aview[...,self.actual_slice] else: contours = None vscale = self.voxel_scale[np.array(self.act_transposition)] height = self.slice_box.height() grid = height / float(self.img_aview.shape[1] * vscale[1]) # width = (self.img_aview.shape[0] * vscale[0])[0] # if width > 800: # height = 400 # grid = height / float(self.img_aview.shape[1] * vscale[1]) mgrid = (grid * vscale[0], grid * vscale[1]) self.slice_box.resizeSlice(new_slice_size=self.img_aview.shape[:-1], new_grid=mgrid) self.slice_box.setSlice(self.img_aview[...,self.actual_slice], self.seeds_aview[...,self.actual_slice], contours) self.allow_select_slice = False self.n_slices = self.img_aview.shape[2] slider_val = self.n_slices - self.actual_slice self.slider.setRange(1, self.n_slices) self.slider.setValue(slider_val) self.allow_select_slice = True self.slider.label.setText('Slice: %d / %d' % (slider_val, self.n_slices)) self.view_label.setText('View size: %d x %d' % self.img_aview.shape[:-1]) self.adjustSize() self.adjustSize() def changeMask(self, val): self.slice_box.setMaskPoints(self.mask_points_tab[val]) def changeContourMode(self, val): self.slice_box.contour_mode = str(val) self.slice_box.updateSlice() def changeEraseMode(self, val): self.slice_box.erase_mode = str(val) def eraseVolume(self, pos, mode): self.showStatus("Processing...") xyz = np.array(pos + (self.actual_slice,)) p = np.zeros_like(xyz) p[np.array(self.act_transposition)] = xyz p = tuple(p) if self.seeds[p] > 0: if mode == 'inside': erase_reg(self.seeds, p, val=0) elif mode == 'outside': erase_reg(self.seeds, p, val=-1) idxs = np.where(self.seeds < 0) self.seeds.fill(0) self.seeds[idxs] = 1 if self.contours is None: contours = None else: contours = self.contours_aview[...,self.actual_slice] self.slice_box.setSlice(self.img_aview[...,self.actual_slice], self.seeds_aview[...,self.actual_slice], contours) self.showStatus("Done") def cropUpdate(self, img): for ii in VIEW_TABLE.iterkeys(): self.last_view_position[ii] = 0 self.actual_slice = 0 self.img = img self.img_aview = self.img.transpose(self.act_transposition) self.contours = None self.contours_aview = None self.seeds = np.zeros(self.img.shape, np.int8) self.seeds_aview = self.seeds.transpose(self.act_transposition) self.seeds_modified = False vscale = self.voxel_scale[np.array(self.act_transposition)] height = self.slice_box.height() grid = height / float(self.img_aview.shape[1] * vscale[1]) mgrid = (grid * vscale[0], grid * vscale[1]) self.slice_box.resizeSlice(new_slice_size=self.img_aview.shape[:-1], new_grid=mgrid) self.slice_box.setSlice(self.img_aview[...,self.actual_slice], self.seeds_aview[...,self.actual_slice], None) self.allow_select_slice = False self.n_slices = self.img_aview.shape[2] self.slider.setValue(self.actual_slice + 1) self.slider.setRange(1, self.n_slices) self.allow_select_slice = True self.slider.label.setText('Slice: %d / %d' % (self.actual_slice + 1, self.n_slices)) self.view_label.setText('View size: %d x %d' % self.img_aview.shape[:-1]) def getCropBounds(self, return_nzs=False, flat=False): nzs = self.seeds.nonzero() cri = [] flag = True for ii in range(3): if nzs[ii].shape[0] == 0: flag = False break smin, smax = np.min(nzs[ii]), np.max(nzs[ii]) if not(flat): if smin == smax: flag = False break cri.append((smin, smax)) if flag: cri = np.array(cri) out = [] offset = [] for jj, ii in enumerate(cri): out.append(slice(ii[0], ii[1] + 1)) offset.append(ii[0]) if return_nzs: return np.array(offset), tuple(out), nzs else: return np.array(offset), tuple(out) else: return None def crop(self): self.showStatus("Processing...") crp = self.getCropBounds() if crp is not None: offset, cri = crp crop = self.img[cri] self.img = np.ascontiguousarray(crop) self.offset += offset self.showStatus('Done') else: self.showStatus('Region not selected!') self.cropUpdate(self.img) def recalculate(self, event): self.saveSliceSeeds() if np.abs(np.min(self.seeds) - np.max(self.seeds)) < 2: self.showStatus('Inner and outer regions not defined!') return self.showStatus("Processing...") self.mode_fun(self) self.selectSlice(self.actual_slice) self.updateVolume() self.showStatus("Done") def deleteSliceSeeds(self, event): self.seeds_aview[...,self.actual_slice] = 0 self.slice_box.setSlice(seeds=self.seeds_aview[...,self.actual_slice]) self.slice_box.updateSlice() def resetSliceDraw(self, event): seeds_orig_aview = self.seeds_orig.transpose(self.act_transposition) self.seeds_aview[...,self.actual_slice] = seeds_orig_aview[...,self.actual_slice] self.slice_box.setSlice(seeds=self.seeds_aview[...,self.actual_slice]) self.slice_box.updateSlice() def quit(self, event): self.close() def updateVolume(self): text = 'Volume:\n unknown' if self.voxel_volume is not None: if self.mode == 'draw': vd = self.seeds else: vd = self.contours if vd is not None: nzs = vd.nonzero() nn = nzs[0].shape[0] if self.volume_unit == 'ml': text = 'Volume [ml]:\n %.2f' %\ (nn * self.voxel_volume / 1000) else: text = 'Volume [mm3]:\n %.2e' % (nn * self.voxel_volume) self.volume_label.setText(text) def getROI(self): crp = self.getCropBounds() if crp is not None: _, cri = crp else: cri = [] for jj, ii in enumerate(self.img.shape): off = self.offset[jj] cri.append(slice(off, off + ii)) return cri
class QTSeedEditor(QDialog): """ DICOM viewer. """ @staticmethod def get_line(mode='h'): line = QFrame() if mode == 'h': line.setFrameStyle(QFrame.HLine) elif mode == 'v': line.setFrameStyle(QFrame.VLine) line.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) return line def initUI(self, shape, vscale, height=600, mode='seed'): """ Initialize UI. Parameters ---------- shape : (int, int, int) Shape of data matrix. vscale : (float, float, float) Voxel scaling. height : int Maximal slice height in pixels. mode : str Editor mode. """ self.slab = {} # picture grid = height / float(shape[1] * vscale[1]) mgrid = (grid * vscale[0], grid * vscale[1]) self.slice_box = SliceBox(shape[:-1], mgrid, mode) self.slice_box.setScrollFun(self.scrollSlices) self.connect(self.slice_box, SIGNAL('focus_slider'), self.focusSliceSlider) # sliders self.allow_select_slice = True self.n_slices = shape[2] self.slider = QSlider(Qt.Vertical) self.slider.valueChanged.connect(self.sliderSelectSlice) self.slider.label = QLabel() self.slider.setRange(1, self.n_slices) self.slider_cw = {} self.slider_cw['c'] = QSlider(Qt.Horizontal) self.slider_cw['c'].valueChanged.connect(self.changeC) self.slider_cw['c'].label = QLabel() self.slider_cw['w'] = QSlider(Qt.Horizontal) self.slider_cw['w'].valueChanged.connect(self.changeW) self.slider_cw['w'].label = QLabel() self.view_label = QLabel('View size: %d x %d' % self.img_aview.shape[:-1]) self.voxel_label = QLabel('Voxel size [mm]:\n %.2f x %.2f x %.2f'\ % tuple(self.voxel_size[np.array(self.act_transposition)])) # combo_view_options = VIEW_TABLE.keys() # combo_view = QComboBox(self) # combo_view.activated[str].connect(self.setView) # combo_view.addItems(combo_view_options) #radio button group for choosing seed class ------------------------ self.current_class = 1 self.slice_box.seed_mark = self.current_class number_group = QGroupBox(QString('Class markers')) vbox_NG = QVBoxLayout() r1 = QRadioButton('class 1') r1.setStyleSheet('QRadioButton {color: red}') r1.setChecked(True) r2 = QRadioButton('class 2') r2.setStyleSheet('QRadioButton {color: green}') r3 = QRadioButton('class 3') r3.setStyleSheet('QRadioButton {color: blue}') r4 = QRadioButton('class 4') r4.setStyleSheet('QRadioButton {color: cyan}') r5 = QRadioButton('class 5') r5.setStyleSheet('QRadioButton {color: magenta}') vbox_NG.addWidget(r1) vbox_NG.addWidget(r2) vbox_NG.addWidget(r3) vbox_NG.addWidget(r4) vbox_NG.addWidget(r5) number_group.setLayout(vbox_NG) self.button_group = QButtonGroup() self.button_group.addButton(r1, 1) self.button_group.addButton(r2, 2) self.button_group.addButton(r3, 3) self.button_group.addButton(r4, 4) self.button_group.addButton(r5, 5) self.connect(self.button_group, SIGNAL("buttonClicked(int)"), self.change_seed_class) #------------------------------------------------------------------- # buttons # btn_save = QPushButton('Save', self) # btn_save.clicked.connect(self.save) btn_quit = QPushButton("Quit", self) btn_quit.clicked.connect(self.quit) # btn_crop = QPushButton('Crop', self) # btn_crop.clicked.connect(self.crop) combo_dmask = QComboBox(self) combo_dmask.activated.connect(self.changeMask) self.mask_points_tab, aux = self.init_draw_mask(DRAW_MASK, mgrid) for icon, label in aux: combo_dmask.addItem(icon, label) self.slice_box.setMaskPoints( self.mask_points_tab[combo_dmask.currentIndex()]) self.status_bar = QStatusBar() vopts = [] vmenu = [] appmenu = [] # btn_recalc = QPushButton("Recalculate", self) # btn_recalc.clicked.connect(self.recalculate) # appmenu.append(QLabel('<b>Segmentation mode</b><br><br><br>' + # 'Select the region of interest<br>' + # 'using the mouse buttons.<br><br>')) # appmenu.append(btn_recalc) # appmenu.append(QLabel()) # self.volume_label = QLabel('Volume [mm3]:\n unknown') # appmenu.append(self.volume_label) # btn_crop = QPushButton("Crop", self) # btn_crop.clicked.connect(self.crop) # appmenu.append(btn_crop) btn_save = QPushButton("Save Seeds", self) btn_save.clicked.connect(self.saveSeeds) appmenu.append(btn_save) btn_del = QPushButton("Delete Seeds", self) btn_del.clicked.connect(self.deleteSliceSeeds) appmenu.append(btn_del) # combo_contour_options = ['fill', 'contours', 'off'] # combo_contour = QComboBox(self) # combo_contour.activated[str].connect(self.changeContourMode) # combo_contour.addItems(combo_contour_options) # self.changeContourMode(combo_contour_options[combo_contour.currentIndex()]) # vopts.append(QLabel('Selection mode:')) # vopts.append(combo_contour) # btn_reset = QPushButton("Reset Seeds", self) # btn_reset.clicked.connect(self.resetSliceDraw) # # appmenu.append(None) # appmenu.append(btn_reset) hbox = QHBoxLayout() vbox = QVBoxLayout() vbox_left = QVBoxLayout() vbox_app = QVBoxLayout() hbox.addWidget(self.slice_box) hbox.addWidget(self.slider) vbox_left.addWidget(self.slider.label) vbox_left.addWidget(self.view_label) vbox_left.addWidget(self.voxel_label) # vbox_left.addWidget(QLabel()) # vbox_left.addWidget(QLabel('View plane:')) # vbox_left.addWidget(combo_view) vbox_left.addWidget(self.get_line()) vbox_left.addWidget(self.slider_cw['c'].label) vbox_left.addWidget(self.slider_cw['c']) vbox_left.addWidget(self.slider_cw['w'].label) vbox_left.addWidget(self.slider_cw['w']) vbox_left.addWidget(self.get_line()) vbox_left.addWidget(QLabel('Drawing mask:')) vbox_left.addWidget(combo_dmask) for ii in vopts: vbox_left.addWidget(ii) for ii in vmenu: if ii is None: vbox_left.addStretch(1) else: vbox_left.addWidget(ii) for ii in appmenu: if ii is None: vbox_app.addStretch(1) else: vbox_app.addWidget(ii) vbox_left.addWidget(self.get_line()) vbox_left.addWidget(number_group) # vbox_app.addWidget(btn_crop) vbox_app.addStretch(1) # vbox_app.addWidget(btn_save) vbox_app.addWidget(btn_quit) hbox.addLayout(vbox_left) hbox.addWidget(self.get_line('v')) hbox.addLayout(vbox_app) vbox.addLayout(hbox) vbox.addWidget(self.status_bar) self.setLayout(vbox) self.setWindowTitle('Seed Editor') self.show() def __init__(self, img, seeds_fname='seeds.npy', actualSlice=0, seeds=None, contours=None, mode='seed', modeFun=None, voxelSize=[1, 1, 1]): """ Initiate Editor Parameters ---------- img : array DICOM data matrix. actualSlice : int Index of actual slice. seeds : array Seeds, user defined regions of interest. contours : array Computed segmentation. mode : str Editor modes: 'seed' - seed editor 'crop' - manual crop 'draw' - drawing modeFun : fun Mode function invoked by user button. voxelSize : tuple of float voxel size [mm] """ QDialog.__init__(self) self.mode = mode self.mode_fun = modeFun self.seeds_fname = seeds_fname # self.datapath = datapath self.actual_view = 'axial' self.act_transposition = VIEW_TABLE[self.actual_view] self.last_view_position = {} for ii in VIEW_TABLE.iterkeys(): self.last_view_position[ii] = img.shape[VIEW_TABLE[ii][-1]] - 1 self.img = img self.img_aview = self.img.transpose(self.act_transposition) self.actual_slice = self.img_aview.shape[-1] - actualSlice - 1 self.last_view_position[self.actual_view] = self.actual_slice # set contours self.contours = contours if self.contours is None: self.contours_aview = None else: self.contours_aview = self.contours.transpose( self.act_transposition) self.voxel_size = np.array(voxelSize) self.voxel_scale = self.voxel_size / float(np.min(self.voxel_size)) self.voxel_volume = np.prod(voxelSize) # set seeds if seeds is None: self.seeds = np.zeros(self.img.shape, np.int8) else: self.seeds = seeds self.seeds_aview = self.seeds.transpose(self.act_transposition) self.seeds_modified = False self.initUI(self.img_aview.shape, self.voxel_scale[np.array(self.act_transposition)], 600, mode) if mode == 'draw': self.seeds_orig = self.seeds.copy() self.slice_box.setEraseFun(self.eraseVolume) # set view window values C/W lb = np.min(img) ub = np.max(img) dul = ub - lb self.cw_range = {'c': [lb, ub], 'w': [1, dul]} self.slider_cw['c'].setRange(lb, ub) self.slider_cw['w'].setRange(1, dul) self.changeC(lb + dul / 2) self.changeW(dul) self.offset = np.zeros((3, ), dtype=np.int16) def change_seed_class(self, id): self.current_class = id self.slice_box.seed_mark = self.current_class # print 'Current seed class changed to ', id, '.' def showStatus(self, msg): self.status_bar.showMessage(QString(msg)) QApplication.processEvents() def init_draw_mask(self, draw_mask, grid): mask_points = [] mask_iconlabel = [] for mask, label in draw_mask: w, h = mask.shape xx, yy = mask.nonzero() mask_points.append((xx - w / 2, yy - h / 2)) img = QImage(w, h, QImage.Format_ARGB32) img.fill(qRgba(255, 255, 255, 0)) for ii in range(xx.shape[0]): img.setPixel(xx[ii], yy[ii], qRgba(0, 0, 0, 255)) img = img.scaled(QSize(w * grid[0], h * grid[1])) icon = QIcon(QPixmap.fromImage(img)) mask_iconlabel.append((icon, label)) return mask_points, mask_iconlabel def saveSliceSeeds(self): aux = self.slice_box.getSliceSeeds() if aux is not None: self.seeds_aview[..., self.actual_slice] = aux self.seeds_modified = True else: self.seeds_modified = False def saveSeeds(self): print 'Saving seeds array ... ', # aux = np.swapaxes(np.swapaxes(self.seeds_aview, 1, 2), 0, 1) aux = np.swapaxes(self.seeds_aview, 0, 2) np.save(self.seeds_fname, aux) print 'done' def updateCropBounds(self): crp = self.getCropBounds() if crp is not None: _, cri = crp self.contours = np.zeros(self.img.shape, np.int8) self.contours[cri].fill(1) self.contours_aview = self.contours.transpose( self.act_transposition) def focusSliceSlider(self): self.slider.setFocus(True) def sliderSelectSlice(self, value): self.selectSlice(self.n_slices - value) def scrollSlices(self, inc): if abs(inc) > 0: new = self.actual_slice + inc self.selectSlice(new) def selectSlice(self, value, force=False): if not (self.allow_select_slice): return if (value < 0) or (value >= self.n_slices): return if (value != self.actual_slice) or force: self.saveSliceSeeds() if self.seeds_modified and (self.mode == 'crop'): self.updateCropBounds() if self.contours is None: contours = None else: contours = self.contours_aview[..., value] slider_val = self.n_slices - value self.slider.setValue(slider_val) self.slider.label.setText('Slice: %d / %d' % (slider_val, self.n_slices)) self.slice_box.setSlice(self.img_aview[..., value], self.seeds_aview[..., value], contours) self.actual_slice = value def getSeeds(self): return self.seeds def getImg(self): return self.img def getOffset(self): return self.offset * self.voxel_size def getSeedsVal(self, label): return self.img[self.seeds == label] def getContours(self): return self.contours def setContours(self, contours): self.contours = contours self.contours_aview = self.contours.transpose(self.act_transposition) self.selectSlice(self.actual_slice) def changeCW(self, value, key): rg = self.cw_range[key] if (value < rg[0]) or (value > rg[1]): return if (value != self.slice_box.getCW()[key]): self.slider_cw[key].setValue(value) self.slider_cw[key].label.setText('%s: %d' % (key.upper(), value)) self.slice_box.setCW(value, key) self.slice_box.updateSliceCW(self.img_aview[..., self.actual_slice]) def changeC(self, value): self.changeCW(value, 'c') def changeW(self, value): self.changeCW(value, 'w') def setView(self, value): self.last_view_position[self.actual_view] = self.actual_slice # save seeds self.saveSliceSeeds() if self.seeds_modified and (self.mode == 'crop'): self.updateCropBounds(self.seeds_aview[..., self.actual_slice]) key = str(value) self.actual_view = key self.actual_slice = self.last_view_position[key] self.act_transposition = VIEW_TABLE[key] self.img_aview = self.img.transpose(self.act_transposition) self.seeds_aview = self.seeds.transpose(self.act_transposition) if self.contours is not None: self.contours_aview = self.contours.transpose( self.act_transposition) contours = self.contours_aview[..., self.actual_slice] else: contours = None vscale = self.voxel_scale[np.array(self.act_transposition)] height = self.slice_box.height() grid = height / float(self.img_aview.shape[1] * vscale[1]) mgrid = (grid * vscale[0], grid * vscale[1]) self.slice_box.resizeSlice(new_slice_size=self.img_aview.shape[:-1], new_grid=mgrid) self.slice_box.setSlice(self.img_aview[..., self.actual_slice], self.seeds_aview[..., self.actual_slice], contours) self.allow_select_slice = False self.n_slices = self.img_aview.shape[2] slider_val = self.n_slices - self.actual_slice self.slider.setValue(slider_val) self.slider.setRange(1, self.n_slices) self.allow_select_slice = True self.slider.label.setText('Slice: %d / %d' % (slider_val, self.n_slices)) self.view_label.setText('View size: %d x %d' % self.img_aview.shape[:-1]) def changeMask(self, val): self.slice_box.setMaskPoints(self.mask_points_tab[val]) def changeContourMode(self, val): self.slice_box.contour_mode = str(val) self.slice_box.updateSlice() def changeEraseMode(self, val): self.slice_box.erase_mode = str(val) def eraseVolume(self, pos, mode): self.showStatus("Processing...") xyz = pos + (self.actual_slice, ) p = tuple(np.array(xyz)[np.array(self.act_transposition)]) if self.seeds[p] > 0: if mode == 'inside': erase_reg(self.seeds, p, val=0) elif mode == 'outside': erase_reg(self.seeds, p, val=-1) idxs = np.where(self.seeds < 0) self.seeds.fill(0) self.seeds[idxs] = 1 if self.contours is None: contours = None else: contours = self.contours_aview[..., self.actual_slice] self.slice_box.setSlice(self.img_aview[..., self.actual_slice], self.seeds_aview[..., self.actual_slice], contours) self.showStatus("Done") def cropUpdate(self, img): for ii in VIEW_TABLE.iterkeys(): self.last_view_position[ii] = 0 self.actual_slice = 0 self.img = img self.img_aview = self.img.transpose(self.act_transposition) self.contours = None self.contours_aview = None self.seeds = np.zeros(self.img.shape, np.int8) self.seeds_aview = self.seeds.transpose(self.act_transposition) self.seeds_modified = False vscale = self.voxel_scale[np.array(self.act_transposition)] height = self.slice_box.height() grid = height / float(self.img_aview.shape[1] * vscale[1]) mgrid = (grid * vscale[0], grid * vscale[1]) self.slice_box.resizeSlice(new_slice_size=self.img_aview.shape[:-1], new_grid=mgrid) self.slice_box.setSlice(self.img_aview[..., self.actual_slice], self.seeds_aview[..., self.actual_slice], None) self.allow_select_slice = False self.n_slices = self.img_aview.shape[2] self.slider.setValue(self.actual_slice + 1) self.slider.setRange(1, self.n_slices) self.allow_select_slice = True self.slider.label.setText('Slice: %d / %d' % (self.actual_slice + 1, self.n_slices)) self.view_label.setText('View size: %d x %d' % self.img_aview.shape[:-1]) def getCropBounds(self): nzs = self.seeds.nonzero() cri = [] flag = True for ii in range(3): if nzs[ii].shape[0] == 0: flag = False break smin, smax = np.min(nzs[ii]), np.max(nzs[ii]) if smin == smax: flag = False break cri.append((smin, smax)) if flag: cri = np.array(cri) out = [] offset = [] for jj, ii in enumerate(cri): out.append(slice(ii[0], ii[1] + 1)) offset.append(ii[0]) return np.array(offset), tuple(out) else: return None def crop(self): self.showStatus("Processing...") crp = self.getCropBounds() if crp is not None: offset, cri = crp crop = self.img[cri] self.img = np.ascontiguousarray(crop) self.offset += offset self.showStatus('Done') else: self.showStatus('Region not selected!') self.cropUpdate(self.img) def recalculate(self, event): self.saveSliceSeeds() if np.abs(np.min(self.seeds) - np.max(self.seeds)) < 2: self.showStatus('At least two regions must be marked!') return self.showStatus("Processing...") # idx = 3 # s = random_walker(self.img[idx,:,:], self.seeds[idx,:,:])#, mode='cg_mg') # plt.figure() # plt.imshow(mark_boundaries(self.img[idx,:,:], s)) # plt.show() # self.segmentation = np.zeros(self.img.shape) # self.segmentation[idx,:,:] = s self.segmentation = random_walker(self.img, self.seeds, mode='cg_mg') self.setContours(self.segmentation - 1) self.selectSlice(self.actual_slice) # self.updateVolume() self.showStatus("Done") def deleteSliceSeeds(self, event): self.seeds_aview[..., self.actual_slice] = 0 self.slice_box.setSlice(seeds=self.seeds_aview[..., self.actual_slice]) self.slice_box.updateSlice() def resetSliceDraw(self, event): seeds_orig_aview = self.seeds_orig.transpose(self.act_transposition) self.seeds_aview[..., self.actual_slice] = seeds_orig_aview[ ..., self.actual_slice] self.slice_box.setSlice(seeds=self.seeds_aview[..., self.actual_slice]) self.slice_box.updateSlice() def quit(self, event): self.close() # def save(self, event): # odp = os.path.expanduser("~/lisa_data") # if not op.exists(odp): # os.makedirs(odp) # # data = self.export() # # data['version'] = self.version # # data['experiment_caption'] = self.experiment_caption # # data['lisa_operator_identifier'] = self.lisa_operator_identifier # pth, filename = op.split(op.normpath(self.datapath)) # # filename += "-" + self.experiment_caption # filepath = 'org-' + filename + '.pklz' # filepath = op.join(odp, filepath) # filepath = misc.suggest_filename(filepath) # misc.obj_to_file(data, filepath, filetype='pklz') # # filepath = 'organ_last.pklz' # filepath = op.join(odp, filepath) # misc.obj_to_file(data, filepath, filetype='pklz') # def export(self): # slab = {} # slab['none'] = 0 # slab['liver'] = 1 # slab['lesions'] = 6 # slab.update(self.slab) # # data = {} # data['version'] = (1, 0, 1) # data['data3d'] = self.img # # data['crinfo'] = self.crinfo # data['segmentation'] = self.segmentation # data['slab'] = slab # # data['voxelsize_mm'] = self.voxelsize_mm # # data['orig_shape'] = self.orig_shape # # data['processing_time'] = self.processing_time # return data def updateVolume(self): text = 'Volume [mm3]:\n unknown' if self.voxel_volume is not None: if self.mode == 'draw': vd = self.seeds else: vd = self.contours if vd is not None: nzs = vd.nonzero() nn = nzs[0].shape[0] text = 'Volume [mm3]:\n %.2e' % (nn * self.voxel_volume) self.volume_label.setText(text) def getROI(self): crp = self.getCropBounds() if crp is not None: _, cri = crp else: cri = [] for jj, ii in enumerate(self.img.shape): off = self.offset[jj] cri.append(slice(off, off + ii)) return cri
class QTSeedEditor(QDialog): """ DICOM viewer. """ @staticmethod def get_line(mode='h'): line = QFrame() if mode == 'h': line.setFrameStyle(QFrame.HLine) elif mode == 'v': line.setFrameStyle(QFrame.VLine) line.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) return line def initUI(self, shape, vscale, height=600, mode='seed'): """ Initialize UI. Parameters ---------- shape : (int, int, int) Shape of data matrix. vscale : (float, float, float) Voxel scaling. height : int Maximal slice height in pixels. mode : str Editor mode. """ # picture grid = height / float(shape[1] * vscale[1]) mgrid = (grid * vscale[0], grid * vscale[1]) self.slice_box = SliceBox(shape[:-1], mgrid, mode) self.slice_box.setScrollFun(self.scrollSlices) self.connect(self.slice_box, SIGNAL('focus_slider'), self.focusSliceSlider) # sliders self.allow_select_slice = True self.n_slices = shape[2] self.slider = QSlider(Qt.Vertical) self.slider.label = QLabel() self.slider.label.setText('Slice: %d / %d' % (self.actual_slice, self.n_slices)) self.slider.setRange(1, self.n_slices) self.slider.valueChanged.connect(self.sliderSelectSlice) self.slider.setValue(self.actual_slice) self.slider_cw = {} self.slider_cw['c'] = QSlider(Qt.Horizontal) self.slider_cw['c'].valueChanged.connect(self.changeC) self.slider_cw['c'].label = QLabel() self.slider_cw['w'] = QSlider(Qt.Horizontal) self.slider_cw['w'].valueChanged.connect(self.changeW) self.slider_cw['w'].label = QLabel() self.view_label = QLabel('View size: %d x %d' % self.img_aview.shape[:-1]) self.voxel_label = QLabel('Voxel size [mm]:\n %.2f x %.2f x %.2f'\ % tuple(self.voxel_size[np.array(self.act_transposition)])) combo_view_options = VIEW_TABLE.keys() combo_view = QComboBox(self) combo_view.activated[str].connect(self.setView) combo_view.addItems(combo_view_options) # buttons self.btn_quit = QPushButton("Return", self) self.btn_quit.clicked.connect(self.quit) combo_dmask = QComboBox(self) combo_dmask.activated.connect(self.changeMask) self.mask_points_tab, aux = self.init_draw_mask(DRAW_MASK, mgrid) for icon, label in aux: combo_dmask.addItem(icon, label) self.slice_box.setMaskPoints( self.mask_points_tab[combo_dmask.currentIndex()]) self.status_bar = QStatusBar() self.seeds_copy = None vopts = [] vmenu = [] appmenu = [] if mode == 'seed' and self.mode_fun is not None: btn_recalc = QPushButton("Recalculate", self) btn_recalc.clicked.connect(self.recalculate) self.btn_save = QPushButton("Save seeds", self) self.btn_save.clicked.connect(self.saveload_seeds) btn_s2b = QPushButton("Seg. to bckgr.", self) btn_s2b.clicked.connect(self.seg_to_background_seeds) btn_s2f = QPushButton("Seg. to forgr.", self) btn_s2f.clicked.connect(self.seg_to_foreground_seeds) appmenu.append( QLabel('<b>Segmentation mode</b><br><br><br>' + 'Select the region of interest<br>' + 'using the mouse buttons:<br><br>' + ' <i>left</i> - inner region<br>' + ' <i>right</i> - outer region<br><br>')) appmenu.append(btn_recalc) appmenu.append(self.btn_save) appmenu.append(btn_s2f) appmenu.append(btn_s2b) appmenu.append(QLabel()) self.volume_label = QLabel('Volume:\n unknown') appmenu.append(self.volume_label) # Set middle pencil as default (M. Jirik) combo_dmask.setCurrentIndex(1) self.slice_box.setMaskPoints( self.mask_points_tab[combo_dmask.currentIndex()]) # -----mjirik---end------ if mode == 'seed' or mode == 'crop'\ or mode == 'mask' or mode == 'draw': combo_seed_label_options = ['all', '1', '2', '3', '4'] combo_seed_label = QComboBox(self) combo_seed_label.activated[str].connect(self.changeFocusedLabel) combo_seed_label.addItems(combo_seed_label_options) self.changeFocusedLabel( combo_seed_label_options[combo_seed_label.currentIndex()]) # vopts.append(QLabel('Label to delete:')) # vopts.append(combo_seed_label) vmenu.append(QLabel('Label to delete:')) vmenu.append(combo_seed_label) btn_del = QPushButton("Del Slice Seeds", self) btn_del.clicked.connect(self.deleteSliceSeeds) vmenu.append(None) vmenu.append(btn_del) btn_del = QPushButton("Del All Seeds", self) btn_del.clicked.connect(self.deleteSeedsInAllImage) vmenu.append(None) vmenu.append(btn_del) combo_contour_options = ['fill', 'contours', 'off'] combo_contour = QComboBox(self) combo_contour.activated[str].connect(self.changeContourMode) combo_contour.addItems(combo_contour_options) self.changeContourMode( combo_contour_options[combo_contour.currentIndex()]) vopts.append(QLabel('Selection mode:')) vopts.append(combo_contour) if mode == 'mask': btn_recalc_mask = QPushButton("Recalculate mask", self) btn_recalc_mask.clicked.connect(self.updateMaskRegion_btn) btn_all = QPushButton("Select all", self) btn_all.clicked.connect(self.maskSelectAll) btn_reset = QPushButton("Reset selection", self) btn_reset.clicked.connect(self.resetSelection) btn_reset_seads = QPushButton("Reset seads", self) btn_reset_seads.clicked.connect(self.resetSeads) btn_add = QPushButton("Add selection", self) btn_add.clicked.connect(self.maskAddSelection) btn_rem = QPushButton("Remove selection", self) btn_rem.clicked.connect(self.maskRemoveSelection) btn_mask = QPushButton("Mask region", self) btn_mask.clicked.connect(self.maskRegion) appmenu.append( QLabel('<b>Mask mode</b><br><br><br>' + 'Select the region to mask<br>' + 'using the left mouse button<br><br>')) appmenu.append(self.get_line('h')) appmenu.append(btn_recalc_mask) appmenu.append(btn_all) appmenu.append(btn_reset) appmenu.append(btn_reset_seads) appmenu.append(self.get_line('h')) appmenu.append(btn_add) appmenu.append(btn_rem) appmenu.append(self.get_line('h')) appmenu.append(btn_mask) appmenu.append(self.get_line('h')) self.mask_qhull = None if mode == 'crop': btn_crop = QPushButton("Crop", self) btn_crop.clicked.connect(self.crop) appmenu.append( QLabel('<b>Crop mode</b><br><br><br>' + 'Select the crop region<br>' + 'using the left mouse button<br><br>')) appmenu.append(btn_crop) if mode == 'draw': appmenu.append( QLabel('<b>Manual segmentation<br> mode</b><br><br><br>' + 'Mark the region of interest<br>' + 'using the mouse buttons:<br><br>' + ' <i>left</i> - draw<br>' + ' <i>right</i> - erase<br>' + ' <i>middle</i> - vol. erase<br><br>')) btn_reset = QPushButton("Reset", self) btn_reset.clicked.connect(self.resetSliceDraw) vmenu.append(None) vmenu.append(btn_reset) combo_erase_options = ['inside', 'outside'] combo_erase = QComboBox(self) combo_erase.activated[str].connect(self.changeEraseMode) combo_erase.addItems(combo_erase_options) self.changeEraseMode( combo_erase_options[combo_erase.currentIndex()]) vopts.append(QLabel('Volume erase mode:')) vopts.append(combo_erase) hbox = QHBoxLayout() vbox = QVBoxLayout() vbox_left = QVBoxLayout() vbox_app = QVBoxLayout() hbox.addWidget(self.slice_box) hbox.addWidget(self.slider) vbox_left.addWidget(self.slider.label) vbox_left.addWidget(self.view_label) vbox_left.addWidget(self.voxel_label) vbox_left.addWidget(QLabel()) vbox_left.addWidget(QLabel('View plane:')) vbox_left.addWidget(combo_view) vbox_left.addWidget(self.get_line()) vbox_left.addWidget(self.slider_cw['c'].label) vbox_left.addWidget(self.slider_cw['c']) vbox_left.addWidget(self.slider_cw['w'].label) vbox_left.addWidget(self.slider_cw['w']) vbox_left.addWidget(self.get_line()) vbox_left.addWidget(QLabel('Drawing mask:')) vbox_left.addWidget(combo_dmask) for ii in vopts: vbox_left.addWidget(ii) for ii in vmenu: if ii is None: vbox_left.addStretch(1) else: vbox_left.addWidget(ii) for ii in appmenu: if ii is None: vbox_app.addStretch(1) else: vbox_app.addWidget(ii) vbox_app.addStretch(1) vbox_app.addWidget(self.btn_quit) hbox.addLayout(vbox_left) hbox.addWidget(self.get_line('v')) hbox.addLayout(vbox_app) vbox.addLayout(hbox) vbox.addWidget(self.status_bar) self.my_layout = vbox self.setLayout(vbox) self.setWindowTitle('Segmentation Editor') self.show() def __init__(self, img, viewPositions=None, seeds=None, contours=None, mode='seed', modeFun=None, voxelSize=[1, 1, 1], volume_unit='mm3'): """ Initiate Editor Parameters ---------- img : array DICOM data matrix. actualSlice : int Index of actual slice. seeds : array Seeds, user defined regions of interest. contours : array Computed segmentation. mode : str Editor modes: 'seed' - seed editor 'crop' - manual crop 'draw' - drawing 'mask' - mask region modeFun : fun Mode function invoked by user button. voxelSize : tuple of float voxel size [mm] volume_unit : allow select output volume in mililiters or mm3 [mm, ml] """ QDialog.__init__(self) self.BACKGROUND_NOMODEL_SEED_LABEL = 4 self.FOREGROUND_NOMODEL_SEED_LABEL = 3 self.mode = mode self.mode_fun = modeFun self.actual_view = 'axial' self.act_transposition = VIEW_TABLE[self.actual_view] self.img = img self.img_aview = self.img.transpose(self.act_transposition) self.volume_unit = volume_unit self.last_view_position = {} for jj, ii in enumerate(VIEW_TABLE.keys()): if viewPositions is None: viewpos = img.shape[VIEW_TABLE[ii][-1]] / 2 else: viewpos = viewPositions[jj] self.last_view_position[ii] =\ img.shape[VIEW_TABLE[ii][-1]] - viewpos - 1 self.actual_slice = self.last_view_position[self.actual_view] # set contours self.contours = contours if self.contours is None: self.contours_aview = None else: self.contours_aview = self.contours.transpose( self.act_transposition) # masked data - has information about which data were removed # 1 == enabled, 0 == deleted # How to return: # editorDialog.exec_() # masked_data = editorDialog.masked self.masked = np.ones(self.img.shape, np.int8) self.voxel_size = np.squeeze(np.asarray(voxelSize)) self.voxel_scale = self.voxel_size / float(np.min(self.voxel_size)) self.voxel_volume = np.prod(voxelSize) # set seeds if seeds is None: self.seeds = np.zeros(self.img.shape, np.int8) else: self.seeds = seeds self.seeds_aview = self.seeds.transpose(self.act_transposition) self.seeds_modified = False self.initUI(self.img_aview.shape, self.voxel_scale[np.array(self.act_transposition)], 600, mode) if mode == 'draw': self.seeds_orig = self.seeds.copy() self.slice_box.setEraseFun(self.eraseVolume) # set view window values C/W lb = np.min(img) self.img_min_val = lb ub = np.max(img) dul = np.double(ub) - np.double(lb) self.cw_range = {'c': [lb, ub], 'w': [1, dul]} self.slider_cw['c'].setRange(lb, ub) self.slider_cw['w'].setRange(1, dul) self.changeC(lb + dul / 2) self.changeW(dul) self.offset = np.zeros((3, ), dtype=np.int16) # set what labels will be deleted by 'delete seeds' button self.textFocusedLabel = "all" def showStatus(self, msg): self.status_bar.showMessage(QString(msg)) QApplication.processEvents() def init_draw_mask(self, draw_mask, grid): mask_points = [] mask_iconlabel = [] for mask, label in draw_mask: w, h = mask.shape xx, yy = mask.nonzero() mask_points.append((xx - w / 2, yy - h / 2)) img = QImage(w, h, QImage.Format_ARGB32) img.fill(qRgba(255, 255, 255, 0)) for ii in range(xx.shape[0]): img.setPixel(xx[ii], yy[ii], qRgba(0, 0, 0, 255)) img = img.scaled(QSize(w * grid[0], h * grid[1])) icon = QIcon(QPixmap.fromImage(img)) mask_iconlabel.append((icon, label)) return mask_points, mask_iconlabel def saveSliceSeeds(self): aux = self.slice_box.getSliceSeeds() if aux is not None: self.seeds_aview[..., self.actual_slice] = aux self.seeds_modified = True else: self.seeds_modified = False def updateMaskRegion_btn(self): self.saveSliceSeeds() self.updateMaskRegion() def updateMaskRegion(self): crp = self.getCropBounds(return_nzs=True) if crp is not None: off, cri, nzs = crp if nzs[0].shape[0] <= 5: self.showStatus("Not enough points (need >= 5)!") else: points = np.transpose(nzs) hull = Delaunay(points) X, Y, Z = np.mgrid[cri[0], cri[1], cri[2]] grid = np.vstack([X.ravel(), Y.ravel(), Z.ravel()]).T simplex = hull.find_simplex(grid) fill = grid[simplex >= 0, :] fill = (fill[:, 0], fill[:, 1], fill[:, 2]) if self.contours is None or self.contours_old is None: self.contours = np.zeros(self.img.shape, np.int8) self.contours_old = self.contours.copy() else: self.contours[self.contours != 2] = 0 self.contours[fill] = 1 self.contours_aview = self.contours.transpose( self.act_transposition) self.selectSlice(self.actual_slice) def maskRegion(self): self.masked[self.contours == 0] = 0 self.img[self.contours != 2] = self.img_min_val self.contours.fill(0) self.contours_old = self.contours.copy() self.seeds.fill(0) self.selectSlice(self.actual_slice) def maskAddSelection(self): self.updateMaskRegion() if self.contours is None: return self.contours[self.contours == 1] = 2 self.contours_old = self.contours.copy() self.seeds.fill(0) self.selectSlice(self.actual_slice) def maskRemoveSelection(self): self.updateMaskRegion() if self.contours is None: return self.contours[self.contours == 1] = 0 self.contours_old = self.contours.copy() self.seeds.fill(0) self.selectSlice(self.actual_slice) def maskSelectAll(self): self.updateMaskRegion() self.seeds[0][0][0] = 1 self.seeds[0][0][-1] = 1 self.seeds[0][-1][0] = 1 self.seeds[0][-1][-1] = 1 self.seeds[-1][0][0] = 1 self.seeds[-1][0][-1] = 1 self.seeds[-1][-1][0] = 1 self.seeds[-1][-1][-1] = 1 self.updateMaskRegion() self.selectSlice(self.actual_slice) def resetSelection(self): self.updateMaskRegion() if self.contours is None: return self.contours.fill(0) self.contours_old = self.contours.copy() self.seeds.fill(0) self.selectSlice(self.actual_slice) def resetSeads(self): self.seeds.fill(0) if self.contours is not None: self.contours = self.contours_old.copy() self.contours_aview = self.contours.transpose( self.act_transposition) self.updateMaskRegion() self.selectSlice(self.actual_slice) def updateCropBounds(self): crp = self.getCropBounds() if crp is not None: _, cri = crp self.contours = np.zeros(self.img.shape, np.int8) self.contours[cri].fill(1) self.contours_aview = self.contours.transpose( self.act_transposition) def focusSliceSlider(self): self.slider.setFocus(True) def sliderSelectSlice(self, value): self.selectSlice(self.n_slices - value) def scrollSlices(self, inc): if abs(inc) > 0: new = self.actual_slice + inc self.selectSlice(new) def selectSlice(self, value, force=False): if not (self.allow_select_slice): return if (value < 0) or (value >= self.n_slices): return if (value != self.actual_slice) or force: self.saveSliceSeeds() if self.seeds_modified: if self.mode == 'crop': self.updateCropBounds() elif self.mode == 'mask': self.updateMaskRegion() if self.contours is None: contours = None else: contours = self.contours_aview[..., value] slider_val = self.n_slices - value self.slider.setValue(slider_val) self.slider.label.setText('Slice: %d / %d' % (slider_val, self.n_slices)) self.slice_box.setSlice(self.img_aview[..., value], self.seeds_aview[..., value], contours) self.actual_slice = value def getSeeds(self): return self.seeds def getImg(self): return self.img def getOffset(self): return self.offset * self.voxel_size def getSeedsVal(self, label): return self.img[self.seeds == label] def getContours(self): return self.contours def setContours(self, contours): """ store segmentation :param contours: segmentation :return: Nothing """ """ :param contours: :return: """ self.contours = contours self.contours_aview = self.contours.transpose(self.act_transposition) self.selectSlice(self.actual_slice) def changeCW(self, value, key): rg = self.cw_range[key] if (value < rg[0]) or (value > rg[1]): return if (value != self.slice_box.getCW()[key]): self.slider_cw[key].setValue(value) self.slider_cw[key].label.setText('%s: %d' % (key.upper(), value)) self.slice_box.setCW(value, key) self.slice_box.updateSliceCW(self.img_aview[..., self.actual_slice]) def changeC(self, value): self.changeCW(value, 'c') def changeW(self, value): self.changeCW(value, 'w') def setView(self, value): self.last_view_position[self.actual_view] = self.actual_slice # save seeds self.saveSliceSeeds() if self.seeds_modified: if self.mode == 'crop': self.updateCropBounds() elif self.mode == 'mask': self.updateMaskRegion() key = str(value) self.actual_view = key self.actual_slice = self.last_view_position[key] self.act_transposition = VIEW_TABLE[key] self.img_aview = self.img.transpose(self.act_transposition) self.seeds_aview = self.seeds.transpose(self.act_transposition) if self.contours is not None: self.contours_aview = self.contours.transpose( self.act_transposition) contours = self.contours_aview[..., self.actual_slice] else: contours = None vscale = self.voxel_scale[np.array(self.act_transposition)] height = self.slice_box.height() grid = height / float(self.img_aview.shape[1] * vscale[1]) # width = (self.img_aview.shape[0] * vscale[0])[0] # if width > 800: # height = 400 # grid = height / float(self.img_aview.shape[1] * vscale[1]) mgrid = (grid * vscale[0], grid * vscale[1]) self.slice_box.resizeSlice(new_slice_size=self.img_aview.shape[:-1], new_grid=mgrid) self.slice_box.setSlice(self.img_aview[..., self.actual_slice], self.seeds_aview[..., self.actual_slice], contours) self.allow_select_slice = False self.n_slices = self.img_aview.shape[2] slider_val = self.n_slices - self.actual_slice self.slider.setRange(1, self.n_slices) self.slider.setValue(slider_val) self.allow_select_slice = True self.slider.label.setText('Slice: %d / %d' % (slider_val, self.n_slices)) self.view_label.setText('View size: %d x %d' % self.img_aview.shape[:-1]) self.adjustSize() self.adjustSize() def changeMask(self, val): self.slice_box.setMaskPoints(self.mask_points_tab[val]) def changeContourMode(self, val): self.slice_box.contour_mode = str(val) self.slice_box.updateSlice() def changeEraseMode(self, val): self.slice_box.erase_mode = str(val) def eraseVolume(self, pos, mode): self.showStatus("Processing...") xyz = np.array(pos + (self.actual_slice, )) p = np.zeros_like(xyz) p[np.array(self.act_transposition)] = xyz p = tuple(p) if self.seeds[p] > 0: if mode == 'inside': erase_reg(self.seeds, p, val=0) elif mode == 'outside': erase_reg(self.seeds, p, val=-1) idxs = np.where(self.seeds < 0) self.seeds.fill(0) self.seeds[idxs] = 1 if self.contours is None: contours = None else: contours = self.contours_aview[..., self.actual_slice] self.slice_box.setSlice(self.img_aview[..., self.actual_slice], self.seeds_aview[..., self.actual_slice], contours) self.showStatus("Done") def cropUpdate(self, img): for ii in VIEW_TABLE.keys(): self.last_view_position[ii] = 0 self.actual_slice = 0 self.img = img self.img_aview = self.img.transpose(self.act_transposition) self.contours = None self.contours_aview = None self.seeds = np.zeros(self.img.shape, np.int8) self.seeds_aview = self.seeds.transpose(self.act_transposition) self.seeds_modified = False vscale = self.voxel_scale[np.array(self.act_transposition)] height = self.slice_box.height() grid = height / float(self.img_aview.shape[1] * vscale[1]) mgrid = (grid * vscale[0], grid * vscale[1]) self.slice_box.resizeSlice(new_slice_size=self.img_aview.shape[:-1], new_grid=mgrid) self.slice_box.setSlice(self.img_aview[..., self.actual_slice], self.seeds_aview[..., self.actual_slice], None) self.allow_select_slice = False self.n_slices = self.img_aview.shape[2] self.slider.setValue(self.actual_slice + 1) self.slider.setRange(1, self.n_slices) self.allow_select_slice = True self.slider.label.setText('Slice: %d / %d' % (self.actual_slice + 1, self.n_slices)) self.view_label.setText('View size: %d x %d' % self.img_aview.shape[:-1]) def getCropBounds(self, return_nzs=False, flat=False): nzs = self.seeds.nonzero() cri = [] flag = True for ii in range(3): if nzs[ii].shape[0] == 0: flag = False break smin, smax = np.min(nzs[ii]), np.max(nzs[ii]) if not (flat): if smin == smax: flag = False break cri.append((smin, smax)) if flag: cri = np.array(cri) out = [] offset = [] for jj, ii in enumerate(cri): out.append(slice(ii[0], ii[1] + 1)) offset.append(ii[0]) if return_nzs: return np.array(offset), tuple(out), nzs else: return np.array(offset), tuple(out) else: return None def crop(self): self.showStatus("Processing...") crp = self.getCropBounds() if crp is not None: offset, cri = crp crop = self.img[cri] self.img = np.ascontiguousarray(crop) self.offset += offset self.showStatus('Done') else: self.showStatus('Region not selected!') self.cropUpdate(self.img) def seg_to_background_seeds(self, event): self.saveSliceSeeds() self.seeds[self.seeds < 3] = 0 from PyQt4.QtCore import pyqtRemoveInputHook # pyqtRemoveInputHook() # import ipdb; ipdb.set_trace() self.seeds[(self.contours == 1) & (self.seeds < 3)] = self.BACKGROUND_NOMODEL_SEED_LABEL self.contours[...] = 0 def seg_to_foreground_seeds(self, event): self.saveSliceSeeds() self.seeds[self.seeds < 3] = 0 # from PyQt4.QtCore import pyqtRemoveInputHook # pyqtRemoveInputHook() # import ipdb; ipdb.set_trace() self.seeds[(self.contours == 1) & (self.seeds < 3)] = self.FOREGROUND_NOMODEL_SEED_LABEL self.contours[...] = 0 def saveload_seeds(self, event): if self.seeds_copy is None: self.seeds_copy = self.seeds.copy() self.seeds[...] = 0 # print "save" # from PyQt4.QtCore import pyqtRemoveInputHook # pyqtRemoveInputHook() # import ipdb; ipdb.set_trace() self.btn_save.setText("Load seeds") else: # from PyQt4.QtCore import pyqtRemoveInputHook # pyqtRemoveInputHook() # import ipdb; ipdb.set_trace() self.seeds[self.seeds_copy > 0] = self.seeds_copy[ self.seeds_copy > 0] self.seeds_copy = None self.btn_save.setText("Save seeds") def recalculate(self, event): self.saveSliceSeeds() if np.abs(np.min(self.seeds) - np.max(self.seeds)) < 2: self.showStatus('Inner and outer regions not defined!') return self.showStatus("Processing...") self.mode_fun(self) self.selectSlice(self.actual_slice) self.updateVolume() self.showStatus("Done") def changeFocusedLabel(self, textlabel): self.textFocusedLabel = textlabel # logger # print " lakjlfkj ", textlabel logger.debug(self.textFocusedLabel) def deleteSliceSeeds(self, event): if self.textFocusedLabel == 'all': self.seeds_aview[..., self.actual_slice] = 0 else: # delete only seeds with specific label self.seeds_aview[self.seeds_aview[..., self.actual_slice] == int(self.textFocusedLabel), self.actual_slice] = 0 self.slice_box.setSlice(seeds=self.seeds_aview[..., self.actual_slice]) self.slice_box.updateSlice() def deleteSeedsInAllImage(self, event): if self.textFocusedLabel == 'all': self.seeds_aview[...] = 0 else: # delete only seeds with specific label self.seeds_aview[self.seeds_aview[...] == int( self.textFocusedLabel)] = 0 self.slice_box.setSlice(seeds=self.seeds_aview[..., self.actual_slice]) self.slice_box.updateSlice() def resetSliceDraw(self, event): seeds_orig_aview = self.seeds_orig.transpose(self.act_transposition) self.seeds_aview[..., self.actual_slice] = seeds_orig_aview[ ..., self.actual_slice] self.slice_box.setSlice(seeds=self.seeds_aview[..., self.actual_slice]) self.slice_box.updateSlice() def quit(self, event): self.close() def updateVolume(self): text = 'Volume:\n unknown' if self.voxel_volume is not None: if self.mode == 'draw': vd = self.seeds else: vd = self.contours if vd is not None: nzs = vd.nonzero() nn = nzs[0].shape[0] if self.volume_unit == 'ml': text = 'Volume [ml]:\n %.2f' %\ (nn * self.voxel_volume / 1000) else: text = 'Volume [mm3]:\n %.2e' % (nn * self.voxel_volume) self.volume_label.setText(text) def getROI(self): crp = self.getCropBounds() if crp is not None: _, cri = crp else: cri = [] for jj, ii in enumerate(self.img.shape): off = self.offset[jj] cri.append(slice(off, off + ii)) return cri
class QTSeedEditor(QDialog): """ DICOM viewer. """ @staticmethod def get_line(mode='h'): line = QFrame() if mode == 'h': line.setFrameStyle(QFrame.HLine) elif mode == 'v': line.setFrameStyle(QFrame.VLine) line.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) return line def initUI(self, shape, vscale, height=600, mode='seed'): """ Initialize UI. Parameters ---------- shape : (int, int, int) Shape of data matrix. vscale : (float, float, float) Voxel scaling. height : int Maximal slice height in pixels. mode : str Editor mode. """ self.slab = {} # picture grid = height / float(shape[1] * vscale[1]) mgrid = (grid * vscale[0], grid * vscale[1]) self.slice_box = SliceBox(shape[:-1], mgrid, mode) self.slice_box.setScrollFun(self.scrollSlices) self.connect(self.slice_box, SIGNAL('focus_slider'), self.focusSliceSlider) # sliders self.allow_select_slice = True self.n_slices = shape[2] self.slider = QSlider(Qt.Vertical) self.slider.valueChanged.connect(self.sliderSelectSlice) self.slider.label = QLabel() self.slider.setRange(1, self.n_slices) self.slider_cw = {} self.slider_cw['c'] = QSlider(Qt.Horizontal) self.slider_cw['c'].valueChanged.connect(self.changeC) self.slider_cw['c'].label = QLabel() self.slider_cw['w'] = QSlider(Qt.Horizontal) self.slider_cw['w'].valueChanged.connect(self.changeW) self.slider_cw['w'].label = QLabel() self.view_label = QLabel('View size: %d x %d' % self.img_aview.shape[:-1]) self.voxel_label = QLabel('Voxel size [mm]:\n %.2f x %.2f x %.2f'\ % tuple(self.voxel_size[np.array(self.act_transposition)])) combo_view_options = VIEW_TABLE.keys() combo_view = QComboBox(self) combo_view.activated[str].connect(self.setView) combo_view.addItems(combo_view_options) #radio button group for choosing seed class ------------------------ self.current_class = 1 self.slice_box.seed_mark = self.current_class number_group = QGroupBox(QString('Class markers')) vbox_NG = QVBoxLayout() r1 = QRadioButton('class 1') r1.setStyleSheet('QRadioButton {color: red}') r1.setChecked(True) r2 = QRadioButton('class 2') r2.setStyleSheet('QRadioButton {color: green}') r3 = QRadioButton('class 3') r3.setStyleSheet('QRadioButton {color: blue}') r4 = QRadioButton('class 4') r4.setStyleSheet('QRadioButton {color: cyan}') r5 = QRadioButton('class 5') r5.setStyleSheet('QRadioButton {color: magenta}') vbox_NG.addWidget(r1) vbox_NG.addWidget(r2) vbox_NG.addWidget(r3) vbox_NG.addWidget(r4) vbox_NG.addWidget(r5) number_group.setLayout(vbox_NG) self.button_group = QButtonGroup() self.button_group.addButton(r1, 1) self.button_group.addButton(r2, 2) self.button_group.addButton(r3, 3) self.button_group.addButton(r4, 4) self.button_group.addButton(r5, 5) self.connect(self.button_group, SIGNAL("buttonClicked(int)"), self.change_seed_class) #------------------------------------------------------------------- # buttons # btn_save = QPushButton('Save', self) # btn_save.clicked.connect(self.save) btn_quit = QPushButton("Quit", self) btn_quit.clicked.connect(self.quit) # btn_crop = QPushButton('Crop', self) # btn_crop.clicked.connect(self.crop) combo_dmask = QComboBox(self) combo_dmask.activated.connect(self.changeMask) self.mask_points_tab, aux = self.init_draw_mask(DRAW_MASK, mgrid) for icon, label in aux: combo_dmask.addItem(icon, label) self.slice_box.setMaskPoints(self.mask_points_tab[combo_dmask.currentIndex()]) self.status_bar = QStatusBar() vopts = [] vmenu = [] appmenu = [] if mode == 'seed': btn_recalc = QPushButton("Recalculate", self) btn_recalc.clicked.connect(self.recalculate) appmenu.append(QLabel('<b>Segmentation mode</b><br><br><br>' + 'Select the region of interest<br>' + 'using the mouse buttons.<br><br>')) appmenu.append(btn_recalc) appmenu.append(QLabel()) self.volume_label = QLabel('Volume [mm3]:\n unknown') appmenu.append(self.volume_label) if mode == 'seed' or mode == 'crop': btn_del = QPushButton("Delete Seeds", self) btn_del.clicked.connect(self.deleteSliceSeeds) vmenu.append(None) vmenu.append(btn_del) combo_contour_options = ['fill', 'contours', 'off'] combo_contour = QComboBox(self) combo_contour.activated[str].connect(self.changeContourMode) combo_contour.addItems(combo_contour_options) self.changeContourMode(combo_contour_options[combo_contour.currentIndex()]) vopts.append(QLabel('Selection mode:')) vopts.append(combo_contour) if mode == 'crop': btn_crop = QPushButton("Crop", self) btn_crop.clicked.connect(self.crop) appmenu.append(QLabel('<b>Crop mode</b><br><br><br>' + 'Select the crop region<br>' + 'using the left mouse button<br><br>')) appmenu.append(btn_crop) # if mode == 'draw': # appmenu.append(QLabel('<b>Manual segmentation<br> mode</b><br><br><br>' + # 'Mark the region of interest<br>' + # 'using the mouse buttons:<br><br>' + # ' <i>left</i> - draw<br>' + # ' <i>right</i> - erase<br>' + # ' <i>middle</i> - vol. erase<br><br>')) # # btn_reset = QPushButton("Reset", self) # btn_reset.clicked.connect(self.resetSliceDraw) # vmenu.append(None) # vmenu.append(btn_reset) # # combo_erase_options = ['inside', 'outside'] # combo_erase = QComboBox(self) # combo_erase.activated[str].connect(self.changeEraseMode) # combo_erase.addItems(combo_erase_options) # self.changeEraseMode(combo_erase_options[combo_erase.currentIndex()]) # vopts.append(QLabel('Volume erase mode:')) # vopts.append(combo_erase) hbox = QHBoxLayout() vbox = QVBoxLayout() vbox_left = QVBoxLayout() vbox_app = QVBoxLayout() hbox.addWidget(self.slice_box) hbox.addWidget(self.slider) vbox_left.addWidget(self.slider.label) vbox_left.addWidget(self.view_label) vbox_left.addWidget(self.voxel_label) vbox_left.addWidget(QLabel()) vbox_left.addWidget(QLabel('View plane:')) vbox_left.addWidget(combo_view) vbox_left.addWidget(self.get_line()) vbox_left.addWidget(self.slider_cw['c'].label) vbox_left.addWidget(self.slider_cw['c']) vbox_left.addWidget(self.slider_cw['w'].label) vbox_left.addWidget(self.slider_cw['w']) vbox_left.addWidget(self.get_line()) vbox_left.addWidget(QLabel('Drawing mask:')) vbox_left.addWidget(combo_dmask) for ii in vopts: vbox_left.addWidget(ii) for ii in vmenu: if ii is None: vbox_left.addStretch(1) else: vbox_left.addWidget(ii) for ii in appmenu: if ii is None: vbox_app.addStretch(1) else: vbox_app.addWidget(ii) vbox_left.addWidget(self.get_line()) vbox_left.addWidget(number_group) # vbox_app.addWidget(btn_crop) vbox_app.addStretch(1) # vbox_app.addWidget(btn_save) vbox_app.addWidget(btn_quit) hbox.addLayout(vbox_left) hbox.addWidget(self.get_line('v')) hbox.addLayout(vbox_app) vbox.addLayout(hbox) vbox.addWidget(self.status_bar) self.setLayout(vbox) self.setWindowTitle('Segmentation Editor') self.show() def __init__(self, img, actualSlice=0, seeds=None, contours=None, mode='seed', modeFun=None, voxelSize=[1,1,1]): """ Initiate Editor Parameters ---------- img : array DICOM data matrix. actualSlice : int Index of actual slice. seeds : array Seeds, user defined regions of interest. contours : array Computed segmentation. mode : str Editor modes: 'seed' - seed editor 'crop' - manual crop 'draw' - drawing modeFun : fun Mode function invoked by user button. voxelSize : tuple of float voxel size [mm] """ QDialog.__init__(self) self.mode = mode self.mode_fun = modeFun # self.datapath = datapath self.actual_view = 'axial' self.act_transposition = VIEW_TABLE[self.actual_view] self.last_view_position = {} for ii in VIEW_TABLE.iterkeys(): self.last_view_position[ii] = img.shape[VIEW_TABLE[ii][-1]] - 1 self.img = img self.img_aview = self.img.transpose(self.act_transposition) self.actual_slice = self.img_aview.shape[-1] - actualSlice - 1 self.last_view_position[self.actual_view] = self.actual_slice # set contours self.contours = contours if self.contours is None: self.contours_aview = None else: self.contours_aview = self.contours.transpose(self.act_transposition) self.voxel_size = np.array(voxelSize) self.voxel_scale = self.voxel_size / float(np.min(self.voxel_size)) self.voxel_volume = np.prod(voxelSize) # set seeds if seeds is None: self.seeds = np.zeros(self.img.shape, np.int8) else: self.seeds = seeds self.seeds_aview = self.seeds.transpose(self.act_transposition) self.seeds_modified = False self.initUI(self.img_aview.shape, self.voxel_scale[np.array(self.act_transposition)], 600, mode) if mode == 'draw': self.seeds_orig = self.seeds.copy() self.slice_box.setEraseFun(self.eraseVolume) # set view window values C/W lb = np.min(img) ub = np.max(img) dul = ub - lb self.cw_range = {'c': [lb, ub], 'w': [1, dul]} self.slider_cw['c'].setRange(lb, ub) self.slider_cw['w'].setRange(1, dul) self.changeC(lb + dul / 2) self.changeW(dul) self.offset = np.zeros((3,), dtype=np.int16) def change_seed_class(self, id): self.current_class = id self.slice_box.seed_mark = self.current_class # print 'Current seed class changed to ', id, '.' def showStatus(self, msg): self.status_bar.showMessage(QString(msg)) QApplication.processEvents() def init_draw_mask(self, draw_mask, grid): mask_points = [] mask_iconlabel = [] for mask, label in draw_mask: w, h = mask.shape xx, yy = mask.nonzero() mask_points.append((xx - w/2, yy - h/2)) img = QImage(w, h, QImage.Format_ARGB32) img.fill(qRgba(255, 255, 255, 0)) for ii in range(xx.shape[0]): img.setPixel(xx[ii], yy[ii], qRgba(0, 0, 0, 255)) img = img.scaled(QSize(w * grid[0], h * grid[1])) icon = QIcon(QPixmap.fromImage(img)) mask_iconlabel.append((icon, label)) return mask_points, mask_iconlabel def saveSliceSeeds(self): aux = self.slice_box.getSliceSeeds() if aux is not None: self.seeds_aview[...,self.actual_slice] = aux self.seeds_modified = True else: self.seeds_modified = False def updateCropBounds(self): crp = self.getCropBounds() if crp is not None: _, cri = crp self.contours = np.zeros(self.img.shape, np.int8) self.contours[cri].fill(1) self.contours_aview = self.contours.transpose(self.act_transposition) def focusSliceSlider(self): self.slider.setFocus(True) def sliderSelectSlice(self, value): self.selectSlice(self.n_slices - value) def scrollSlices(self, inc): if abs(inc) > 0: new = self.actual_slice + inc self.selectSlice(new) def selectSlice(self, value, force=False): if not(self.allow_select_slice): return if (value < 0) or (value >= self.n_slices): return if (value != self.actual_slice) or force: self.saveSliceSeeds() if self.seeds_modified and (self.mode == 'crop'): self.updateCropBounds() if self.contours is None: contours = None else: contours = self.contours_aview[...,value] slider_val = self.n_slices - value self.slider.setValue(slider_val) self.slider.label.setText('Slice: %d / %d' % (slider_val, self.n_slices)) self.slice_box.setSlice(self.img_aview[...,value], self.seeds_aview[...,value], contours) self.actual_slice = value def getSeeds(self): return self.seeds def getImg(self): return self.img def getOffset(self): return self.offset * self.voxel_size def getSeedsVal(self, label): return self.img[self.seeds==label] def getContours(self): return self.contours def setContours(self, contours): self.contours = contours self.contours_aview = self.contours.transpose(self.act_transposition) self.selectSlice(self.actual_slice) def changeCW(self, value, key): rg = self.cw_range[key] if (value < rg[0]) or (value > rg[1]): return if (value != self.slice_box.getCW()[key]): self.slider_cw[key].setValue(value) self.slider_cw[key].label.setText('%s: %d' % (key.upper(), value)) self.slice_box.setCW(value, key) self.slice_box.updateSliceCW(self.img_aview[...,self.actual_slice]) def changeC(self, value): self.changeCW(value, 'c') def changeW(self, value): self.changeCW(value, 'w') def setView(self, value): self.last_view_position[self.actual_view] = self.actual_slice # save seeds self.saveSliceSeeds() if self.seeds_modified and (self.mode == 'crop'): self.updateCropBounds(self.seeds_aview[...,self.actual_slice]) key = str(value) self.actual_view = key self.actual_slice = self.last_view_position[key] self.act_transposition = VIEW_TABLE[key] self.img_aview = self.img.transpose(self.act_transposition) self.seeds_aview = self.seeds.transpose(self.act_transposition) if self.contours is not None: self.contours_aview = self.contours.transpose(self.act_transposition) contours = self.contours_aview[...,self.actual_slice] else: contours = None vscale = self.voxel_scale[np.array(self.act_transposition)] height = self.slice_box.height() grid = height / float(self.img_aview.shape[1] * vscale[1]) mgrid = (grid * vscale[0], grid * vscale[1]) self.slice_box.resizeSlice(new_slice_size=self.img_aview.shape[:-1], new_grid=mgrid) self.slice_box.setSlice(self.img_aview[...,self.actual_slice], self.seeds_aview[...,self.actual_slice], contours) self.allow_select_slice = False self.n_slices = self.img_aview.shape[2] slider_val = self.n_slices - self.actual_slice self.slider.setValue(slider_val) self.slider.setRange(1, self.n_slices) self.allow_select_slice = True self.slider.label.setText('Slice: %d / %d' % (slider_val, self.n_slices)) self.view_label.setText('View size: %d x %d' % self.img_aview.shape[:-1]) def changeMask(self, val): self.slice_box.setMaskPoints(self.mask_points_tab[val]) def changeContourMode(self, val): self.slice_box.contour_mode = str(val) self.slice_box.updateSlice() def changeEraseMode(self, val): self.slice_box.erase_mode = str(val) def eraseVolume(self, pos, mode): self.showStatus("Processing...") xyz = pos + (self.actual_slice,) p = tuple(np.array(xyz)[np.array(self.act_transposition)]) if self.seeds[p] > 0: if mode == 'inside': erase_reg(self.seeds, p, val=0) elif mode == 'outside': erase_reg(self.seeds, p, val=-1) idxs = np.where(self.seeds < 0) self.seeds.fill(0) self.seeds[idxs] = 1 if self.contours is None: contours = None else: contours = self.contours_aview[...,self.actual_slice] self.slice_box.setSlice(self.img_aview[...,self.actual_slice], self.seeds_aview[...,self.actual_slice], contours) self.showStatus("Done") def cropUpdate(self, img): for ii in VIEW_TABLE.iterkeys(): self.last_view_position[ii] = 0 self.actual_slice = 0 self.img = img self.img_aview = self.img.transpose(self.act_transposition) self.contours = None self.contours_aview = None self.seeds = np.zeros(self.img.shape, np.int8) self.seeds_aview = self.seeds.transpose(self.act_transposition) self.seeds_modified = False vscale = self.voxel_scale[np.array(self.act_transposition)] height = self.slice_box.height() grid = height / float(self.img_aview.shape[1] * vscale[1]) mgrid = (grid * vscale[0], grid * vscale[1]) self.slice_box.resizeSlice(new_slice_size=self.img_aview.shape[:-1], new_grid=mgrid) self.slice_box.setSlice(self.img_aview[...,self.actual_slice], self.seeds_aview[...,self.actual_slice], None) self.allow_select_slice = False self.n_slices = self.img_aview.shape[2] self.slider.setValue(self.actual_slice + 1) self.slider.setRange(1, self.n_slices) self.allow_select_slice = True self.slider.label.setText('Slice: %d / %d' % (self.actual_slice + 1, self.n_slices)) self.view_label.setText('View size: %d x %d' % self.img_aview.shape[:-1]) def getCropBounds(self): nzs = self.seeds.nonzero() cri = [] flag = True for ii in range(3): if nzs[ii].shape[0] == 0: flag = False break smin, smax = np.min(nzs[ii]), np.max(nzs[ii]) if smin == smax: flag = False break cri.append((smin, smax)) if flag: cri = np.array(cri) out = [] offset = [] for jj, ii in enumerate(cri): out.append(slice(ii[0], ii[1] + 1)) offset.append(ii[0]) return np.array(offset), tuple(out) else: return None def crop(self): self.showStatus("Processing...") crp = self.getCropBounds() if crp is not None: offset, cri = crp crop = self.img[cri] self.img = np.ascontiguousarray(crop) self.offset += offset self.showStatus('Done') else: self.showStatus('Region not selected!') self.cropUpdate(self.img) def recalculate(self, event): self.saveSliceSeeds() if np.abs(np.min(self.seeds) - np.max(self.seeds)) < 2: self.showStatus('At least two regions must be marked!') return self.showStatus("Processing...") # idx = 3 # s = random_walker(self.img[idx,:,:], self.seeds[idx,:,:])#, mode='cg_mg') # plt.figure() # plt.imshow(mark_boundaries(self.img[idx,:,:], s)) # plt.show() # self.segmentation = np.zeros(self.img.shape) # self.segmentation[idx,:,:] = s self.segmentation = random_walker(self.img, self.seeds, mode='cg_mg') self.setContours(self.segmentation - 1) self.selectSlice(self.actual_slice) # self.updateVolume() self.showStatus("Done") def deleteSliceSeeds(self, event): self.seeds_aview[...,self.actual_slice] = 0 self.slice_box.setSlice(seeds=self.seeds_aview[...,self.actual_slice]) self.slice_box.updateSlice() def resetSliceDraw(self, event): seeds_orig_aview = self.seeds_orig.transpose(self.act_transposition) self.seeds_aview[...,self.actual_slice] = seeds_orig_aview[...,self.actual_slice] self.slice_box.setSlice(seeds=self.seeds_aview[...,self.actual_slice]) self.slice_box.updateSlice() def quit(self, event): self.close() # def save(self, event): # odp = os.path.expanduser("~/lisa_data") # if not op.exists(odp): # os.makedirs(odp) # # data = self.export() # # data['version'] = self.version # # data['experiment_caption'] = self.experiment_caption # # data['lisa_operator_identifier'] = self.lisa_operator_identifier # pth, filename = op.split(op.normpath(self.datapath)) # # filename += "-" + self.experiment_caption # filepath = 'org-' + filename + '.pklz' # filepath = op.join(odp, filepath) # filepath = misc.suggest_filename(filepath) # misc.obj_to_file(data, filepath, filetype='pklz') # # filepath = 'organ_last.pklz' # filepath = op.join(odp, filepath) # misc.obj_to_file(data, filepath, filetype='pklz') # def export(self): # slab = {} # slab['none'] = 0 # slab['liver'] = 1 # slab['lesions'] = 6 # slab.update(self.slab) # # data = {} # data['version'] = (1, 0, 1) # data['data3d'] = self.img # # data['crinfo'] = self.crinfo # data['segmentation'] = self.segmentation # data['slab'] = slab # # data['voxelsize_mm'] = self.voxelsize_mm # # data['orig_shape'] = self.orig_shape # # data['processing_time'] = self.processing_time # return data def updateVolume(self): text = 'Volume [mm3]:\n unknown' if self.voxel_volume is not None: if self.mode == 'draw': vd = self.seeds else: vd = self.contours if vd is not None: nzs = vd.nonzero() nn = nzs[0].shape[0] text = 'Volume [mm3]:\n %.2e' % (nn * self.voxel_volume) self.volume_label.setText(text) def getROI(self): crp = self.getCropBounds() if crp is not None: _, cri = crp else: cri = [] for jj, ii in enumerate(self.img.shape): off = self.offset[jj] cri.append(slice(off, off + ii)) return cri