Skip to content

Commit be5047b

Browse files
Merge branch 'dino' of https://github.com/mouseland/cellpose into dino
2 parents d51eaa9 + 328ae59 commit be5047b

12 files changed

Lines changed: 195 additions & 185 deletions

File tree

cellpose/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ def get_arg_parser():
6565

6666
# model settings
6767
model_args = parser.add_argument_group("Model Arguments")
68-
model_args.add_argument("--pretrained_model", required=False, default="cpsam",
68+
model_args.add_argument("--pretrained_model", required=False, default="cpdino",
6969
type=str,
70-
help="model to use for running or starting training")
70+
help="path to model for segmentation or starting training, or builtin model name: cpdino, cpsam_v2, cpdino-vitb, or cpsam")
7171
model_args.add_argument(
7272
"--add_model", required=False, default=None, type=str,
7373
help="model path to copy model to hidden .cellpose folder for using in GUI/CLI")

cellpose/gui/gui.py

Lines changed: 108 additions & 91 deletions
Large diffs are not rendered by default.

cellpose/gui/gui3d.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ def __init__(self, image=None, logger=None):
110110
# MainW init
111111
MainW.__init__(self, image=image, logger=logger)
112112

113+
self.win.scene().sigMouseClicked.connect(self.plot_clicked)
114+
113115
# add gradZ view
114116
self.ViewDropDown.insertItem(3, "gradZ")
115117

@@ -289,9 +291,10 @@ def add_mask(self, points=None, color=(100, 200, 50), dense=True):
289291
ar, ac = np.nonzero(mask)
290292
ar, ac = ar + vr.min() - 2, ac + vc.min() - 2
291293
# get dense outline
292-
contours = cv2.findContours(mask, cv2.RETR_EXTERNAL,
293-
cv2.CHAIN_APPROX_NONE)
294-
pvc, pvr = contours[-2][0].squeeze().T
294+
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL,
295+
cv2.CHAIN_APPROX_NONE)
296+
contour = contours[np.argmax([c.size for c in contours])]
297+
pvc, pvr = contour.squeeze().T
295298
vr, vc = pvr + vr.min() - 2, pvc + vc.min() - 2
296299
# concatenate all points
297300
ar, ac = np.hstack((np.vstack((vr, vc)), np.vstack((ar, ac))))
@@ -305,9 +308,9 @@ def add_mask(self, points=None, color=(100, 200, 50), dense=True):
305308
# compute outline of new mask
306309
mask = np.zeros((np.ptp(ar) + 4, np.ptp(ac) + 4), "uint8")
307310
mask[ar - ar.min() + 2, ac - ac.min() + 2] = 1
308-
contours = cv2.findContours(mask, cv2.RETR_EXTERNAL,
309-
cv2.CHAIN_APPROX_NONE)
310-
pvc, pvr = contours[-2][0].squeeze().T
311+
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
312+
contour = contours[np.argmax([c.size for c in contours])]
313+
pvc, pvr = contour.squeeze().T
311314
vr, vc = pvr + ar.min() - 2, pvc + ac.min() - 2
312315
ars = np.concatenate((ars, ar), axis=0)
313316
acs = np.concatenate((acs, ac), axis=0)
@@ -560,13 +563,7 @@ def plot_clicked(self, event):
560563
if event.button()==QtCore.Qt.LeftButton \
561564
and not event.modifiers() & (QtCore.Qt.ShiftModifier | QtCore.Qt.AltModifier)\
562565
and not self.removing_region:
563-
if event.double():
564-
try:
565-
self.p0.setYRange(0, self.Ly + self.pr)
566-
except:
567-
self.p0.setYRange(0, self.Ly)
568-
self.p0.setXRange(0, self.Lx)
569-
elif self.loaded and not self.in_stroke:
566+
if self.loaded and not self.in_stroke:
570567
if self.orthobtn.isChecked():
571568
items = self.win.scene().items(event.scenePos())
572569
for x in items:
@@ -621,6 +618,11 @@ def keyPressEvent(self, event):
621618
self.scroll.setValue(self.currentZ)
622619
event.accept()
623620
return
621+
622+
# when in stroke, allow escaping out of drawing
623+
else:
624+
if event.key() == QtCore.Qt.Key_Escape:
625+
self.layer.end_stroke(keep_stroke=False)
624626
if event.key() == QtCore.Qt.Key_Minus or event.key() == QtCore.Qt.Key_Equal:
625627
self.p0.keyPressEvent(event)
626628
event.accept()

cellpose/gui/guiparts.py

Lines changed: 26 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import numpy as np
1111
import pathlib, os
1212

13+
from cellpose import models
1314
from cellpose.gui.io import _save_sets
1415

1516

@@ -111,32 +112,6 @@ def setup(self):
111112
)
112113

113114

114-
# def create_channel_choose():
115-
# # choose channel
116-
# ChannelChoose = [QComboBox(), QComboBox()]
117-
# ChannelLabels = []
118-
# ChannelChoose[0].addItems(["gray", "red", "green", "blue"])
119-
# ChannelChoose[1].addItems(["none", "red", "green", "blue"])
120-
# cstr = ["chan to segment:", "chan2 (optional): "]
121-
# for i in range(2):
122-
# ChannelLabels.append(QLabel(cstr[i]))
123-
# if i == 0:
124-
# ChannelLabels[i].setToolTip(
125-
# "this is the channel in which the cytoplasm or nuclei exist \
126-
# that you want to segment")
127-
# ChannelChoose[i].setToolTip(
128-
# "this is the channel in which the cytoplasm or nuclei exist \
129-
# that you want to segment")
130-
# else:
131-
# ChannelLabels[i].setToolTip(
132-
# "if <em>cytoplasm</em> model is chosen, and you also have a \
133-
# nuclear channel, then choose the nuclear channel for this option")
134-
# ChannelChoose[i].setToolTip(
135-
# "if <em>cytoplasm</em> model is chosen, and you also have a \
136-
# nuclear channel, then choose the nuclear channel for this option")
137-
138-
# return ChannelChoose, ChannelLabels
139-
140115
def unsilence_exceptions(func):
141116
""" Wrapper to unsilence Qt exceptions and re-raise them """
142117
def wrapper(*args, **kwargs):
@@ -148,19 +123,6 @@ def wrapper(*args, **kwargs):
148123
logger.debug(''.join(traceback.format_exception(type(e), e, e.__traceback__)))
149124
return wrapper
150125

151-
class ModelButton(QPushButton):
152-
153-
def __init__(self, parent, model_name, text):
154-
super().__init__()
155-
self.setEnabled(False)
156-
self.setText(text)
157-
self.setFont(parent.boldfont)
158-
self.clicked.connect(lambda: self.press(parent))
159-
self.model_name = "cpsam"
160-
161-
def press(self, parent):
162-
parent.compute_segmentation(model_name="cpsam")
163-
164126

165127
class FilterButton(QPushButton):
166128

@@ -428,7 +390,7 @@ def niter(self):
428390

429391
class TrainWindow(QDialog):
430392

431-
def __init__(self, parent, model_strings):
393+
def __init__(self, parent):
432394
super().__init__(parent)
433395
self.setGeometry(100, 100, 900, 550)
434396
self.setWindowTitle("train settings")
@@ -446,7 +408,7 @@ def __init__(self, parent, model_strings):
446408
# choose initial model
447409
yoff += 1
448410
self.ModelChoose = QComboBox()
449-
self.ModelChoose.addItems(model_strings)
411+
self.ModelChoose.addItems(models.MODEL_NAMES)
450412
self.ModelChoose.setFixedWidth(150)
451413
self.ModelChoose.setCurrentIndex(parent.training_params["model_index"])
452414
self.l0.addWidget(self.ModelChoose, yoff, 1, 1, 1)
@@ -667,6 +629,7 @@ def mouseClickEvent(self, ev):
667629
if y >= 0 and y < self.parent.Ly and x >= 0 and x < self.parent.Lx:
668630
if ev.button() == QtCore.Qt.LeftButton and not ev.double():
669631
idx = self.parent.cellpix[self.parent.currentZ][y, x]
632+
self.parent.logger.debug(f'clicked on idx: {idx}')
670633
if idx > 0:
671634
if ev.modifiers() & QtCore.Qt.ControlModifier:
672635
# delete mask selected
@@ -686,9 +649,17 @@ def mouseClickEvent(self, ev):
686649
elif self.parent.masksOn and not self.parent.deleting_multiple:
687650
self.parent.unselect_cell()
688651

652+
@unsilence_exceptions
653+
def mouseDoubleClickEvent(self, ev) -> None:
654+
ev.accept()
655+
return
656+
689657
@unsilence_exceptions
690658
def mouseDragEvent(self, ev):
691-
ev.ignore()
659+
if ev.button() == QtCore.Qt.RightButton:
660+
ev.accept()
661+
else:
662+
ev.ignore()
692663
return
693664

694665
@unsilence_exceptions
@@ -717,6 +688,9 @@ def is_at_start(self, pos):
717688
# first check if you ever left the start
718689
if len(self.parent.current_stroke) > 3:
719690
stroke = np.array(self.parent.current_stroke)
691+
stroke = stroke[stroke[:, 0] == self.parent.currentZ]
692+
if len(stroke) <= 3:
693+
return False
720694
dist = (((stroke[1:, 1:] -
721695
stroke[:1, 1:][np.newaxis, :, :])**2).sum(axis=-1))**0.5
722696
dist = dist.flatten()
@@ -731,10 +705,19 @@ def is_at_start(self, pos):
731705
else:
732706
return False
733707

