Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: framework for drawing channel-specific annotations interactively #204

Open
5 tasks
scott-huberty opened this issue Oct 2, 2023 · 5 comments
Open
5 tasks

Comments

@scott-huberty
Copy link
Contributor

scott-huberty commented Oct 2, 2023

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.
@larsoner
Copy link
Member

larsoner commented Oct 2, 2023

... 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.

@mscheltienne
Copy link
Member

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.

@scott-huberty
Copy link
Contributor Author

Updated!

@nmarkowitz
Copy link
Contributor

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)

@scott-huberty
Copy link
Contributor Author

scott-huberty commented Oct 3, 2023

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 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants