diff --git a/cellacdc/_warnings.py b/cellacdc/_warnings.py index e313b347..4d6ee500 100644 --- a/cellacdc/_warnings.py +++ b/cellacdc/_warnings.py @@ -202,6 +202,34 @@ def warnPromptSegmentPointsLayerNotInit(qparent=None): ) return msg.cancel +def warnNoIDsInS(qparent=None): + from cellacdc import widgets + txt = html_utils.paragraph(f""" + None of the IDs present at this frame + are in 'S' phase.

+ This tool can be used only for mother-bud pairs.

+ Thank you for your patience! + """) + msg = widgets.myMessageBox(wrapText=False) + msg.warning( + qparent, 'No cells in "S" phase', txt, + ) + return msg.cancel + +def warnSelectedIDisNotInS(ID, qparent=None): + from cellacdc import widgets + txt = html_utils.paragraph(f""" + The selected ID {ID} cell cycle stage is not 'S'!

+ Make sure you are hovering a cell ID in 'S' (mother of bud), + when activating this mode.

+ Thank you for your patience! + """) + msg = widgets.myMessageBox(wrapText=False) + msg.warning( + qparent, 'Selected ID not in S', txt, + ) + return msg.cancel + def warnPromptSegmentModelNotInit(qparent=None): from cellacdc import widgets txt = html_utils.paragraph(f""" diff --git a/cellacdc/cca_functions.py b/cellacdc/cca_functions.py index cd87389b..2ca15a5a 100755 --- a/cellacdc/cca_functions.py +++ b/cellacdc/cca_functions.py @@ -864,7 +864,41 @@ def add_generation_num_of_relative_ID( acdc_df_with_col = acdc_df_by_frame_i.reset_index() return acdc_df_with_col + +def get_IDs_gen_num_will_divide(cca_df): + """Get a list of (ID, gen_num) of cells that require `will_divide`>0 + + Parameters + ---------- + cca_df : pd.DataFrame + DataFrame with cc annotations for every frame and Cell_ID + Returns + ------- + list of tuples + List of (ID, gen_num) of cells that require `will_divide`>0 + """ + + cca_df_buds = cca_df.query('relationship == "bud"') + IDs_gen_num_will_divide = [] + for budID, bud_cca_df in cca_df_buds.groupby('Cell_ID'): + all_gen_nums = cca_df.query(f'Cell_ID == {budID}')['generation_num'] + if not (all_gen_nums > 0).any(): + # bud division is annotated in the future + continue + + mothID = int(bud_cca_df['relative_ID'].iloc[0]) + first_frame_bud = bud_cca_df['frame_i'].iloc[0] + gen_num_moth = cca_df.query( + f'(frame_i == {first_frame_bud}) & (Cell_ID == {mothID})' + )['generation_num'].iloc[0] + + IDs_gen_num_will_divide.append((mothID, gen_num_moth)) + IDs_gen_num_will_divide.append((budID, 0)) + + return IDs_gen_num_will_divide + + def get_IDs_gen_num_will_divide_wrong(global_cca_df): """Get a list of (ID, gen_num) of cells whose `will_divide`>0 but the next generation does not exist (i.e., `will_divide` is wrong) diff --git a/cellacdc/docs/source/tooltips.rst b/cellacdc/docs/source/tooltips.rst index 8a040489..8c169ab7 100644 --- a/cellacdc/docs/source/tooltips.rst +++ b/cellacdc/docs/source/tooltips.rst @@ -159,6 +159,12 @@ :height: 16px :width: 16px +.. |annotateSingleMotherBudPairButton| image:: https://raw.githubusercontent.com/SchmollerLab/Cell_ACDC/refs/heads/main/cellacdc/resources/icons/lock_id_annotate_future.svg + :target: https://github.com/SchmollerLab/Cell_ACDC/blob/main/cellacdc/resources/icons/lock_id_annotate_future.svg + :alt: annotateSingleMotherBudPairButton icon + :height: 16px + :width: 16px + .. |segmentToolAction| image:: https://raw.githubusercontent.com/SchmollerLab/Cell_ACDC/refs/heads/main/cellacdc/resources/icons/segment.svg :target: https://github.com/SchmollerLab/Cell_ACDC/blob/main/cellacdc/resources/icons/segment.svg :alt: segmentToolAction icon @@ -485,6 +491,12 @@ Edit tools: Cell cycle analysis * **Automatically assign bud to mother (** |assignBudMothAutoAction| **):** Automatically assign buds to mothers using YeastMate. * **Manually edit cell cycle annotations table (** |editCcaToolAction| **"Ctrl+Shift+P"):** Manually edit cell cycle annotations table. * **Re-initialize cell cycle annotations table (** |reInitCcaAction| **):** Re-initialize cell cycle annotations table from this frame onward. NOTE: This will erase all the already annotated future frames information (from the current session not the saved information). +* **Annotate one mother-bud pair at the time (** |annotateSingleMotherBudPairButton| **"Y"):** + 1. Activate to annotate a single mother-bud pair at the time. + 2. Annotate past and future frames + 3. Deactivate to go back to the frame you were annotating before activating this tool. + + NOTE: When annotating future frames, the other cells will not be displayed and they will be ignored. Edit tools: Normal division: Lineage tree ----------------------------------------- diff --git a/cellacdc/gui.py b/cellacdc/gui.py index 0a448430..4b4374ef 100755 --- a/cellacdc/gui.py +++ b/cellacdc/gui.py @@ -1039,6 +1039,25 @@ def gui_createToolBars(self): self.checkableQButtonsGroup.addButton(self.setIsHistoryKnownButton) self.functionsNotTested3D.append(self.setIsHistoryKnownButton) + self.annotateSingleMotherBudPairButton = QToolButton(self) + self.annotateSingleMotherBudPairButton.setIcon( + QIcon(":lock_id_annotate_future.svg") + ) + self.annotateSingleMotherBudPairButton.setCheckable(True) + self.annotateSingleMotherBudPairButton.setShortcut('Y') + self.annotateSingleMotherBudPairButton.setVisible(False) + self.annotateSingleMotherBudPairButton.action = ccaToolBar.addWidget( + self.annotateSingleMotherBudPairButton + ) + self.checkableButtons.append(self.annotateSingleMotherBudPairButton) + self.widgetsWithShortcut['Annotate one mother-bud pair at the time'] = ( + self.annotateSingleMotherBudPairButton + ) + self.checkableQButtonsGroup.addButton( + self.annotateSingleMotherBudPairButton + ) + self.functionsNotTested3D.append(self.annotateSingleMotherBudPairButton) + ccaToolBar.addAction(self.assignBudMothAutoAction) ccaToolBar.addAction(self.editCcaToolAction) ccaToolBar.addAction(self.reInitCcaAction) @@ -1589,6 +1608,7 @@ def autoSaveWorkerTimerCallback(self, worker, posData): worker._enqueue(posData) def autoSaveWorkerDone(self): + self.logger.info('Autosaving done.') self.setStatusBarLabel(log=False) def ccaCheckerWorkerDone(self): @@ -3439,9 +3459,13 @@ def gui_connectEditActions(self): self.whitelistIDsToolbar.sigViewOGIDs.connect(self.whitelistViewOGIDs) - self.whitelistIDsToolbar.sigAddNewIDs.connect(self.whitelistAddNewIDsToggled) + self.whitelistIDsToolbar.sigAddNewIDs.connect( + self.whitelistAddNewIDsToggled + ) - self.whitelistIDsToolbar.sigLoadOGLabs.connect(self.whitelistLoadOGLabs_cb) + self.whitelistIDsToolbar.sigLoadOGLabs.connect( + self.whitelistLoadOGLabs_cb + ) self.whitelistIDsToolbar.sigTrackOGagainstPreviousFrame.connect( self.whitelistTrackOGagainstPreviousFrame_cb @@ -3463,6 +3487,10 @@ def gui_connectEditActions(self): self.addScaleBarAction.toggled.connect(self.addScaleBar) self.addTimestampAction.toggled.connect(self.addTimestamp) self.saveLabColormapAction.triggered.connect(self.saveLabelsColormap) + + self.annotateSingleMotherBudPairButton.toggled.connect( + self.annotateSingleMotherBudPair_cb + ) self.enableSmartTrackAction.toggled.connect(self.enableSmartTrack) # Brush/Eraser size action @@ -3474,18 +3502,26 @@ def gui_connectEditActions(self): self.modeComboBox.activated.connect(self.clearComboBoxFocus) self.equalizeHistPushButton.toggled.connect(self.equalizeHist) - self.editOverlayColorAction.triggered.connect(self.toggleOverlayColorButton) - self.editTextIDsColorAction.triggered.connect(self.toggleTextIDsColorButton) + self.editOverlayColorAction.triggered.connect( + self.toggleOverlayColorButton + ) + self.editTextIDsColorAction.triggered.connect( + self.toggleTextIDsColorButton + ) self.overlayColorButton.sigColorChanging.connect(self.changeOverlayColor) self.overlayColorButton.sigColorChanged.connect(self.saveOverlayColor) - self.textIDsColorButton.sigColorChanging.connect(self.updateTextAnnotColor) + self.textIDsColorButton.sigColorChanging.connect( + self.updateTextAnnotColor + ) self.textIDsColorButton.sigColorChanged.connect(self.saveTextIDsColors) self.setMeasurementsAction.triggered.connect(self.showSetMeasurements) self.addCustomMetricAction.triggered.connect(self.addCustomMetric) self.addCombineMetricAction.triggered.connect(self.addCombineMetric) - self.labelsGrad.colorButton.sigColorChanging.connect(self.updateBkgrColor) + self.labelsGrad.colorButton.sigColorChanging.connect( + self.updateBkgrColor + ) self.labelsGrad.colorButton.sigColorChanged.connect(self.saveBkgrColor) self.labelsGrad.sigGradientChangeFinished.connect(self.updateLabelsCmap) self.labelsGrad.sigGradientChanged.connect(self.ticksCmapMoved) @@ -13253,6 +13289,130 @@ def copyLostObjContour_cb(self, checked): self.lostObjImage = np.zeros_like(self.currentLab2D) self.updateLostContoursImage(0) + def getHoveredMotherBudIDs(self): + posData = self.data[self.pos_i] + + hoverID = self.getLastHoveredID() + try: + ccs = posData.cca_df.loc[hoverID, 'cell_cycle_stage'] + except Exception as err: + ccs = 'G1' + + cca_df = posData.cca_df + cca_df_S = cca_df[cca_df['cell_cycle_stage'] == 'S'] + IDs_in_S = cca_df_S.index.to_list() + + if not IDs_in_S: + _warnings.warnNoIDsInS(qparent=self) + return + + while ccs != 'S': + win = apps.QLineEditDialog( + title='Selected ID not in S', + msg='The cell cycle stage of the selected ID ' + 'is not "S".

' + 'Enter the ID (mother or bud) ' + 'that you want to annotate.', + parent=self, + isInteger=True, + allowedValues=IDs_in_S, + defaultTxt=IDs_in_S[0] + ) + win.exec_() + if win.cancel: + return + + hoverID = win.EntryID + ccs = cca_df.loc[hoverID, 'cell_cycle_stage'] + + relationship = posData.cca_df.loc[hoverID, 'relationship'] + relative_ID = posData.cca_df.loc[hoverID, 'relative_ID'] + mothID = hoverID if relationship == 'mother' else relative_ID + budID = hoverID if relationship == 'bud' else relative_ID + + return int(mothID), int(budID) + + def annotateSingleMotherBudPair_cb(self, checked): + posData = self.data[self.pos_i] + self.modeComboBox.setDisabled(checked) + if checked: + self.store_data() + + self.annotateSingleMothBudPairState = {} + mothIDbudID = self.getHoveredMotherBudIDs() + + if mothIDbudID is None: + self.annotateSingleMotherBudPairButton.setChecked(False) + return + + mothID, budID = mothIDbudID + + self.logger.info( + 'Setting annotation mode for single mother-bud pair = ' + f'{(mothID, budID)}, at frame n. {posData.frame_i+1}' + ) + + self.annotateSingleMothBudPairState['doWarnLostObj'] = ( + self.warnLostCellsAction.isChecked() + ) + self.annotateSingleMothBudPairState['doAnnotateLostObjs'] = ( + self.annotLostObjsToggle.isChecked() + ) + + self.annotateSingleMothBudPairState['mother_ID'] = ( + mothID + ) + self.annotateSingleMothBudPairState['bud_ID'] = ( + budID + ) + self.annotateSingleMothBudPairState['frame_i_to_restore'] = ( + posData.frame_i + ) + self.annotateSingleMothBudPairState['last_cca_frame_i'] = ( + self.navigateScrollBar.maximum()-1 + ) + + self.warnLostCellsAction.setChecked(False) + self.annotLostObjsToggle.setChecked(False) + + self.ax1.sigRangeChanged.connect(self.highlightManualAnnotMode) + self.ax1.setHighlighted(True, color='green') + else: + frame_to_restore = self.annotateSingleMothBudPairState.get( + 'frame_i_to_restore' + ) + if frame_to_restore is None: + return + + self.store_single_mother_bud_pair_data() + + self.logger.info( + f'Restoring view to frame n. {posData.frame_i+1}...' + ) + posData.frame_i = frame_to_restore + last_cca_frame_i_to_restore = ( + self.annotateSingleMothBudPairState['last_cca_frame_i'] + ) + self.annotateSingleMotherBudPairRestoreLastCcaFrame( + last_cca_frame_i_to_restore + ) + self.get_data() + self.updateAllImages() + self.updateScrollbars() + self.ax1.sigRangeChanged.disconnect() + self.ax1.setHighlighted(False) + + self.warnLostCellsAction.setChecked( + self.annotateSingleMothBudPairState['doWarnLostObj'] + ) + self.annotLostObjsToggle.setChecked( + self.annotateSingleMothBudPairState['doAnnotateLostObjs'] + ) + + self.annotateSingleMothBudPairState = {} + + QTimer.singleShot(150, self.autoRange) + def manualAnnotPast_cb(self, checked): posData = self.data[self.pos_i] if checked: @@ -13347,18 +13507,32 @@ def highlightManualAnnotMode(self, viewBox, viewRange): self.ax1.setHighlighted(True) def updateHighlightedAxis(self): - if not self.manualAnnotPastButton.isChecked(): - return + color = None + if self.manualAnnotPastButton.isChecked(): + frame_to_restore = self.manualAnnotState.get('frame_i_to_restore') + posData = self.data[self.pos_i] + if posData.frame_i == frame_to_restore: + color = 'green' + elif posData.frame_i < frame_to_restore: + color = 'gold' + else: + color = 'red' - frame_to_restore = self.manualAnnotState.get('frame_i_to_restore') - posData = self.data[self.pos_i] - if posData.frame_i == frame_to_restore: - color = 'green' - elif posData.frame_i < frame_to_restore: - color = 'gold' - else: - color = 'red' + if self.annotateSingleMotherBudPairButton.isChecked(): + frame_to_restore = self.annotateSingleMothBudPairState.get( + 'frame_i_to_restore' + ) + posData = self.data[self.pos_i] + if posData.frame_i == frame_to_restore: + color = 'green' + elif posData.frame_i < frame_to_restore: + color = 'gold' + else: + color = 'red' + if color is None: + return + self.ax1.setHighlightingRectItemsColor(color) def updateLostNewCurrentIDs(self): @@ -18810,8 +18984,16 @@ def manualAnnotRestoreLastTrackedFrame(self, last_tracked_i_to_restore): posData.allData_li[frame_i] = data_frame_i - self.navigateScrollBar.setMaximum(last_tracked_i_to_restore+1) - self.navSpinBox.setMaximum(last_tracked_i_to_restore+1) + posData.last_tracked_i = last_tracked_i_to_restore + self.setNavigateScrollBarMaximum() + + def annotateSingleMotherBudPairRestoreLastCcaFrame( + self, cca_frame_i_to_restore + ): + if self.navigateScrollBar.maximum()-1 <= cca_frame_i_to_restore: + return + + self.resetCcaFuture(cca_frame_i_to_restore+1) def setNavigateScrollBarMaximum(self): posData = self.data[self.pos_i] @@ -20686,6 +20868,8 @@ def initGlobalAttr(self): self.timestampDialog = None self.scaleBarDialog = None self.countObjsWindow = None + + self.annotateSingleMothBudPairState = {} self.initLabelRoiModelDialog = None # Second channel used by cellpose @@ -21068,6 +21252,40 @@ def store_manual_annot_data( # data_frame_i['manually_edited_lab']['zoom_slice'] = zoom_slice + def store_single_mother_bud_pair_data(self, cca_df=None): + posData = self.data[self.pos_i] + data_frame_i = posData.allData_li[posData.frame_i] + + if cca_df is None: + cca_df = data_frame_i['acdc_df'] + + if cca_df is None: + return + + mothID = self.annotateSingleMothBudPairState.get('mother_ID') + if mothID is None: + return + + budID = self.annotateSingleMothBudPairState['bud_ID'] + single_moth_bud_pair_cca = cca_df.loc[[mothID, budID]].copy() + stored_moth_bud_pairs_cca = data_frame_i.get('moth_bud_pairs_cca') + if stored_moth_bud_pairs_cca is None: + data_frame_i['moth_bud_pairs_cca'] = single_moth_bud_pair_cca + else: + if mothID in stored_moth_bud_pairs_cca.index: + stored_moth_bud_pairs_cca = stored_moth_bud_pairs_cca.drop( + index=mothID + ) + + if budID in stored_moth_bud_pairs_cca.index: + stored_moth_bud_pairs_cca = stored_moth_bud_pairs_cca.drop( + index=budID + ) + + data_frame_i['moth_bud_pairs_cca'] = pd.concat( + [stored_moth_bud_pairs_cca, single_moth_bud_pair_cca], + ).drop_duplicates() + @exception_handler def store_data( self, pos_i=None, enforce=True, debug=False, mainThread=True, @@ -21164,6 +21382,7 @@ def store_data( if not mainThread: self.pointsLayerDataToDf(posData) + self.store_cca_df( pos_i=pos_i, mainThread=mainThread, autosave=autosave, store_cca_df_copy=store_cca_df_copy @@ -21395,7 +21614,7 @@ def checkCcaPastFramesNewIDs(self): # Remove IDs found in past frames from new_IDs list newIDs = np.array(posData.new_IDs, dtype=np.uint32) - mask_index = np.in1d(newIDs, cca_df_i.index) + mask_index = np.isin(newIDs, cca_df_i.index) posData.new_IDs = list(newIDs[~mask_index]) if not posData.new_IDs: return found_cca_df_IDs @@ -21526,6 +21745,28 @@ def _getCcaCostMatrix( return cost + def getSingleMotherBudPairCca_df(self): + if not self.annotateSingleMotherBudPairButton.isChecked(): + return + + posData = self.data[self.pos_i] + start_frame_i = ( + self.annotateSingleMothBudPairState['frame_i_to_restore'] + ) + + if posData.frame_i <= start_frame_i: + # Past frame --> no need to get cca_df + return + + mothID = self.annotateSingleMothBudPairState['mother_ID'] + budID = self.annotateSingleMothBudPairState['bud_ID'] + + # Get previous dataframe + acdc_df = posData.allData_li[posData.frame_i-1]['acdc_df'] + prev_cca_df = acdc_df[self.cca_df_colnames].loc[[mothID, budID]].copy() + + return prev_cca_df + def autoCca_df(self, enforceAll=False): """ Assign each bud to a mother with scipy linear sum assignment @@ -21551,6 +21792,13 @@ def autoCca_df(self, enforceAll=False): proceed = self.warnFrameNeverVisitedSegmMode() return notEnoughG1Cells, proceed + # Get temporary cca_df with single mother-bud pair mode active + cca_df = self.getSingleMotherBudPairCca_df() + if cca_df is not None: + posData.cca_df = cca_df + self.store_single_mother_bud_pair_data(cca_df=cca_df) + return notEnoughG1Cells, proceed + # Determine if this is the last visited frame for repeating # bud assignment on non manually correct (corrected_on_frame_i>0) buds. # The idea is that the user could have assigned division on a cell @@ -21561,15 +21809,33 @@ def autoCca_df(self, enforceAll=False): curr_df, enforceAll=enforceAll ) - frameAlreadyAnnotated = ( + # Get previous dataframe + acdc_df = posData.allData_li[posData.frame_i-1]['acdc_df'] + prev_cca_df = acdc_df[self.cca_df_colnames].copy() + + # When using "single mother-bud pair annotation" mode, + # there might be some IDs that do not have annotations yet + # --> not fully annotated frame + missingIDs = [] + if posData.cca_df is not None: + missingIDs = [ + ID for ID in posData.IDs + if ID not in posData.cca_df.index + and ID in prev_cca_df.index + ] + + frameAlreadyFullyAnnotated = ( posData.cca_df is not None and not enforceAll and not isLastVisitedAgain + and not missingIDs ) # Use stored cca_df and do not modify it with automatic stuff - if frameAlreadyAnnotated: + if frameAlreadyFullyAnnotated: return notEnoughG1Cells, proceed + self.updateLostNewCurrentIDs() + # Keep only correctedAssignIDs if requested # For the last visited frame we perform assignment again only on # IDs where we didn't manually correct assignment @@ -21595,22 +21861,24 @@ def autoCca_df(self, enforceAll=False): notEnoughG1Cells = False proceed = False return notEnoughG1Cells, proceed - - # Get previous dataframe - acdc_df = posData.allData_li[posData.frame_i-1]['acdc_df'] - prev_cca_df = acdc_df[self.cca_df_colnames].copy() - + if posData.cca_df is None: posData.cca_df = prev_cca_df.copy() else: posData.cca_df = curr_df[self.cca_df_colnames].copy() + if missingIDs: + # Add missing IDs from previous cca_df + posData.cca_df.loc[missingIDs] = prev_cca_df.loc[missingIDs] + + posData.cca_df = posData.cca_df.dropna() + # concatenate new IDs found in past frames (before frame_i-1) if found_cca_df_IDs is not None: cca_df = pd.concat([posData.cca_df, *found_cca_df_IDs]) unique_idx = ~cca_df.index.duplicated(keep='first') posData.cca_df = cca_df[unique_idx] - + # If there are no new IDs we are done if not posData.new_IDs: proceed = True @@ -21919,7 +22187,7 @@ def set_2Dlab(self, lab2D): else: posData.lab = lab2D - def get_labels( + def get_labels_array( self, from_store=False, frame_i=None, @@ -22013,7 +22281,7 @@ def _get_editID_info(self, df): for row in manually_edited_df.itertuples() ] return editID_info - + def apply_manual_edits_to_lab_if_needed(self, lab): posData = self.data[self.pos_i] data_frame_i = posData.allData_li[posData.frame_i] @@ -22057,7 +22325,7 @@ def _get_data_unvisited(self, posData, debug=False, lin_tree_init=True,): elif str(self.modeComboBox.currentText()) == 'Normal division: Lineage tree': # Warn that we are visiting a frame that was never segm-checked - # on cell cycle analysis mode + # on Normal division: Lineage tree mode msg = widgets.myMessageBox() txt = html_utils.paragraph( 'Segmentation and Tracking was never checked from ' @@ -22073,10 +22341,8 @@ def _get_data_unvisited(self, posData, debug=False, lin_tree_init=True,): return proceed_cca, never_visited # Requested frame was never visited before. Load from HDD - labels = self.get_labels() - posData.lab = self.apply_manual_edits_to_lab_if_needed( - labels - ) + lab = self.get_labels_array() + posData.lab = self.apply_manual_edits_to_lab_if_needed(lab) posData.rp = skimage.measure.regionprops(posData.lab) self.setManualBackgroundLab() @@ -22119,7 +22385,7 @@ def _get_data_unvisited(self, posData, debug=False, lin_tree_init=True,): def _get_data_visited(self, posData, debug=False, lin_tree_init=True,): # Requested frame was already visited. Load from RAM. never_visited = False - posData.lab = self.get_labels(from_store=True) + posData.lab = self.get_labels_array(from_store=True) posData.rp = skimage.measure.regionprops(posData.lab) df = posData.allData_li[posData.frame_i]['acdc_df'] if df is None: @@ -22340,8 +22606,8 @@ def initCca(self): 'No, stay on current frame') ) if goToFrameButton == msg.clickedButton: - self.addMissingIDs_cca_df(posData) - self.store_cca_df() + # self.addMissingIDs_cca_df(posData) + # self.store_cca_df() msg = 'Looking good!' self.last_cca_frame_i = last_cca_frame_i posData.frame_i = last_cca_frame_i @@ -22605,7 +22871,9 @@ def resetWillDivideInfo(self): if global_cca_df is None: return + global_cca_df.to_csv('global_cca_df_with_single_moth_bud_pair.csv') global_cca_df = load._fix_will_divide(global_cca_df) + self.storeFromConcatCcaDf(global_cca_df) def ccaCheckerStopChecking(self): @@ -22738,9 +23006,8 @@ def get_cca_df(self, frame_i=None, return_df=False, debug=False): cca_df = None i = posData.frame_i if frame_i is None else frame_i df = posData.allData_li[i]['acdc_df'] - if df is not None: - if 'cell_cycle_stage' in df.columns: - cca_df = df[self.cca_df_colnames].copy() + if df is not None and 'cell_cycle_stage' in df.columns: + cca_df = df[self.cca_df_colnames].copy() if cca_df is None and self.isSnapshot: cca_df = self.getBaseCca_df() @@ -22748,6 +23015,12 @@ def get_cca_df(self, frame_i=None, return_df=False, debug=False): if cca_df is not None: cca_df = cca_df.dropna() + else: + moth_bud_pairs_cca = ( + posData.allData_li[i].get('moth_bud_pairs_cca', None) + ) + if moth_bud_pairs_cca is not None: + cca_df = moth_bud_pairs_cca if return_df: return cca_df @@ -22888,6 +23161,9 @@ def store_cca_df( if store_cca_df_copy and cca_df is not None: posData.allData_li[i]['cca_df'] = cca_df.copy() + if self.annotateSingleMotherBudPairButton.isChecked(): + self.store_single_mother_bud_pair_data(cca_df=cca_df) + if autosave: self.enqAutosave() self.enqCcaIntegrityChecker() @@ -28401,7 +28677,7 @@ def getPrevFrameIDs(self, current_frame_i=None): return prevIDs # IDs in previous frame were not stored --> load prev lab from HDD - prev_lab = self.get_labels( + prev_lab = self.get_labels_array( from_store=False, frame_i=prev_frame_i, return_copy=False @@ -28457,6 +28733,24 @@ def setTitleText( self, lost_IDs=None, new_IDs=None, IDs_with_holes=None, tracked_lost_IDs=None ): + if self.annotateSingleMotherBudPairButton.isChecked(): + mothID = self.annotateSingleMothBudPairState.get( + 'mother_ID' + ) + budID = self.annotateSingleMothBudPairState.get( + 'bud_ID' + ) + frame_to_restore = self.annotateSingleMothBudPairState.get( + 'frame_i_to_restore' + ) + txt = ( + f'Annotating mother-bud pair {(mothID, budID)} ' + f'since frame n. {frame_to_restore+1}' + ) + htmlTxt = f'{txt}' + self.titleLabel.setText(htmlTxt) + return + if self.manualAnnotPastButton.isChecked(): lockedID = self.editIDspinbox.value() frame_to_restore = self.manualAnnotState.get('frame_i_to_restore') @@ -29000,7 +29294,7 @@ def getTrackedLostIDs(self, prev_lab=None, IDs_in_frames=None, frame_i=None): frame_i = posData.frame_i if prev_lab is None: - prev_lab = self.get_labels( + prev_lab = self.get_labels_array( from_store=True, frame_i=posData.frame_i-1, return_existing=False, diff --git a/cellacdc/load.py b/cellacdc/load.py index 2e72a546..91356640 100755 --- a/cellacdc/load.py +++ b/cellacdc/load.py @@ -515,8 +515,7 @@ def _fix_corrected_assignment_i(acdc_df: pd.DataFrame): def _fix_will_divide(acdc_df): """Resetting annotaions in GUI sometimes does not fully reset `will_divide` - column. Here we set `will_divide` back to 0 for those cells whose - next generation does not exist (division was not annotated) + column. Here we reset `will_divide` Parameters ---------- @@ -532,19 +531,22 @@ def _fix_will_divide(acdc_df): if 'cell_cycle_stage' not in acdc_df.columns: return acdc_df - required_cols = ['frame_i', 'Cell_ID', 'generation_num', 'will_divide'] + required_cols = [ + 'frame_i', 'Cell_ID', 'generation_num', 'will_divide', 'relationship', + 'relative_ID' + ] cca_df_mask = ~acdc_df['cell_cycle_stage'].isna() cca_df = acdc_df[cca_df_mask].reset_index()[required_cols] - IDs_will_divide_wrong = ( - cca_functions.get_IDs_gen_num_will_divide_wrong(cca_df) - ) - if not IDs_will_divide_wrong: + IDs_gen_num_will_divide = cca_functions.get_IDs_gen_num_will_divide(cca_df) + + if not IDs_gen_num_will_divide: return acdc_df - cca_df = cca_df.reset_index().set_index(['Cell_ID', 'generation_num']) - cca_df.loc[IDs_will_divide_wrong, 'will_divide'] = 0 + cca_df['will_divide'] = 0.0 + cca_df = cca_df.reset_index().set_index(['Cell_ID', 'generation_num']) + cca_df.loc[IDs_gen_num_will_divide, 'will_divide'] = 1.0 cca_df = cca_df.reset_index() acdc_df = acdc_df.reset_index() @@ -554,6 +556,21 @@ def _fix_will_divide(acdc_df): cca_df_index = cca_df_mask[cca_df_mask].index acdc_df.loc[cca_df_index, 'will_divide'] = cca_df['will_divide'] + # IDs_will_divide_wrong = ( + # cca_functions.get_IDs_gen_num_will_divide_wrong(cca_df) + # ) + # if IDs_will_divide_wrong: + # cca_df = cca_df.reset_index().set_index(['Cell_ID', 'generation_num']) + # cca_df.loc[IDs_will_divide_wrong, 'will_divide'] = 0 + # cca_df = cca_df.reset_index() + # acdc_df = acdc_df.reset_index() + + # cca_df = cca_df.set_index(['frame_i', 'Cell_ID']) + # acdc_df = acdc_df.set_index(['frame_i', 'Cell_ID']) + + # cca_df_index = cca_df_mask[cca_df_mask].index + # acdc_df.loc[cca_df_index, 'will_divide'] = cca_df['will_divide'] + return acdc_df def _add_missing_columns(acdc_df): diff --git a/cellacdc/myutils.py b/cellacdc/myutils.py index 8fad6fdd..5806ea69 100644 --- a/cellacdc/myutils.py +++ b/cellacdc/myutils.py @@ -5001,7 +5001,8 @@ def get_empty_stored_data_dict(): 'rois': [], 'delMasks': [], 'delIDsROI': [], 'state': [] }, 'IDs': [], - 'manually_edited_lab': {'lab': {}, 'zoom_slice': None} + 'manually_edited_lab': {'lab': {}, 'zoom_slice': None}, + 'single_moth_bud_pair_cca': None, } def iterate_along_axes(arr, axes, arr_ndim=None): diff --git a/cellacdc/workers.py b/cellacdc/workers.py index 917bca9d..7dff64e0 100755 --- a/cellacdc/workers.py +++ b/cellacdc/workers.py @@ -842,7 +842,7 @@ def __init__(self, mutex, waitCond, savedSegmData): self.stopSaving = False self.isSaving = False self.isPaused = False - self.dataQ = deque(maxlen=5) + self.dataQ = deque(maxlen=2) self.isAutoSaveON = False self.isAutoSaveAnnotON = True self.debug = False