734-
def end_stroke(self):
708+
def end_stroke(self, keep_stroke=True):
735709
if hasattr(self, 'scatter') and self.scatter is not None:
736710
if self.scatter.scene() == self.parent.layer.scene():
737711
self.parent.p0.removeItem(self.scatter)
712+
if not keep_stroke:
713+
if not self.parent.stroke_appended:
714+
self.parent.strokes.append(self.parent.current_stroke)
715+
self.parent.stroke_appended = True
716+
self.parent.remove_stroke(delete_points=False)
717+
self.parent.current_stroke = [s for s in self.parent.current_stroke
718+
if s[0] != self.parent.currentZ]
719+
self.parent.in_stroke = False
720+
return
738721
if not self.parent.stroke_appended:
739722
self.parent.strokes.append(self.parent.current_stroke)
740723
self.parent.stroke_appended = True

cellpose/gui/io.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,22 @@ def _add_model(parent, filename=None, load_model=True):
4141

4242
for ind, model_string in enumerate(parent.model_strings[:-1]):
4343
if model_string == fname:
44-
_remove_model(parent, ind=ind + 1, verbose=False)
44+
_remove_model(parent, ind=ind + 1)
4545

4646
parent.ModelChooseC.setCurrentIndex(len(parent.model_strings))
4747
if load_model:
4848
parent.model_choose(custom=True)
4949

5050

51-
def _remove_model(parent, ind=None, verbose=True):
51+
def _remove_model(parent, ind=None):
5252
if ind is None:
5353
ind = parent.ModelChooseC.currentIndex()
5454
if ind > 0:
55-
ind -= 1
56-
parent.ModelChooseC.removeItem(ind + 1)
57-
del parent.model_strings[ind]
58-
# remove model from txt path
5955
modelstr = parent.ModelChooseC.currentText()
56+
parent.ModelChooseC.removeItem(ind)
57+
# remove model from txt path
6058
remove_model(modelstr)
59+
parent.model_strings.remove(modelstr)
6160
if len(parent.model_strings) > 0:
6261
parent.ModelChooseC.setCurrentIndex(len(parent.model_strings))
6362
else:
@@ -329,7 +328,11 @@ def _load_seg(parent, filename=None, image=None, image_file=None, load_3D=False)
329328
parent.ismanual = dat["ismanual"]
330329

331330
if "current_channel" in dat:
332-
parent.color = (dat["current_channel"] + 2) % 5
331+
# saved as int, loading int is built into .color property
332+
color = dat['current_channel']
333+
if isinstance(color, tuple):
334+
color = color[0]
335+
parent.color = color
333336

334337
if "flows" in dat:
335338
parent.flows = dat["flows"]
@@ -355,6 +358,7 @@ def _load_seg(parent, filename=None, image=None, image_file=None, load_3D=False)
355358

356359
parent.enable_buttons()
357360
parent.update_layer()
361+
parent.update_plot()
358362
del dat
359363
gc.collect()
360364

@@ -551,6 +555,8 @@ def _save_sets(parent):
551555
flow_threshold = parent.segmentation_settings.flow_threshold
552556
cellprob_threshold = parent.segmentation_settings.cellprob_threshold
553557

558+
# use ints instead of strings for backwards compatibility with old _seg.npy files
559+
color_int = list(parent.RGBDropDown.name_map.keys()).index(parent.color)
554560
if parent.NZ > 1:
555561
dat = {
556562
"outlines":
@@ -559,7 +565,7 @@ def _save_sets(parent):
559565
parent.cellcolors[1:],
560566
"masks":
561567
parent.cellpix,
562-
"current_channel": (parent.color - 2) % 5,
568+
"current_channel": color_int,
563569
"filename":
564570
parent.filename,
565571
"flows":

cellpose/io.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def logger_setup(cp_path=".cellpose", logfile_name="run.log", stdout_file_replac
7979
log_file = cp_dir.joinpath(logfile_name)
8080
try:
8181
log_file.unlink()
82-
except:
82+
except FileNotFoundError:
8383
print('creating new log file')
8484
logfile_fh = logging.FileHandler(log_file)
8585
if stdout_file_replacement is not None:
@@ -348,6 +348,9 @@ def remove_model(filename, delete=False):
348348
filename = os.path.split(filename)[-1]
349349
from . import models
350350
model_strings = models.get_user_models()
351+
if filename not in model_strings:
352+
raise ValueError(f'filename not found: {filename}')
353+
model_strings.remove(filename)
351354
if len(model_strings) > 0:
352355
with open(models.MODEL_LIST_PATH, "w") as textfile:
353356
for fname in model_strings:
@@ -356,10 +359,10 @@ def remove_model(filename, delete=False):
356359
# write empty file
357360
textfile = open(models.MODEL_LIST_PATH, "w")
358361
textfile.close()
359-
print(f"{filename} removed from custom model list")
362+
io_logger.info(f"{filename} removed from custom model list")
360363
if delete:
361-
os.remove(os.fspath(models.MODEL_DIR.joinpath(fname)))
362-
print("model deleted")
364+
os.remove(os.fspath(models.MODEL_DIR.joinpath(filename)))
365+
io_logger.info(f"{filename} model deleted from disk")
363366

364367

365368
def add_model(filename):

cellpose/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
_MODEL_DIR_DEFAULT = Path.home().joinpath(".cellpose", "models")
2525
MODEL_DIR = Path(_MODEL_DIR_ENV) if _MODEL_DIR_ENV else _MODEL_DIR_DEFAULT
2626

27-
MODEL_NAMES = ["cpsam", "cpsam_v2", "cpdino", "cpdino-vitb"]
27+
MODEL_NAMES = ["cpdino", "cpsam_v2", "cpdino-vitb", "cpsam"]
2828

2929
MODEL_LIST_PATH = os.fspath(MODEL_DIR.joinpath("gui_models.txt"))
3030

cellpose/transforms.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -873,12 +873,13 @@ def random_rotate_and_resize(imgs, lbls=None, scale_range=1., bsize=256, rescale
873873
if rescale is not None:
874874
scale *= 1. / rescale[n]
875875
scale_per_image[n] = scale
876-
angle = 360 * np.random.rand() - 180
876+
angle = np.float32(360 * np.random.rand() - 180)
877877
Ly, Lx = imgs[n].shape[-2:]
878-
dyx = np.maximum(0, np.array([Ly * scale - yx[0], Lx * scale - yx[1]]))
878+
dyx = np.maximum(0, np.array([Ly * scale - yx[0], Lx * scale - yx[1]])).astype("float32")
879879
flip = np.random.rand() > 0.5
880-
translations = list(dyx * np.random.rand(2) * 1 - dyx * 0.5)
881-
matrix = F._get_inverse_affine_matrix([0., 0.], angle, translations, scale, [0., 0.])
880+
translations = list(dyx * np.random.rand(2).astype("float32") * 1 - dyx * 0.5)
881+
matrix = F._get_inverse_affine_matrix(np.zeros(2, "float32"), angle, translations,
882+
scale, np.zeros(2, "float32"))
882883
theta = torch.tensor(matrix, device=device).reshape(1, 2, 3)
883884
grid = F_t._gen_affine_grid(theta, w=Lx, h=Ly, ow=yx[1], oh=yx[0])
884885

docs/gui.rst

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,28 +40,28 @@ For multi-Z 3D data, please use the 3D version of the GUI:
4040
Using the GUI
4141
~~~~~~~~~~~~~~~~~~~~~~~
4242

43-
The GUI serves two main functions:
43+
The GUI serves three main functions:
4444

4545
1. Running the segmentation algorithm.
4646
2. Manually labelling data.
47-
3. (NEW) Fine-tuning a pretrained cellpose model on your own data.
47+
3. Fine-tuning a pretrained cellpose model on your own data.
4848

4949
Main GUI mouse controls (works in all views):
5050

5151
- Pan = left-click + drag
5252
- Zoom = scroll wheel (or +/= and - buttons)
53-
- Full view = double left-click
5453
- Select mask = left-click on mask
5554
- Delete mask = Ctrl (or Command on Mac) + left-click
5655
- Merge masks = Alt + left-click (will merge last two)
5756
- Start draw mask = right-click
58-
- End draw mask = right-click, or return to circle at beginning
57+
- End draw mask = press esc, right-click, or return to circle at beginning
5958

6059
Drawing masks
6160
~~~~~~~~~~~~~~~~~~~~~~
6261

6362
Masks are started with right-click, then hover your mouse (do not hold it down),
6463
and return it to the red circle to complete the mask. The mask should now be completed.
64+
At any point, you can escape drawing a mask with the Esc key.
6565

6666
Overlaps in masks are NOT allowed. If you draw a mask on top of another
6767
mask, it is cropped so that it doesn't overlap with the old mask. Masks
@@ -146,6 +146,8 @@ Keyboard shortcuts
146146
+---------------------+-----------------------------------------------+
147147
| =/+ // - | zoom in // zoom out |
148148
+---------------------+-----------------------------------------------+
149+
| SPACE | reset zoom |
150+
+---------------------+-----------------------------------------------+
149151
| CTRL+Z | undo previously drawn mask/stroke |
150152
+---------------------+-----------------------------------------------+
151153
| CTRL+0 | clear all masks |

0 commit comments

Comments
 (0)