mne-qt-browser icon indicating copy to clipboard operation
mne-qt-browser copied to clipboard

ENH: framework for drawing channel-specific annotations interactively

Open scott-huberty opened this issue 2 years ago • 5 comments

Now that #202 is merged, I wanted to leave a summary here of our game-plan for interactively drawing channel-specific annotations. (this was discussed with @larsoner and @drammock during the September 2023 intermediate code sprint).

CC @nmarkowitz who I believe is also interested in working on this.

To draw a channel specific annotation interactively

  1. Enter annotation mode, and draw a (channel-agnostic) annotation as one normally would
  2. (After drawing the annotation, it is automatically the selected annotation). Now click on a channel name to convert the annotation from channel agnostic to channel specific, and to associate the clicked channel with the annotation.
  3. Click on more channels to associate them with the annotation.

Ground work that needs to be done.

  • [ ] When in Annotation mode, one should be able to select and deselect annotations (i.e. it should be possible for NO annotations to be selected. Currently this is only possible upon initial launch of the browser, but once any annotation is selected, then there will always be some annotation selected.)
  • [ ] When in annotation mode, channel names/traces should not be toggled between bad/not-bad when clicked on. Instead, when clicked on they will become associated with an annotation.
  • [ ] However the above behaviour should be disallowed if the selected annotation is off-screen, to avoid scenarios where the user unwittingly changes an annotation without receiving visual feedback.
  • [ ] When in annotation mode, once a channel-agnostic annotation is converted to a channel-specific annotation, it's visualization should be updated to reflect this (it should have a lower alpha and a dashed border, plus the shaded rectangles around associated channels).
  • [ ] If you de-select all the channel names that are associated with an annotation, it should be converted to a channel-agnostic annotation.

scott-huberty avatar Oct 02 '23 17:10 scott-huberty

... and then we have to figure out how to disallow changing channel removal/addition when the annotation is off-screen time-wise. I think the simple solution is that a channel can be added/removed from the selected annotation only if some part of the the selected annotation is in the visible time span.

larsoner avatar Oct 02 '23 17:10 larsoner

And could we also add that the selected annotation can be changed into a channel-wise annotation and/or additional channels can be added to the currently selected channel-wise annotation by clicking on the channel trace? Thus disabling entirely the bad/not bad interaction when in annotation mode.

mscheltienne avatar Oct 02 '23 17:10 mscheltienne

Updated!

scott-huberty avatar Oct 02 '23 22:10 scott-huberty

Here's some code that could be useful. It works by interacting with the ChannelAxis (the y-axis listing channel names) and doing shift+left-click to toggle the channel being part of the active annotation. It adds to the already existing mouseClickEvent for it. Basically, if in annotation mode, it gets the name of the channel pressed, gets the annotation index currently active, and then adds/removes that channel to the list of channels associated with that annotation. The added function for this is in the "####" section. I think pieces of it can be used for this next step.

def mouseClickEvent(self, event):
        """Customize mouse click events for ChannelAxis"""
        # Clean up channel-texts
        if not self.mne.butterfly:
            self.ch_texts = {k: v for k, v in self.ch_texts.items()
                             if k in [tr.ch_name for tr in self.mne.traces]}
            # Get channel-name from position of channel-description
            ypos = event.scenePos().y()
            y_values = np.asarray(list(self.ch_texts.values()))[:, 1, :]
            y_diff = np.abs(y_values - ypos)
            ch_idx = int(np.argmin(y_diff, axis=0)[0])
            ch_name = list(self.ch_texts)[ch_idx]
            trace = [tr for tr in self.mne.traces
                     if tr.ch_name == ch_name][0]

	########################################################
            # If shift+left-click in annotation mode then add to the annotation
            if event.button() == Qt.LeftButton and bool(Qt.ShiftModifier) and self.mne.annotation_mode:
                # Find what the currently active annotation is: self.mne.current_description
                # Access the instance of the annotation
                current_annotation_idx = [annot_ii for annot_ii in range(len(self.mne.inst.annotations))
                                          if self.mne.inst.annotations[annot_ii]['description'] == self.mne.current_description][0]

                ch_list_in_annot = list(self.mne.inst.annotations.ch_names[current_annotation_idx])
                if ch_name not in ch_list_in_annot:
                    self.mne.inst.annotations.ch_names[current_annotation_idx] = tuple( ch_list_in_annot + [ch_name] )
                else:
                    # Remove ch_name from annotation
                    ch_list_in_annot.pop(ch_list_in_annot.index(ch_name))
                    self.mne.inst.annotations.ch_names[current_annotation_idx] = tuple(ch_list_in_annot)
	########################################################


            elif event.button() == Qt.LeftButton:
                trace.toggle_bad()
            elif event.button() == Qt.RightButton:
                self.weakmain()._create_ch_context_fig(trace.range_idx)

nmarkowitz avatar Oct 03 '23 14:10 nmarkowitz

Thx @nmarkowitz !

I probably won't have time this month to get to this so feel free to start a PR if you beat me to it.

current_annotation_idx = [annot_ii for annot_ii in range(len(self.mne.inst.annotations)) if self.mne.inst.annotations[annot_ii]['description'] == self.mne.current_description][0]

FYI I don't think this will work. If there is more than 1 annotation with the same description, this will always return the index of the first annotation that matches the description. I think we'll need a more robust way to find the current annotation. (EDIT) I can probably make a suggestion but I'd need to dig into the code a bit 😄

scott-huberty avatar Oct 03 '23 14:10 scott-huberty