From 3bcdd36643ba56b52a0d7478ae8d9fa7d0f8108b Mon Sep 17 00:00:00 2001 From: Carsen Stringer Date: Wed, 13 Sep 2023 04:52:48 -0400 Subject: [PATCH 1/2] adding ability to draw multiple ROIs in single z-plane for complex shapes (TODO documentation) --- cellpose/gui/gui.py | 100 +++++++++++++++++++++------------------ cellpose/gui/guiparts.py | 6 +-- 2 files changed, 57 insertions(+), 49 deletions(-) diff --git a/cellpose/gui/gui.py b/cellpose/gui/gui.py index c412d46a..ca0d2274 100644 --- a/cellpose/gui/gui.py +++ b/cellpose/gui/gui.py @@ -1253,7 +1253,8 @@ def remove_stroke(self, delete_points=True, stroke_ind=-1): if self.outlinesOn: self.layerz[stroke[outpix,1],stroke[outpix,2]] = np.array(self.outcolor) if delete_points: - self.current_point_set = self.current_point_set[:-1*(stroke[:,-1]==1).sum()] + # self.current_point_set = self.current_point_set[:-1*(stroke[:,-1]==1).sum()] + del self.current_point_set[stroke_ind] self.update_layer() del self.strokes[stroke_ind] @@ -1416,10 +1417,9 @@ def update_crosshairs(self): def add_set(self): if len(self.current_point_set) > 0: - self.current_point_set = np.array(self.current_point_set) while len(self.strokes) > 0: self.remove_stroke(delete_points=False) - if len(self.current_point_set) > 8: + if len(self.current_point_set[0]) > 8: color = self.colormap[self.ncells,:3] median = self.add_mask(points=self.current_point_set, color=color) if median is not None: @@ -1436,51 +1436,61 @@ def add_set(self): self.current_point_set = [] self.update_layer() - def add_mask(self, points=None, color=(100,200,50)): + def add_mask(self, points=None, color=(100,200,50), dense=True): + # points is list of strokes + + points_all = np.concatenate(points, axis=0) + # loop over z values median = [] - if points.shape[1] < 3: - points = np.concatenate((np.zeros((points.shape[0],1), "int32"), points), axis=1) - zdraw = np.unique(points[:,0]) + zdraw = np.unique(points_all[:,0]) zrange = np.arange(zdraw.min(), zdraw.max()+1, 1, int) zmin = zdraw.min() pix = np.zeros((2,0), "uint16") mall = np.zeros((len(zrange), self.Ly, self.Lx), "bool") k=0 for z in zdraw: - iz = points[:,0] == z - vr = points[iz,1] - vc = points[iz,2] - # get points inside drawn points - mask = np.zeros((np.ptp(vr)+4, np.ptp(vc)+4), np.uint8) - pts = np.stack((vc-vc.min()+2,vr-vr.min()+2), axis=-1)[:,np.newaxis,:] - mask = cv2.fillPoly(mask, [pts], (255,0,0)) - ar, ac = np.nonzero(mask) - ar, ac = ar+vr.min()-2, ac+vc.min()-2 - # get dense outline - contours = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) - pvc, pvr = contours[-2][0].squeeze().T - vr, vc = pvr + vr.min() - 2, pvc + vc.min() - 2 - # concatenate all points - ar, ac = np.hstack((np.vstack((vr, vc)), np.vstack((ar, ac)))) - # if these pixels are overlapping with another cell, reassign them - ioverlap = self.cellpix[z][ar, ac] > 0 - if (~ioverlap).sum() < 8: - print('ERROR: cell too small without overlaps, not drawn') - return None - elif ioverlap.sum() > 0: - ar, ac = ar[~ioverlap], ac[~ioverlap] - # compute outline of new mask - mask = np.zeros((np.ptp(ar)+4, np.ptp(ac)+4), np.uint8) - mask[ar-ar.min()+2, ac-ac.min()+2] = 1 - contours = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) - pvc, pvr = contours[-2][0].squeeze().T - vr, vc = pvr + ar.min() - 2, pvc + ac.min() - 2 - self.draw_mask(z, ar, ac, vr, vc, color) - - median.append(np.array([np.median(ar), np.median(ac)])) + ars, acs, vrs, vcs = np.zeros(0, "int"), np.zeros(0, "int"), np.zeros(0, "int"), np.zeros(0, "int") + for stroke in points: + stroke = np.concatenate(stroke, axis=0).reshape(-1, 4) + iz = stroke[:,0] == z + vr = stroke[iz,1] + vc = stroke[iz,2] + if iz.sum() > 0: + # get points inside drawn points + mask = np.zeros((np.ptp(vr)+4, np.ptp(vc)+4), np.uint8) + pts = np.stack((vc-vc.min()+2,vr-vr.min()+2), axis=-1)[:,np.newaxis,:] + mask = cv2.fillPoly(mask, [pts], (255,0,0)) + ar, ac = np.nonzero(mask) + ar, ac = ar+vr.min()-2, ac+vc.min()-2 + # get dense outline + contours = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) + pvc, pvr = contours[-2][0].squeeze().T + vr, vc = pvr + vr.min() - 2, pvc + vc.min() - 2 + # concatenate all points + ar, ac = np.hstack((np.vstack((vr, vc)), np.vstack((ar, ac)))) + # if these pixels are overlapping with another cell, reassign them + ioverlap = self.cellpix[z][ar, ac] > 0 + if (~ioverlap).sum() < 8: + print('ERROR: cell too small without overlaps, not drawn') + return None + elif ioverlap.sum() > 0: + ar, ac = ar[~ioverlap], ac[~ioverlap] + # compute outline of new mask + mask = np.zeros((np.ptp(ar)+4, np.ptp(ac)+4), np.uint8) + mask[ar-ar.min()+2, ac-ac.min()+2] = 1 + contours = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) + pvc, pvr = contours[-2][0].squeeze().T + vr, vc = pvr + ar.min() - 2, pvc + ac.min() - 2 + ars = np.concatenate((ars, ar), axis=0) + acs = np.concatenate((acs, ac), axis=0) + vrs = np.concatenate((vrs, vr), axis=0) + vcs = np.concatenate((vcs, vc), axis=0) + self.draw_mask(z, ars, acs, vrs, vcs, color) + + median.append(np.array([np.median(ars), np.median(acs)])) mall[z-zmin, ar, ac] = True - pix = np.append(pix, np.vstack((ar, ac)), axis=-1) + pix = np.append(pix, np.vstack((ars, acs)), axis=-1) mall = mall[:, pix[0].min():pix[0].max()+1, pix[1].min():pix[1].max()+1].astype(np.float32) ymin, xmin = pix[0].min(), pix[1].min() @@ -1514,12 +1524,12 @@ def draw_mask(self, z, ar, ac, vr, vc, color, idx=None): self.cellpix[z, vr, vc] = idx self.cellpix[z, ar, ac] = idx self.outpix[z, vr, vc] = idx - self.layerz[ar, ac, :3] = color - if self.masksOn: - self.layerz[ar, ac, -1] = self.opacity - if self.outlinesOn: - self.layerz[vr, vc] = np.array(self.outcolor) - + if z==self.currentZ: + self.layerz[ar, ac, :3] = color + if self.masksOn: + self.layerz[ar, ac, -1] = self.opacity + if self.outlinesOn: + self.layerz[vr, vc] = np.array(self.outcolor) def compute_scale(self): self.diameter = float(self.Diameter.text()) diff --git a/cellpose/gui/guiparts.py b/cellpose/gui/guiparts.py index 661eefc8..dcf75ca5 100644 --- a/cellpose/gui/guiparts.py +++ b/cellpose/gui/guiparts.py @@ -569,7 +569,6 @@ def hoverEvent(self, ev): self.drawAt(ev.pos()) if self.is_at_start(ev.pos()): self.end_stroke() - else: ev.acceptClicks(QtCore.Qt.RightButton) #ev.acceptClicks(QtCore.Qt.LeftButton) @@ -607,11 +606,11 @@ def end_stroke(self): self.parent.stroke_appended = True self.parent.current_stroke = np.array(self.parent.current_stroke) ioutline = self.parent.current_stroke[:,3]==1 - self.parent.current_point_set.extend(list(self.parent.current_stroke[ioutline])) + self.parent.current_point_set.append(list(self.parent.current_stroke[ioutline])) self.parent.current_stroke = [] if self.parent.autosave: self.parent.add_set() - if len(self.parent.current_point_set) > 0 and self.parent.autosave: + if len(self.parent.current_point_set) and len(self.parent.current_point_set[0]) > 0 and self.parent.autosave: self.parent.add_set() self.parent.in_stroke = False @@ -623,7 +622,6 @@ def tabletEvent(self, ev): def drawAt(self, pos, ev=None): mask = self.strokemask - set = self.parent.current_point_set stroke = self.parent.current_stroke pos = [int(pos.y()), int(pos.x())] dk = self.drawKernel From cd0680da0cdcda4a401fd207f4523dd5ecf5f92c Mon Sep 17 00:00:00 2001 From: Carsen Stringer Date: Wed, 13 Sep 2023 05:01:09 -0400 Subject: [PATCH 2/2] adding documentation --- docs/gui.rst | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/gui.rst b/docs/gui.rst index 7386b360..e1a2c931 100644 --- a/docs/gui.rst +++ b/docs/gui.rst @@ -48,17 +48,32 @@ Main GUI mouse controls (works in all views): - Start draw mask = right-click - End draw mask = right-click, or return to circle at beginning +Drawing masks +~~~~~~~~~~~~~~~~~~~~~~ + +Masks are started with right-click, then hover your mouse (do not hold it down), +and return it to the red circle to complete the mask. The mask should now be completed. + Overlaps in masks are NOT allowed. If you draw a mask on top of another mask, it is cropped so that it doesn't overlap with the old mask. Masks in 2D should be single strokes (if *single_stroke* is checked). If you want to draw masks in 3D, then you can turn *single_stroke* option off and draw a stroke on each plane with the cell and then press -ENTER. +ENTER. You can also draw multiple strokes on the same plane for +complex cell shapes, but do not do this in 2D if you plan to train a cellpose model +(the cell flows will not work correctly). .. note:: 3D labelling will fill in unlabelled z-planes so that you do not - have to densely label, for example you can skip some planes. + have to densely label, for example you can skip some planes, and the + cell will be interpolated between planes. + +After each mask is drawn in 2D, it is saved to the ``_seg.npy``. If this is slow (for +large images), this "autosave" option can be turned off in the "File" menu +("Disable autosave _seg.npy file"). In 3D, +the mask is never auto-saved, instead save masks by clicking CTRL+S, or "Save" in the +"File" menu. Segmentation options