Skip to content

Commit

Permalink
Merge pull request #4 from melonora/jose_branch1
Browse files Browse the repository at this point in the history
Jose branch1
  • Loading branch information
melonora authored Mar 26, 2024
2 parents e5853a1 + 78389d7 commit fd50273
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 1 deletion.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,38 @@ You can install `napari-cell-gater` via [pip]:



## How to use
Writing this helps me organize my thoughts on what to do next

1. Users will select the necesary directories: images, masks, and quantification directories.

Assumptions:
files inside these directories are named according to the samples name.
For example, the image for sample 1, should be named "1.ome.tif"; the mask file should be named "1.tif"; and the quantification file "1.csv"

2. Users will then select which channels to gate, default is all of them.

3. Users will then select a sample, and a marker from dropdown menus.
Upon selecting sample and marker.
3 layers will load:
1. the nuclear stain of the image (we assume it is channel 0, perhaps we can allow users to pick which one to use with dropdown)
2. the segmentation mask (as a labels layer) (for large images this might be a problem; Cylinter solves this by pyrimidazing a binary label)
3. the channel_to_be_gated
a Widget will appear showing a scatter plot (default: x-axis=channel_to_be_gated intensity, y-axis=Area) (y-axis could be change to another column)
underneath the scatterplot a slider will appear, the position of the slider will show up as a vertical line in the scatter plot

4. Users will then adjust the contrast with the Napari menu

5. Users will drag the slider to what they think is correct

6. User will click a button, which will then plot a points layer on top of the image.
The points should be on the x,y coordenates of cells that have a channel intensity larger than the threshold picked by the slider.

7. User will repeat steps 5 and 6 until satisfied

8. User wil then click a button to save the gate for the current marker and sample.
Here either the next marker will load, or if all markers for a sample are done, go to next sample.
I prefer an automated switch, but we should allow a user to go back to sample_marker of interest for review.

## Contributing

Expand Down
5 changes: 5 additions & 0 deletions src/cell_gater/model/data_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ class DataModel:
_lower_bound_marker: str | None = field(default=None, init=False)
_upper_bound_marker: str | None = field(default=None, init=False)
_markers: Sequence[str] = field(default_factory=list, init=False)

_active_marker: str | None = field(default=None, init=False)
_active_sample: str | None = field(default=None, init=False)

_gates: pd.DataFrame = field(default_factory=pd.DataFrame, init=False)
_current_gate: float = field(default_factory=float, init=False)

def __post_init__(self) -> None:
"""Allow fields in the dataclass to emit events when changed."""
Expand Down
87 changes: 86 additions & 1 deletion src/cell_gater/widgets/scatter_widget.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
from typing import Any

# for scatter plot
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
from qtpy.QtCore import Qt
from qtpy.QtWidgets import (
QComboBox,
QLabel,
QPushButton,
QSizePolicy,
QVBoxLayout,
QWidget,
)

from cell_gater.model.data_model import DataModel
from cell_gater.utils.misc import napari_notification


class ScatterWidget(QWidget):
Expand All @@ -33,7 +38,8 @@ def __init__(self, model: DataModel) -> None:
self.layout().addWidget(self.sample_selection_dropdown, 4, 1)

def _on_sample_changed(self):
pass
"""Set the active sample."""
self.model.active_sample = self.sample_selection_dropdown.currentText()

def _set_samples_dropdown(self, event: Any) -> None:
"""Set the items for the samples dropdown QComboBox."""
Expand All @@ -45,3 +51,82 @@ def _set_samples_dropdown(self, event: Any) -> None:
if len(self.model.samples) > 0:
self.sample_selection_dropdown.addItems([None])
self.sample_selection_dropdown.addItems(self.model.samples)

def plot_scatter_plot(self, model: DataModel) -> None:
"""Plot the scatter plot."""
# check if sample and marker are selected
assert self.model.active_marker is not None
assert self.model.active_sample is not None

# get the data for the scatter plot
df = self.model.regionprops_df
df = df[df["sample_id"] == self.model.active_sample]

# plot the scatter plot
fig, ax = plt.subplots()
# plot scatter on canvas axis

# if desired_y_axis is None:
# desired_y_axis = "Area"
# should this be through a dropdown widget? or datamodel attribute?

ax.scatter(
x=df[self.active_marker],
y=df["Area"], # later change to desired_y_axis
color="steelblue",
ec="white",
lw=0.1,
alpha=1.0,
s=80000 / int(df.shape[0]),
)

ax.set_ylabel("Area") # later change to desired_y_axis
ax.set_xlabel(f"{self.active_marker} intensity")

# add vertical line at current gate if it exists
if self.model.current_gate is not None:
ax.axvline(x=self.model.current_gate, color="red", linewidth=1.0, linestyle="--")

minimum = df[self.model.active_marker].min()
maximum = df[self.model.active_marker].max()
value_initial = df[self.model.active_marker].median()
value_step = minimum / 100

# add slider as an axis, underneath the scatter plot
axSlider = fig.add_axes([0.1, 0.01, 0.8, 0.03], facecolor="yellow")
slider = Slider(axSlider, "Gate", minimum, maximum, valinit=value_initial, valstep=value_step, color="black")

def update_gate(val):
self.model.current_gate = val
ax.axvline(x=self.model.current_gate, color="red", linewidth=1.0, linestyle="--")
napari_notification(f"Gate set to {val}")

slider.on_changed(update_gate)

# TODO add the plot to a widget and display it

def plot_points(self, model: DataModel) -> None:
"""Plot positive cells in Napari."""
if self.model.active_marker is not None:
marker = self.model.active_marker
if self.model.active_sample is not None:
sample = self.model.active_sample

viewer = self.model.viewer
df = self.model.regionprops_df
df = df[df["sample_id"] == self.model.active_sample]
gate = self.model.gates.loc[marker, sample]

viewer.add_points(
df[df[marker] > gate][["X_centroid", "Y_centroid"]],
name=f"{gate} and its positive cells",
face_color="red",
edge_color="black",
size=15,
)

def plot_points_button(self):
"""Plot points button."""
self.plot_points_button = QPushButton("Plot Points")
self.plot_points_button.clicked.connect(self.plot_points)
self.layout().addWidget(self.plot_points_button, 1, 2) # not sure where to put this button

0 comments on commit fd50273

Please sign in to comment.