Skip to content

Commit

Permalink
Merge branch 'main' into 18-napari-import-data
Browse files Browse the repository at this point in the history
  • Loading branch information
HaleySchuhl authored Sep 12, 2024
2 parents 730cb8c + 1dd5436 commit 9a345f2
Show file tree
Hide file tree
Showing 26 changed files with 1,189 additions and 44 deletions.
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[run]
omit =
config.py
config-3.py
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ See [this page](https://plantcv.readthedocs.io/en/latest/pr_review_process/) for
- [ ] Test coverage remains 100%
- [ ] Documentation tested
- [ ] New documentation pages added to `plantcv/mkdocs.yml`
- [ ] Changes to function input/output signatures added to `updating.md`
- [ ] Changes to function input/output signatures added to `changelog.md`
- [ ] Code reviewed
- [ ] PR approved
8 changes: 4 additions & 4 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10']
python-version: ['3.9', '3.10', '3.11']
os: [ubuntu-latest]
env:
OS: ${{ matrix.os }}
Expand All @@ -30,8 +30,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends libyaml-dev libegl1-mesa libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0 libxcb-cursor0 xserver-xephyr xvfb
sudo apt-get install --no-install-recommends libyaml-dev libegl1-mesa libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0 libxcb-cursor0 xserver-xephyr
python -m pip install --upgrade pip
pip install flake8 pytest pytest-cov pytest-qt pytest-xvfb ipython anyio
- name: Lint with flake8
Expand All @@ -48,8 +47,9 @@ jobs:
pip uninstall -y opencv-python
pip install opencv-python-headless
- name: Tests
uses: aganders3/headless-gui@v2
uses: aganders3/headless-gui@v2.2
with:
xvfb-screen-size: 1280x720x24
run: pytest --cov-report=xml --cov=./
- name: Upload coverage to Deepsource
uses: deepsourcelabs/test-coverage-action@master
Expand Down
373 changes: 373 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
## Changelog

All notable changes to this project will be documented below.

#### annotate.get_centroids

* v0.1dev: coords_list = **annotate.get_centroids**(*bin_img*)

#### annotate.napari_classes

* v0.1dev: class_list = **annotate.napari_classes**(*viewer*)

#### annotate.napari_join_labels

* v0.1dev: relabeled_mask, mask_dict = **annotate.napari_join_labels**(*img, viewer*)

#### annotate.napari_label_classes

* v0.1dev: viewer = **annotate.napari_label_classes**(*img, classes, show=True*)

#### annotate.napari_open

* v0.1dev: viewer = **annotate.napari_open**(*img, mode = 'native', show=True*)

#### annotate.napari_save_coor

* v0.1dev: datadict = **annotate.napari_save_coor**(*viewer, filepath*)

#### annotate.Points

* v0.1dev: viewer = **annotate.Points**(*img, figsize=(12,6), label="dafault"*)
44 changes: 44 additions & 0 deletions docs/get_centroids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## Get Centroids

Extract the centroid coordinate (column,row) from regions in a binary image.

**plantcv.annotate.get_centroids**(*bin_img*)

**returns** list containing coordinates of centroids

- **Parameters:**
- bin_img - Binary image containing the connected regions to consider
- **Context:**
- Given an arbitrary mask of the objects of interest, `get_centroids`
returns a list of coordinates that can the be imported into the annotation class [Points](Points.md).

- **Example use:**
- Below

**Binary image**

![count_img](img/documentation_images/get_centroids/discs_mask.png)

```python

from plantcv import plantcv as pcv

# Set global debug behavior to None (default), "print" (to file),
# or "plot"
pcv.params.debug = "plot"

# Apply get centroids to the binary image
coords = pcv.annotate.get_centroids(bin_img=binary_img)
print(coords)
# [[1902, 600], [1839, 1363], [1837, 383], [1669, 1977], [1631, 1889], [1590, 1372], [1550, 1525],
# [1538, 1633], [1522, 1131], [1494, 2396], [1482, 1917], [1446, 1808], [1425, 726], [1418, 2392],
# [1389, 198], [1358, 1712], [1288, 522], [1289, 406], [1279, 368], [1262, 1376], [1244, 1795],
# [1224, 1327], [1201, 624], [1181, 725], [1062, 85], [999, 840], [885, 399], [740, 324], [728, 224],
# [697, 860], [660, 650], [638, 2390], [622, 1565], [577, 497], [572, 2179], [550, 2230], [547, 1826],
# [537, 892], [538, 481], [524, 2144], [521, 2336], [497, 201], [385, 1141], [342, 683], [342, 102],
# [332, 1700], [295, 646], [271, 60], [269, 1626], [210, 1694], [189, 878], [178, 1570], [171, 2307],
# [61, 286], [28, 2342]]

```

**Source Code:** [Here](https://github.com/danforthcenter/plantcv-annotate/blob/main/plantcv/annotate/get_centroids.py)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions docs/napari_join_labels.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ img, path, name = pcv.readimage("./grayimg.png")

viewer = pcvan.napari_label_classes(img=img, ['background', 'wing','seed'])

labeledmask, mask_dict = pcvan.napari_join_lables(img=img, viewer)

# Should open interactive napari viewer

labeledmask, mask_dict = pcvan.napari_join_lables(img=img, viewer=viewer)

```

![Screenshot](img/documentation_images/napari_label_classes/napari_label_classes.png)
Expand Down
2 changes: 1 addition & 1 deletion docs/napari_label_classes.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## Label Image with Napari

This function opens an image in Napari and then defines a set of classes to label. A random shape label is assigned to each class.
Image can be annotate as long as viewer is open.
Image can be annotated as long as viewer is open.

**plantcv.annotate.napari_label_classes*(*img, classes, show=True*)

Expand Down
9 changes: 5 additions & 4 deletions docs/napari_open.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
## Open Image with Napari

Open image data (e.g. RGB, gray, hyperspectral) with an interactive Napari viewer. If a gray image is opened, the image will be pseudocolored for better visualization.
Open image data (e.g. RGB, gray, hyperspectral) with an interactive Napari viewer. Labeled masks may be colorized for better visualization.

**plantcv.annotate.napari_open**(*img, show=True*)
**plantcv.annotate.napari_open**(*img, mode='native', show=True*)

**returns** napari viewer object

- **Parameters:**
- img - image data (compatible with gray, RGB, and hyperspectral data. If data is hyperspecral it should be the array e.g. hyperspectral.array_data)
- img - image data (compatible with gray, RGB, and hyperspectral data. If data is hyperspecral it should be the array e.g. `hyperspectral.array_data`)
- mode - 'native' or 'colorize'. If 'colorized' is selected gray images will be colorized.
- show - if show = True, viewer is launched. False setting is useful for test purposes.

- **Context:**
Expand All @@ -24,7 +25,7 @@ import plantcv.annotate as pcvan
# Create an instance of the Points class
img, path, name = pcv.readimage("./grayimg.png")

viewer = pcvan.napari_open(img=img)
viewer = pcvan.napari_open(img=img, mode='colorize')

# Should open interactive napari viewer

Expand Down
6 changes: 2 additions & 4 deletions docs/napari_save_coor.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Save Points Labeled in Napari to a File
- Filepath - File to save data. If the file exits an extension will be added.

- **Context:**
- Save points labeled in Napari to a file in case the same points need to be used.
- Save points labeled in Napari to a file to checkpoint annotation progress or reuse.

- **Example use:**
- Save points labeled to a file
Expand All @@ -23,13 +23,11 @@ import plantcv.annotate as pcvan

# Create an instance of the Points class
img, path, name = pcv.readimage("./grayimg.png")

# Should open interactive napari viewer
viewer = pcvan.napari_label_classes(img=img, classes=['background', 'wing','seed'])

dictobj = pcvan.napari_save_coor(viewer, 'testdata.txt')

# Should open interactive napari viewer

```

![Screenshot](img/documentation_images/napari_label_classes/napari_label_classes.png)
Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ nav:
- Napari Read Coor: napari_read_coor.md
- Napari Save Coor: napari_save_coor.md
- Points: Points.md

- Get Centroids: get_centroids.md
markdown_extensions:
- toc:
permalink: True
Expand Down
7 changes: 7 additions & 0 deletions plantcv/annotate/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
from importlib.metadata import version
from plantcv.annotate.classes import Points
from plantcv.annotate.get_centroids import get_centroids
from plantcv.annotate.napari_classes import napari_classes
from plantcv.annotate.napari_open import napari_open
from plantcv.annotate.napari_label_classes import napari_label_classes
from plantcv.annotate.napari_join_labels import napari_join_labels
from plantcv.annotate.napari_save_coor import napari_save_coor
from plantcv.annotate.napari_read_coor import napari_read_coor

# Auto versioning
__version__ = version("plantcv-annotate")

__all__ = [
"Points",
"get_centroids",
"napari_classes",
"napari_open",
"napari_label_classes",
Expand Down
160 changes: 160 additions & 0 deletions plantcv/annotate/classes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Class helpers

# Imports
import cv2
import json
from math import floor
import matplotlib.pyplot as plt
from plantcv.plantcv.annotate.points import _find_closest_pt
from plantcv.plantcv import warn


class Points:
"""Point annotation/collection class to use in Jupyter notebooks. It allows the user to
interactively click to collect coordinates from an image. Left click collects the point and
right click removes the closest collected point.
"""

def __init__(self, img, figsize=(12, 6), label="default", color="r", view_all=False):
"""Points initialization method.
Parameters
----------
img : numpy.ndarray
image to annotate
figsize : tuple, optional
figure plotting size, by default (12, 6)
label : str, optional
class label, by default "default"
"""
self.img = img
self.figsize = figsize
self.label = label # current label
self.color = color # current color
self.view_all = view_all # a flag indicating whether or not view all labels
self.coords = {} # dictionary of all coordinates per group label
self.events = [] # includes right and left click events
self.count = {} # a dictionary that saves the counts of different groups (labels)
self.sample_labels = [] # list of all sample labels, one to one with points collected
self.colors = {} # all used colors

self.view(label=self.label, color=self.color, view_all=self.view_all)

def onclick(self, event):
"""Handle mouse click events
Parameters
----------
event : matplotlib.backend_bases.MouseEvent
matplotlib MouseEvent object
"""
print(type(event))
self.events.append(event)
if event.button == 1:
# Add point to the plot
self.ax.plot(event.xdata, event.ydata, marker='x', c=self.color)
self.coords[self.label].append((floor(event.xdata), floor(event.ydata)))
self.count[self.label] += 1
self.sample_labels.append(self.label)
else:
idx_remove, _ = _find_closest_pt((event.xdata, event.ydata), self.coords[self.label])
# remove the closest point to the user right clicked one
self.coords[self.label].pop(idx_remove)
self.count[self.label] -= 1
idx_remove = idx_remove + self.p_not_current
self.ax.lines[idx_remove].remove()
self.sample_labels.pop(idx_remove)
self.fig.canvas.draw()

def print_coords(self, filename):
"""Save collected coordinates to a file.
Parameters
----------
filename : str
output filename
"""
# Open the file for writing
with open(filename, "w") as fp:
# Save the data in JSON format with indentation
json.dump(obj=self.coords, fp=fp, indent=4)

def import_list(self, coords, label="default"):
"""Import coordinates.
Parameters
----------
coords : list
list of coordinates (tuples)
label : str, optional
class label, by default "default"
"""
if label not in self.coords:
self.coords[label] = []
for (y, x) in coords:
self.coords[label].append((x, y))
self.count[label] = len(self.coords[label])
self.view(label=label, color=self.color, view_all=False)
else:
warn(f"{label} already included and counted, nothing is imported!")

def import_file(self, filename):
"""Import coordinates from a file.
Parameters
----------
filename : str
JSON file containing Points annotations
"""
with open(filename, "r") as fp:
coords = json.load(fp)

keys = list(coords.keys())

for key in keys:
keycoor = coords[key]
keycoor = list(map(lambda sub: (sub[1], sub[0]), keycoor))
self.import_list(keycoor, label=key)

def view(self, label="default", color="r", view_all=False):
"""View coordinates for a specific class label.
Parameters
----------
label : str, optional
class label, by default "default"
color : str, optional
marker color, by default "r"
view_all : bool, optional
view all classes or a single class, by default False
"""
if label not in self.coords and color in self.colors.values():
warn("The color assigned to the new class label is already used, if proceeding, "
"items from different classes will not be distinguishable in plots!")
self.label = label
self.color = color
self.view_all = view_all

if self.label not in self.coords:
self.coords[self.label] = []
self.count[self.label] = 0
self.colors[self.label] = self.color

self.fig, self.ax = plt.subplots(1, 1, figsize=self.figsize)

self.events = []
self.fig.canvas.mpl_connect('button_press_event', self.onclick)

self.ax.imshow(cv2.cvtColor(self.img, cv2.COLOR_BGR2RGB))
self.ax.set_title("Please left click on objects\n Right click to remove")
self.p_not_current = 0
# if view_all is True, show all already marked markers
if self.view_all:
for k in self.coords:
for (x, y) in self.coords[k]:
self.ax.plot(x, y, marker='x', c=self.colors[k])
if self.label not in self.coords or len(self.coords[self.label]) == 0:
self.p_not_current += 1
else:
for (x, y) in self.coords[self.label]:
self.ax.plot(x, y, marker='x', c=self.color)
Loading

0 comments on commit 9a345f2

Please sign in to comment.