From 80b1cd3f2ab4fdcd0e8a6c3bd37f7d3543aac71e Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Thu, 29 Feb 2024 11:56:08 -0600 Subject: [PATCH 01/96] add _find_closest and onclick --- plantcv/annotate/classes.py | 69 +++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 plantcv/annotate/classes.py diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py new file mode 100644 index 0000000..86e6c2a --- /dev/null +++ b/plantcv/annotate/classes.py @@ -0,0 +1,69 @@ +# Class helpers + +# Imports +import cv2 +import numpy as np +from math import floor +import matplotlib.pyplot as plt +from scipy.spatial import distance + + +def _find_closest_pt(pt, pts): + """ + Find the closest (Euclidean) point to a given point from a list of points. + + Inputs: + pt = coordinates of a point + pts = a list of tuples (coordinates) + + Outputs: + idx = index of the closest point + coord = coordinates of the closest point + + :param pt: tuple + :param pts: list + :return idx: int + :return coord: tuple + """ + if pt in pts: + idx = pts.index(pt) + return idx, pt + + dists = distance.cdist([pt], pts, 'euclidean') + idx = np.argmin(dists) + return idx, pts[idx] + +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)): + """Initialization + :param img: image data + :param figsize: desired figure size, (12,6) by default + :attribute points: list of points as (x,y) coordinates tuples + """ + self.fig, self.ax = plt.subplots(1, 1, figsize=figsize) + self.ax.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) + + self.points = [] + self.events = [] + + self.fig.canvas.mpl_connect('button_press_event', self.onclick) + + def onclick(self, event): + """Handle mouse click events.""" + self.events.append(event) + if event.button == 1: + + self.ax.plot(event.xdata, event.ydata, 'x', c='red') + self.points.append((floor(event.xdata), floor(event.ydata))) + + else: + idx_remove, _ = _find_closest_pt((event.xdata, event.ydata), self.points) + # remove the closest point to the user right clicked one + self.points.pop(idx_remove) + self.ax.lines[idx_remove].remove() + self.fig.canvas.draw() From 7d0e0e8b8b35cecc47c07196bac12ba0ea1f6acd Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Fri, 1 Mar 2024 09:55:05 -0600 Subject: [PATCH 02/96] test suite setup --- plantcv/tests/__init__.py | 0 plantcv/tests/conftest.py | 0 plantcv/tests/plantcv/annotate/__init__.py | 0 plantcv/tests/plantcv/annotate/conftest.py | 21 +++++++++ plantcv/tests/plantcv/annotate/test_points.py | 46 +++++++++++++++++++ 5 files changed, 67 insertions(+) create mode 100644 plantcv/tests/__init__.py create mode 100644 plantcv/tests/conftest.py create mode 100644 plantcv/tests/plantcv/annotate/__init__.py create mode 100644 plantcv/tests/plantcv/annotate/conftest.py create mode 100644 plantcv/tests/plantcv/annotate/test_points.py diff --git a/plantcv/tests/__init__.py b/plantcv/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plantcv/tests/conftest.py b/plantcv/tests/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/plantcv/tests/plantcv/annotate/__init__.py b/plantcv/tests/plantcv/annotate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plantcv/tests/plantcv/annotate/conftest.py b/plantcv/tests/plantcv/annotate/conftest.py new file mode 100644 index 0000000..1cb5f66 --- /dev/null +++ b/plantcv/tests/plantcv/annotate/conftest.py @@ -0,0 +1,21 @@ +import pytest +import os +import matplotlib + +# Disable plotting +matplotlib.use("Template") + + +class AnnotateTestData: + def __init__(self): + """Initialize simple variables.""" + # Test data directory + self.datadir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "testdata") + # RGB image + self.small_rgb_img = os.path.join(self.datadir, "setaria_small_plant_rgb.png") + + +@pytest.fixture(scope="session") +def annotate_test_data(): + """Test data object for the PlantCV annotate submodule.""" + return AnnotateTestData() \ No newline at end of file diff --git a/plantcv/tests/plantcv/annotate/test_points.py b/plantcv/tests/plantcv/annotate/test_points.py new file mode 100644 index 0000000..e74cba6 --- /dev/null +++ b/plantcv/tests/plantcv/annotate/test_points.py @@ -0,0 +1,46 @@ +import cv2 +import matplotlib +from plantcv.plantcv import Points + + +def test_points_interactive(annotate_test_data): + """Test for PlantCV.""" + # Read in a test grayscale image + img = cv2.imread(annotate_test_data.small_rgb_img) + + # initialize interactive tool + drawer_rgb = Points(img, figsize=(12, 6)) + + # simulate mouse clicks + # event 1, left click to add point + e1 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=1) + point1 = (200, 200) + e1.xdata, e1.ydata = point1 + drawer_rgb.onclick(e1) + + # event 2, left click to add point + e2 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=1) + e2.xdata, e2.ydata = (300, 200) + drawer_rgb.onclick(e2) + + # event 3, left click to add point + e3 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=1) + e3.xdata, e3.ydata = (50, 50) + drawer_rgb.onclick(e3) + + # event 4, right click to remove point with exact coordinates + e4 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=3) + e4.xdata, e4.ydata = (50, 50) + drawer_rgb.onclick(e4) + + # event 5, right click to remove point with coordinates close but not equal + e5 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=3) + e5.xdata, e5.ydata = (301, 200) + drawer_rgb.onclick(e5) + + assert drawer_rgb.points[0] == point1 \ No newline at end of file From 9325e4db98bd4e672eeab8d1e8d532a496d6776d Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Fri, 1 Mar 2024 09:58:11 -0600 Subject: [PATCH 03/96] add test data --- .../tests/testdata/setaria_small_plant_rgb.png | Bin 0 -> 37309 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 plantcv/tests/testdata/setaria_small_plant_rgb.png diff --git a/plantcv/tests/testdata/setaria_small_plant_rgb.png b/plantcv/tests/testdata/setaria_small_plant_rgb.png new file mode 100644 index 0000000000000000000000000000000000000000..551e3c965c211431aad753393229931f465e7bbe GIT binary patch literal 37309 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 literal 0 HcmV?d00001 From bfbeee3bde599b46eb831bf9ad1447a382f6f116 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Fri, 1 Mar 2024 11:54:44 -0600 Subject: [PATCH 04/96] import _find_closest_pt from pcv --- plantcv/annotate/classes.py | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py index 86e6c2a..00c9a48 100644 --- a/plantcv/annotate/classes.py +++ b/plantcv/annotate/classes.py @@ -5,34 +5,9 @@ import numpy as np from math import floor import matplotlib.pyplot as plt -from scipy.spatial import distance +from plantcv.plantcv.annotate.points import _find_closest_pt -def _find_closest_pt(pt, pts): - """ - Find the closest (Euclidean) point to a given point from a list of points. - - Inputs: - pt = coordinates of a point - pts = a list of tuples (coordinates) - - Outputs: - idx = index of the closest point - coord = coordinates of the closest point - - :param pt: tuple - :param pts: list - :return idx: int - :return coord: tuple - """ - if pt in pts: - idx = pts.index(pt) - return idx, pt - - dists = distance.cdist([pt], pts, 'euclidean') - idx = np.argmin(dists) - return idx, pts[idx] - 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 From 9f3ef4210297229243c871e06c79e0589b7f5383 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Fri, 1 Mar 2024 11:54:58 -0600 Subject: [PATCH 05/96] update test name and import statement --- plantcv/tests/plantcv/annotate/test_points.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plantcv/tests/plantcv/annotate/test_points.py b/plantcv/tests/plantcv/annotate/test_points.py index e74cba6..00fb06c 100644 --- a/plantcv/tests/plantcv/annotate/test_points.py +++ b/plantcv/tests/plantcv/annotate/test_points.py @@ -1,9 +1,9 @@ import cv2 import matplotlib -from plantcv.plantcv import Points +from plantcv.annotate.classes import Points -def test_points_interactive(annotate_test_data): +def test_points(annotate_test_data): """Test for PlantCV.""" # Read in a test grayscale image img = cv2.imread(annotate_test_data.small_rgb_img) From 144312e9c32349c515b7eb60b4740e53054e57fb Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 09:14:03 -0600 Subject: [PATCH 06/96] trailing whitespace and unused import --- plantcv/annotate/classes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py index 00c9a48..5c2dc70 100644 --- a/plantcv/annotate/classes.py +++ b/plantcv/annotate/classes.py @@ -2,10 +2,9 @@ # Imports import cv2 -import numpy as np from math import floor import matplotlib.pyplot as plt -from plantcv.plantcv.annotate.points import _find_closest_pt +from plantcv.plantcv.annotate.points import _find_closest_pt class Points: From c53dfa7d5108c31a405b98b97823c7c22b03fe85 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 09:15:53 -0600 Subject: [PATCH 07/96] add line return at end of file --- plantcv/tests/plantcv/annotate/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plantcv/tests/plantcv/annotate/conftest.py b/plantcv/tests/plantcv/annotate/conftest.py index 1cb5f66..7263063 100644 --- a/plantcv/tests/plantcv/annotate/conftest.py +++ b/plantcv/tests/plantcv/annotate/conftest.py @@ -18,4 +18,4 @@ def __init__(self): @pytest.fixture(scope="session") def annotate_test_data(): """Test data object for the PlantCV annotate submodule.""" - return AnnotateTestData() \ No newline at end of file + return AnnotateTestData() From 3b02f56fce2d1d33578965a916a4b2c6220b48d6 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 09:15:57 -0600 Subject: [PATCH 08/96] add line return at end of file --- plantcv/tests/plantcv/annotate/test_points.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plantcv/tests/plantcv/annotate/test_points.py b/plantcv/tests/plantcv/annotate/test_points.py index 00fb06c..d510f29 100644 --- a/plantcv/tests/plantcv/annotate/test_points.py +++ b/plantcv/tests/plantcv/annotate/test_points.py @@ -43,4 +43,4 @@ def test_points(annotate_test_data): e5.xdata, e5.ydata = (301, 200) drawer_rgb.onclick(e5) - assert drawer_rgb.points[0] == point1 \ No newline at end of file + assert drawer_rgb.points[0] == point1 From a985065ada82838e50e7d987df051a17890f0a9d Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 09:19:04 -0600 Subject: [PATCH 09/96] refactor points attribute to coords https://app.deepsource.com/gh/danforthcenter/plantcv-annotate/run/4dd4c28b-a30f-46b5-b846-75cdaa4ec90e/python/PTC-W0052?listindex=4 --- plantcv/annotate/classes.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py index 5c2dc70..f80be1e 100644 --- a/plantcv/annotate/classes.py +++ b/plantcv/annotate/classes.py @@ -17,12 +17,12 @@ def __init__(self, img, figsize=(12, 6)): """Initialization :param img: image data :param figsize: desired figure size, (12,6) by default - :attribute points: list of points as (x,y) coordinates tuples + :attribute coords: list of points as (x,y) coordinates tuples """ self.fig, self.ax = plt.subplots(1, 1, figsize=figsize) self.ax.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) - self.points = [] + self.coords = [] self.events = [] self.fig.canvas.mpl_connect('button_press_event', self.onclick) @@ -33,11 +33,11 @@ def onclick(self, event): if event.button == 1: self.ax.plot(event.xdata, event.ydata, 'x', c='red') - self.points.append((floor(event.xdata), floor(event.ydata))) + self.coords.append((floor(event.xdata), floor(event.ydata))) else: - idx_remove, _ = _find_closest_pt((event.xdata, event.ydata), self.points) + idx_remove, _ = _find_closest_pt((event.xdata, event.ydata), self.coords) # remove the closest point to the user right clicked one - self.points.pop(idx_remove) + self.coords.pop(idx_remove) self.ax.lines[idx_remove].remove() self.fig.canvas.draw() From c2fb9990f33d178683d54991bfaa3c948a690d59 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 09:24:10 -0600 Subject: [PATCH 10/96] attribute name in tests update attribute name in test too --- plantcv/tests/plantcv/annotate/test_points.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plantcv/tests/plantcv/annotate/test_points.py b/plantcv/tests/plantcv/annotate/test_points.py index d510f29..78e4e6e 100644 --- a/plantcv/tests/plantcv/annotate/test_points.py +++ b/plantcv/tests/plantcv/annotate/test_points.py @@ -43,4 +43,4 @@ def test_points(annotate_test_data): e5.xdata, e5.ydata = (301, 200) drawer_rgb.onclick(e5) - assert drawer_rgb.points[0] == point1 + assert drawer_rgb.coords[0] == point1 From 915ff0d660a78c8e94b218cf0ebbda46481812c9 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 09:36:20 -0600 Subject: [PATCH 11/96] move test file into correct subdir --- .../annotate/test_points.py => tests/test_annotate_points.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename plantcv/tests/plantcv/annotate/test_points.py => tests/test_annotate_points.py (94%) diff --git a/plantcv/tests/plantcv/annotate/test_points.py b/tests/test_annotate_points.py similarity index 94% rename from plantcv/tests/plantcv/annotate/test_points.py rename to tests/test_annotate_points.py index 78e4e6e..25d794a 100644 --- a/plantcv/tests/plantcv/annotate/test_points.py +++ b/tests/test_annotate_points.py @@ -3,10 +3,10 @@ from plantcv.annotate.classes import Points -def test_points(annotate_test_data): +def test_points(test_data): """Test for PlantCV.""" # Read in a test grayscale image - img = cv2.imread(annotate_test_data.small_rgb_img) + img = cv2.imread(test_data.small_rgb_img) # initialize interactive tool drawer_rgb = Points(img, figsize=(12, 6)) From 530f65ea779ab00f25ff4e7e39ab60b97ed9d33b Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 09:36:35 -0600 Subject: [PATCH 12/96] Update conftest.py add small rgb image for annotation test --- tests/conftest.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 16b3a25..d00a4e5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,23 @@ +import os +import pytest import matplotlib # Disable plotting matplotlib.use("Template") + + +class TestData: + def __init__(self): + """Initialize simple variables.""" + # Test data directory + self.datadir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "testdata") + # Flat image directory + self.snapshot_dir = os.path.join(self.datadir, "snapshot_dir") + # RGB image + self.small_rgb_img = os.path.join(self.datadir, "setaria_small_plant_rgb.png") + + +@pytest.fixture(scope="session") +def test_data(): + """Test data object for the main PlantCV package.""" + return TestData() \ No newline at end of file From 2eb428963dcb0c082255800f92ae194ce0a9c1f2 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 09:37:16 -0600 Subject: [PATCH 13/96] move testdata into tests/ --- .../testdata/setaria_small_plant_rgb.png | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename {plantcv/tests => tests}/testdata/setaria_small_plant_rgb.png (100%) diff --git a/plantcv/tests/testdata/setaria_small_plant_rgb.png b/tests/testdata/setaria_small_plant_rgb.png similarity index 100% rename from plantcv/tests/testdata/setaria_small_plant_rgb.png rename to tests/testdata/setaria_small_plant_rgb.png From fe2f12fafd719f5846da3699f24632ef88eef8f3 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 09:39:03 -0600 Subject: [PATCH 14/96] delete extra test structure --- plantcv/tests/__init__.py | 0 plantcv/tests/conftest.py | 0 plantcv/tests/plantcv/annotate/__init__.py | 0 plantcv/tests/plantcv/annotate/conftest.py | 21 --------------------- 4 files changed, 21 deletions(-) delete mode 100644 plantcv/tests/__init__.py delete mode 100644 plantcv/tests/conftest.py delete mode 100644 plantcv/tests/plantcv/annotate/__init__.py delete mode 100644 plantcv/tests/plantcv/annotate/conftest.py diff --git a/plantcv/tests/__init__.py b/plantcv/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/plantcv/tests/conftest.py b/plantcv/tests/conftest.py deleted file mode 100644 index e69de29..0000000 diff --git a/plantcv/tests/plantcv/annotate/__init__.py b/plantcv/tests/plantcv/annotate/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/plantcv/tests/plantcv/annotate/conftest.py b/plantcv/tests/plantcv/annotate/conftest.py deleted file mode 100644 index 7263063..0000000 --- a/plantcv/tests/plantcv/annotate/conftest.py +++ /dev/null @@ -1,21 +0,0 @@ -import pytest -import os -import matplotlib - -# Disable plotting -matplotlib.use("Template") - - -class AnnotateTestData: - def __init__(self): - """Initialize simple variables.""" - # Test data directory - self.datadir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "testdata") - # RGB image - self.small_rgb_img = os.path.join(self.datadir, "setaria_small_plant_rgb.png") - - -@pytest.fixture(scope="session") -def annotate_test_data(): - """Test data object for the PlantCV annotate submodule.""" - return AnnotateTestData() From 8c08aceaf108623a93aae91debcb99324cc2f47d Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 11:10:37 -0600 Subject: [PATCH 15/96] populate init --- plantcv/annotate/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plantcv/annotate/__init__.py b/plantcv/annotate/__init__.py index e69de29..2c0c221 100644 --- a/plantcv/annotate/__init__.py +++ b/plantcv/annotate/__init__.py @@ -0,0 +1,5 @@ +from plantcv.annotate.classes import Points + +__all__ = [ + "Points" + ] From 0c95c4ba5b64884288ab544d79b09ef6520d1977 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 14:35:12 -0600 Subject: [PATCH 16/96] add viewing method, update onclick and add print_coords --- plantcv/annotate/classes.py | 93 ++++++++++++++++++++++++++++++++----- 1 file changed, 81 insertions(+), 12 deletions(-) diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py index f80be1e..79ae835 100644 --- a/plantcv/annotate/classes.py +++ b/plantcv/annotate/classes.py @@ -2,42 +2,111 @@ # 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 +def _view(self, label="default", color="c", view_all=False): + """ + View the label for a specific class label + Inputs: + label = (optional) class label, by default label="total" + color = desired color, by default color="c" + view_all = indicator of whether view all classes, by default view_all=False + :param label: string + :param color: string + :param view_all: boolean + :return: + """ + 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!") + if label is not None: + self.label = label + self.color = color + self.view_all = view_all + + if label not in self.coords: + self.coords[self.label] = [] + self.count[self.label] = 0 + self.colors[self.label] = 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 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=color) + 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)): + def __init__(self, img, figsize=(12, 6), label="default"): """Initialization :param img: image data :param figsize: desired figure size, (12,6) by default :attribute coords: list of points as (x,y) coordinates tuples """ - self.fig, self.ax = plt.subplots(1, 1, figsize=figsize) - self.ax.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) - - self.coords = [] - self.events = [] + #self.fig, self.ax = plt.subplots(1, 1, figsize=figsize) + #self.ax.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) + + self.img = img + 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.label = label # current label + self.sample_labels = [] # list of all sample labels, one to one with points collected + self.view_all = None # a flag indicating whether or not view all labels + self.color = None # current color + self.colors = {} # all used colors + self.figsize = figsize - self.fig.canvas.mpl_connect('button_press_event', self.onclick) + _view(self, label=label, color="r", view_all=True) def onclick(self, event): """Handle mouse click events.""" self.events.append(event) if event.button == 1: - self.ax.plot(event.xdata, event.ydata, 'x', c='red') - self.coords.append((floor(event.xdata), floor(event.ydata))) - + 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) + 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.pop(idx_remove) + 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. + Input variables: + filename = Name of the file to save collected coordinate + :param filename: str + """ + # 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) From 3ead6b683f5636ece4d3d05e2de0348d8fb8df70 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 14:35:43 -0600 Subject: [PATCH 17/96] add test for save_coords --- tests/test_annotate_points.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/tests/test_annotate_points.py b/tests/test_annotate_points.py index 25d794a..fc966bd 100644 --- a/tests/test_annotate_points.py +++ b/tests/test_annotate_points.py @@ -1,3 +1,5 @@ +"""Tests for annotate.Points.""" +import os import cv2 import matplotlib from plantcv.annotate.classes import Points @@ -43,4 +45,32 @@ def test_points(test_data): e5.xdata, e5.ydata = (301, 200) drawer_rgb.onclick(e5) - assert drawer_rgb.coords[0] == point1 + assert drawer_rgb.coords["default"][0] == point1 + +def test_points_save_coords(test_data, tmpdir): + """Test for PlantCV.""" + cache_dir = tmpdir.mkdir("cache") + filename = os.path.join(cache_dir, 'plantcv_print_coords.txt') + # Read in a test grayscale image + img = cv2.imread(test_data.small_rgb_img) + + # initialize interactive tool + drawer_rgb = Points(img, figsize=(12, 6)) + + # simulate mouse clicks + # event 1, left click to add point + e1 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=1) + point1 = (200, 200) + e1.xdata, e1.ydata = point1 + drawer_rgb.onclick(e1) + + # event 2, left click to add point + e2 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=1) + e2.xdata, e2.ydata = (300, 200) + drawer_rgb.onclick(e2) + + # Save collected coords out + drawer_rgb.print_coords(filename=filename) + assert os.path.exists(filename) \ No newline at end of file From c16ffe491d1afbfb9c854b192e8c01c65104a932 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 15:21:57 -0600 Subject: [PATCH 18/96] delete whitespace, other deepsource, and add import_list method --- plantcv/annotate/classes.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py index 79ae835..ac550f7 100644 --- a/plantcv/annotate/classes.py +++ b/plantcv/annotate/classes.py @@ -53,6 +53,7 @@ def _view(self, label="default", color="c", view_all=False): for (x, y) in self.coords[self.label]: self.ax.plot(x, y, marker='x', c=color) + 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 @@ -63,20 +64,19 @@ def __init__(self, img, figsize=(12, 6), label="default"): """Initialization :param img: image data :param figsize: desired figure size, (12,6) by default + :param label: current label for group of annotations, similar to pcv.params.sample_label :attribute coords: list of points as (x,y) coordinates tuples """ - #self.fig, self.ax = plt.subplots(1, 1, figsize=figsize) - #self.ax.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) - + self.img = img - self.coords = {} # dictionary of all coordinates per group label - self.events = [] # includes right and left click events + 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.label = label # current label - self.sample_labels = [] # list of all sample labels, one to one with points collected + self.sample_labels = [] # list of all sample labels, one to one with points collected self.view_all = None # a flag indicating whether or not view all labels self.color = None # current color - self.colors = {} # all used colors + self.colors = {} # all used colors self.figsize = figsize _view(self, label=label, color="r", view_all=True) @@ -110,3 +110,20 @@ def print_coords(self, filename): 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 center coordinates of already detected objects + Inputs: + coords = list of center coordinates of already detected objects. + label = class label for imported coordinates, by default label="default". + :param coords: list + :param label: string + """ + 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]) + + else: + warn(f"{label} already included and counted, nothing is imported!") From 2037760b4b87c095cba9ebe1b807487c60da6733 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 15:22:08 -0600 Subject: [PATCH 19/96] add test for importing lists --- tests/test_annotate_points.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/test_annotate_points.py b/tests/test_annotate_points.py index fc966bd..4bba424 100644 --- a/tests/test_annotate_points.py +++ b/tests/test_annotate_points.py @@ -47,11 +47,11 @@ def test_points(test_data): assert drawer_rgb.coords["default"][0] == point1 -def test_points_save_coords(test_data, tmpdir): +def test_points_print_coords(test_data, tmpdir): """Test for PlantCV.""" cache_dir = tmpdir.mkdir("cache") filename = os.path.join(cache_dir, 'plantcv_print_coords.txt') - # Read in a test grayscale image + # Read in a test image img = cv2.imread(test_data.small_rgb_img) # initialize interactive tool @@ -73,4 +73,18 @@ def test_points_save_coords(test_data, tmpdir): # Save collected coords out drawer_rgb.print_coords(filename=filename) - assert os.path.exists(filename) \ No newline at end of file + assert os.path.exists(filename) + +def test_points_import_list(test_data): + """Test for PlantCV.""" + # Read in a test image + img = cv2.imread(test_data.small_rgb_img) + # initialize interactive tool + drawer_rgb = Points(img, figsize=(12, 6), label="default") + totalpoints1 = [(158, 531), (361, 112), (500, 418), (269.25303806488864, 385.69839981447126), + (231.21964288863632, 445.995245825603), (293.37177646934134, 448.778177179963), (240.49608073650273, 277.1640769944342), + (279.4571196975417, 240.05832560296852), (77.23077461405376, 165.84682282003712), (420, 364), + (509.5127783246289, 353.2308673469388), (527.1380102355752, 275.3087894248609), (445.50535717435065, 138.94515306122452)] + drawer_rgb.import_list(coords=totalpoints1, label="imported") + + assert len(drawer_rgb.coords["imported"]) == 13 From 230b6df204156293a3913bc3567cfba7e1ea9e25 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 15:44:40 -0600 Subject: [PATCH 20/96] add import_file method and docstring returns --- plantcv/annotate/classes.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py index ac550f7..4d59eae 100644 --- a/plantcv/annotate/classes.py +++ b/plantcv/annotate/classes.py @@ -105,6 +105,7 @@ def print_coords(self, filename): Input variables: filename = Name of the file to save collected coordinate :param filename: str + :return: """ # Open the file for writing with open(filename, "w") as fp: @@ -118,6 +119,7 @@ def import_list(self, coords, label="default"): label = class label for imported coordinates, by default label="default". :param coords: list :param label: string + :return: """ if label not in self.coords: self.coords[label] = [] @@ -127,3 +129,21 @@ def import_list(self, coords, label="default"): else: warn(f"{label} already included and counted, nothing is imported!") + + def import_file(self, filename): + """Method to import coordinates from file to Points object + + Inputs: + filename = filename of stored coordinates and classes + :param filename: str + :return: + """ + coord_file = open(filename, "r") + coords = json.load(coord_file) + + 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) From 1ba7f4d369967d485b8aedd60e63cd12e9c74bb1 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 15:44:49 -0600 Subject: [PATCH 21/96] add test data file --- tests/conftest.py | 3 +- tests/testdata/points_file_import.coords | 306 +++++++++++++++++++++++ 2 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 tests/testdata/points_file_import.coords diff --git a/tests/conftest.py b/tests/conftest.py index d00a4e5..a3d09df 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,8 @@ def __init__(self): self.snapshot_dir = os.path.join(self.datadir, "snapshot_dir") # RGB image self.small_rgb_img = os.path.join(self.datadir, "setaria_small_plant_rgb.png") - + # Text file with tuple coordinates (by group label) + self.pollen_coords = os.path.join(self.datadir, "points_file_import.coords") @pytest.fixture(scope="session") def test_data(): diff --git a/tests/testdata/points_file_import.coords b/tests/testdata/points_file_import.coords new file mode 100644 index 0000000..2be1117 --- /dev/null +++ b/tests/testdata/points_file_import.coords @@ -0,0 +1,306 @@ +{ + "total": [ + [ + 1524, + 59 + ], + [ + 1642, + 78 + ], + [ + 723, + 159 + ], + [ + 1711, + 218 + ], + [ + 736, + 263 + ], + [ + 1431, + 275 + ], + [ + 1573, + 338 + ], + [ + 531, + 358 + ], + [ + 1437, + 365 + ], + [ + 904, + 380 + ], + [ + 1255, + 387 + ], + [ + 1140, + 417 + ], + [ + 112, + 561 + ], + [ + 2056, + 690 + ], + [ + 418, + 700 + ], + [ + 1738, + 703 + ], + [ + 1255, + 959 + ], + [ + 1791, + 1042 + ], + [ + 1310, + 1065 + ], + [ + 902, + 1084 + ], + [ + 1319, + 1131 + ], + [ + 1084, + 1156 + ], + [ + 457, + 1183 + ], + [ + 1763, + 1190 + ], + [ + 1407, + 1232 + ], + [ + 2053, + 1283 + ], + [ + 1140, + 1383 + ], + [ + 566, + 1393 + ], + [ + 240, + 1403 + ], + [ + 1681, + 1421 + ], + [ + 398, + 470 + ], + [ + 438, + 432 + ], + [ + 446, + 502 + ], + [ + 919, + 252 + ], + [ + 947, + 204 + ], + [ + 997, + 219 + ], + [ + 967, + 267 + ], + [ + 1034, + 400 + ], + [ + 1074, + 364 + ], + [ + 1104, + 329 + ], + [ + 1165, + 267 + ], + [ + 1235, + 304 + ], + [ + 1272, + 262 + ], + [ + 1297, + 299 + ], + [ + 1350, + 249 + ], + [ + 1387, + 229 + ], + [ + 1488, + 492 + ], + [ + 1565, + 202 + ], + [ + 1595, + 247 + ], + [ + 1633, + 274 + ], + [ + 839, + 101 + ], + [ + 861, + 162 + ], + [ + 914, + 46 + ], + [ + 1052, + 56 + ], + [ + 1102, + 149 + ], + [ + 1150, + 127 + ], + [ + 280, + 442 + ], + [ + 240, + 477 + ], + [ + 93, + 490 + ], + [ + 270, + 733 + ], + [ + 351, + 713 + ], + [ + 363, + 627 + ], + [ + 215, + 1036 + ], + [ + 498, + 973 + ], + [ + 606, + 1224 + ], + [ + 646, + 1264 + ], + [ + 353, + 1244 + ], + [ + 343, + 1284 + ], + [ + 300, + 1244 + ], + [ + 323, + 1196 + ] + ], + "germinated": [ + [ + 1485, + 494 + ], + [ + 95, + 481 + ], + [ + 1588, + 974 + ], + [ + 1994, + 1039 + ], + [ + 1901, + 198 + ] + ] +} \ No newline at end of file From 3c7c60987c1d360e9e7233452f2b5751656d022e Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 4 Mar 2024 15:45:13 -0600 Subject: [PATCH 22/96] add test for importing from file --- tests/test_annotate_points.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_annotate_points.py b/tests/test_annotate_points.py index 4bba424..3aea50f 100644 --- a/tests/test_annotate_points.py +++ b/tests/test_annotate_points.py @@ -88,3 +88,12 @@ def test_points_import_list(test_data): drawer_rgb.import_list(coords=totalpoints1, label="imported") assert len(drawer_rgb.coords["imported"]) == 13 + +def test_points_import_file(test_data): + """Test for PlantCV.""" + img = cv2.imread(test_data.small_rgb_img) + counter = Points(img, figsize=(8, 6)) + file = test_data.pollen_coords + counter.import_file(file) + + assert counter.count['total'] == 70 From da2443c7d2b452b7d612643e96798f9ee0a2f062 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Tue, 5 Mar 2024 07:39:06 -0600 Subject: [PATCH 23/96] add test for warning triggered in import_file --- tests/test_annotate_points.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_annotate_points.py b/tests/test_annotate_points.py index 3aea50f..e4fe0b9 100644 --- a/tests/test_annotate_points.py +++ b/tests/test_annotate_points.py @@ -97,3 +97,13 @@ def test_points_import_file(test_data): counter.import_file(file) assert counter.count['total'] == 70 + +def test_points_import_file_duplicate(test_data): + """Test for PlantCV.""" + img = cv2.imread(test_data.small_rgb_img) + # set label to "total" so that group labels dduplicated to trigger warning + counter = Points(img, figsize=(8, 6), label="total") + file = test_data.pollen_coords + counter.import_file(file) + + assert counter.count['total'] == 0 From d2606c9f49e3059ba8641d1f2b31840cf8dc6b9f Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Tue, 5 Mar 2024 08:08:55 -0600 Subject: [PATCH 24/96] actually add method .view --- plantcv/annotate/classes.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py index 4d59eae..4fed8b5 100644 --- a/plantcv/annotate/classes.py +++ b/plantcv/annotate/classes.py @@ -147,3 +147,17 @@ def import_file(self, filename): 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="c", view_all=False): + """Method to view current annotations + + Inputs: + label = (optional) class label, by default label="total" + color = desired color, by default color="c" + view_all = indicator of whether view all classes, by default view_all=False + :param label: string + :param color: string + :param view_all: boolean + :return: + """ + _view(self, label=label, color=color, view_all=view_all) From 9b2689fcd06f745440c6a2930c5d6903db79a094 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Tue, 5 Mar 2024 08:09:04 -0600 Subject: [PATCH 25/96] add tests for untested code --- tests/test_annotate_points.py | 50 +++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/tests/test_annotate_points.py b/tests/test_annotate_points.py index e4fe0b9..e7f64cb 100644 --- a/tests/test_annotate_points.py +++ b/tests/test_annotate_points.py @@ -6,7 +6,7 @@ def test_points(test_data): - """Test for PlantCV.""" + """Test for plantcv-annotate.""" # Read in a test grayscale image img = cv2.imread(test_data.small_rgb_img) @@ -48,7 +48,7 @@ def test_points(test_data): assert drawer_rgb.coords["default"][0] == point1 def test_points_print_coords(test_data, tmpdir): - """Test for PlantCV.""" + """Test for plantcv-annotate.""" cache_dir = tmpdir.mkdir("cache") filename = os.path.join(cache_dir, 'plantcv_print_coords.txt') # Read in a test image @@ -76,7 +76,7 @@ def test_points_print_coords(test_data, tmpdir): assert os.path.exists(filename) def test_points_import_list(test_data): - """Test for PlantCV.""" + """Test for plantcv-annotate.""" # Read in a test image img = cv2.imread(test_data.small_rgb_img) # initialize interactive tool @@ -90,7 +90,7 @@ def test_points_import_list(test_data): assert len(drawer_rgb.coords["imported"]) == 13 def test_points_import_file(test_data): - """Test for PlantCV.""" + """Test for plantcv-annotate.""" img = cv2.imread(test_data.small_rgb_img) counter = Points(img, figsize=(8, 6)) file = test_data.pollen_coords @@ -98,12 +98,40 @@ def test_points_import_file(test_data): assert counter.count['total'] == 70 -def test_points_import_file_duplicate(test_data): - """Test for PlantCV.""" +def test_points_view(test_data): + """Test for plantcv-annotate.""" + # Read in a test grayscale image img = cv2.imread(test_data.small_rgb_img) - # set label to "total" so that group labels dduplicated to trigger warning - counter = Points(img, figsize=(8, 6), label="total") - file = test_data.pollen_coords - counter.import_file(file) - assert counter.count['total'] == 0 + # initialize interactive tool + drawer_rgb = Points(img, figsize=(12, 6)) + + # simulate mouse clicks + # event 1, left click to add point + e1 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=1) + point1 = (200, 200) + e1.xdata, e1.ydata = point1 + drawer_rgb.onclick(e1) + drawer_rgb.view(view_all=True) + drawer_rgb.view(view_all=False) + + assert str(drawer_rgb.fig) == "Figure(1200x600)" + +def test_points_view_warn(test_data): + """Test for plantcv-annotate.""" + # Read in a test grayscale image + img = cv2.imread(test_data.small_rgb_img) + + # initialize interactive tool, implied default label and "r" color + drawer_rgb = Points(img, figsize=(12, 6)) + + # simulate mouse clicks, event 1=left click to add point + e1 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=1) + point1 = (200, 200) + e1.xdata, e1.ydata = point1 + drawer_rgb.onclick(e1) + drawer_rgb.view(label="new", color='r') + + assert str(drawer_rgb.fig) == "Figure(1200x600)" From fa7aeb822fccf55c1ed9a2fbfa2e90dd50d33439 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Tue, 5 Mar 2024 09:23:06 -0600 Subject: [PATCH 26/96] plot updated annotations after importing file and list --- plantcv/annotate/classes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py index 4fed8b5..ac5e3e2 100644 --- a/plantcv/annotate/classes.py +++ b/plantcv/annotate/classes.py @@ -126,7 +126,7 @@ def import_list(self, coords, label="default"): for (y, x) in coords: self.coords[label].append((x, y)) self.count[label] = len(self.coords[label]) - + _view(self, label=label, color=self.color, view_all=False) else: warn(f"{label} already included and counted, nothing is imported!") From 664c3dd5b927c4de6d06091f70731015889a6b39 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Tue, 5 Mar 2024 12:08:10 -0600 Subject: [PATCH 27/96] add tests for full coverage --- tests/test_annotate_points.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/test_annotate_points.py b/tests/test_annotate_points.py index e7f64cb..60f9a80 100644 --- a/tests/test_annotate_points.py +++ b/tests/test_annotate_points.py @@ -89,6 +89,17 @@ def test_points_import_list(test_data): assert len(drawer_rgb.coords["imported"]) == 13 +def test_points_import_list_warn(test_data): + """Test for plantcv-annotate.""" + # Read in a test image + img = cv2.imread(test_data.small_rgb_img) + # initialize interactive tool + drawer_rgb = Points(img, figsize=(12, 6), label="default") + totalpoints1 = [(158, 531), (361, 112), (500, 418), (445.50535717435065, 138.94515306122452)] + drawer_rgb.import_list(coords=totalpoints1) + + assert len(drawer_rgb.coords["default"]) == 0 + def test_points_import_file(test_data): """Test for plantcv-annotate.""" img = cv2.imread(test_data.small_rgb_img) @@ -113,7 +124,11 @@ def test_points_view(test_data): point1 = (200, 200) e1.xdata, e1.ydata = point1 drawer_rgb.onclick(e1) - drawer_rgb.view(view_all=True) + drawer_rgb.view(label="new", view_all=True) + e2 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=1) + e2.xdata, e2.ydata = (300, 200) + drawer_rgb.onclick(e2) drawer_rgb.view(view_all=False) assert str(drawer_rgb.fig) == "Figure(1200x600)" From cd48f8422880130d7ee594e742171ec0be8ea242 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Tue, 5 Mar 2024 13:06:52 -0600 Subject: [PATCH 28/96] deepsource issues --- plantcv/annotate/classes.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py index ac5e3e2..77dec0d 100644 --- a/plantcv/annotate/classes.py +++ b/plantcv/annotate/classes.py @@ -64,10 +64,9 @@ def __init__(self, img, figsize=(12, 6), label="default"): """Initialization :param img: image data :param figsize: desired figure size, (12,6) by default - :param label: current label for group of annotations, similar to pcv.params.sample_label + :param label: current label for group of annotations, similar to pcv.params.sample_label :attribute coords: list of points as (x,y) coordinates tuples """ - self.img = img self.coords = {} # dictionary of all coordinates per group label self.events = [] # includes right and left click events @@ -132,11 +131,11 @@ def import_list(self, coords, label="default"): def import_file(self, filename): """Method to import coordinates from file to Points object - + Inputs: filename = filename of stored coordinates and classes :param filename: str - :return: + :return: """ coord_file = open(filename, "r") coords = json.load(coord_file) @@ -150,7 +149,7 @@ def import_file(self, filename): def view(self, label="default", color="c", view_all=False): """Method to view current annotations - + Inputs: label = (optional) class label, by default label="total" color = desired color, by default color="c" @@ -159,5 +158,5 @@ def view(self, label="default", color="c", view_all=False): :param color: string :param view_all: boolean :return: - """ + """ _view(self, label=label, color=color, view_all=view_all) From 6540e27365ea0d15da8a8d98c21ce62d5a79017b Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Tue, 5 Mar 2024 13:42:09 -0600 Subject: [PATCH 29/96] deepsource issue about reading and writing files --- plantcv/annotate/classes.py | 10 +++++----- tests/test_annotate_points.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py index 77dec0d..3d8f960 100644 --- a/plantcv/annotate/classes.py +++ b/plantcv/annotate/classes.py @@ -99,15 +99,15 @@ def onclick(self, event): self.sample_labels.pop(idx_remove) self.fig.canvas.draw() - def print_coords(self, filename): + def print_coords(self, outfile): """Save collected coordinates to a file. Input variables: - filename = Name of the file to save collected coordinate + outfile = Name of the file to save collected coordinate :param filename: str :return: """ # Open the file for writing - with open(filename, "w") as fp: + with open(outfile, "w") as fp: # Save the data in JSON format with indentation json.dump(obj=self.coords, fp=fp, indent=4) @@ -137,8 +137,8 @@ def import_file(self, filename): :param filename: str :return: """ - coord_file = open(filename, "r") - coords = json.load(coord_file) + with open(filename, "r") as fp: + coords = json.load(fp) keys = list(coords.keys()) diff --git a/tests/test_annotate_points.py b/tests/test_annotate_points.py index 60f9a80..1373f11 100644 --- a/tests/test_annotate_points.py +++ b/tests/test_annotate_points.py @@ -72,7 +72,7 @@ def test_points_print_coords(test_data, tmpdir): drawer_rgb.onclick(e2) # Save collected coords out - drawer_rgb.print_coords(filename=filename) + drawer_rgb.print_coords(outfile=filename) assert os.path.exists(filename) def test_points_import_list(test_data): From 2cabecdcbf36e9e033dc8a445d709df11ec1da1b Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Tue, 5 Mar 2024 14:20:07 -0600 Subject: [PATCH 30/96] whitespace --- plantcv/annotate/classes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py index 3d8f960..62945f9 100644 --- a/plantcv/annotate/classes.py +++ b/plantcv/annotate/classes.py @@ -149,7 +149,7 @@ def import_file(self, filename): def view(self, label="default", color="c", view_all=False): """Method to view current annotations - + Inputs: label = (optional) class label, by default label="total" color = desired color, by default color="c" From c771c265642930bc6208b94e8164281730e3f1e1 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Wed, 6 Mar 2024 14:09:46 -0600 Subject: [PATCH 31/96] Create get_centroids.py --- plantcv/annotate/get_centroids.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 plantcv/annotate/get_centroids.py diff --git a/plantcv/annotate/get_centroids.py b/plantcv/annotate/get_centroids.py new file mode 100644 index 0000000..70b1fd7 --- /dev/null +++ b/plantcv/annotate/get_centroids.py @@ -0,0 +1,29 @@ +# Get centroids. region props method (WILL Merge into PCV.annotate, opening PR this week) + +from skimage.measure import label, regionprops + +def get_centroids(bin_img): + """Get the coordinates (row,column) of the centroid of each connected region in a binary image. + + Inputs: + bin_img = Binary image containing the connected regions to consider + + Returns: + coords = List of coordinates (row,column) of the centroids of the regions + + :param bin_img: numpy.ndarray + :return coords: list + """ + # find contours in the binary image + labeled_img = label(bin_img) + # measure regions + obj_measures = regionprops(labeled_img) + + coords = [] + for obj in obj_measures: + # Convert coord values to int + coord = tuple(map(int, obj.centroid)) + coords.append(coord) + + + return coords From ccf8e0eda59b763edbd88e0f33c9d712151dd4c8 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Wed, 6 Mar 2024 14:16:35 -0600 Subject: [PATCH 32/96] add bin image to tests --- tests/conftest.py | 2 ++ tests/testdata/setaria_small_plant_mask.png | Bin 0 -> 940 bytes 2 files changed, 2 insertions(+) create mode 100644 tests/testdata/setaria_small_plant_mask.png diff --git a/tests/conftest.py b/tests/conftest.py index a3d09df..1b83096 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,8 @@ def __init__(self): self.snapshot_dir = os.path.join(self.datadir, "snapshot_dir") # RGB image self.small_rgb_img = os.path.join(self.datadir, "setaria_small_plant_rgb.png") + # Binary image + self.small_bin_img = os.path.join(self.datadir, "setaria_small_plant_mask.png") # Text file with tuple coordinates (by group label) self.pollen_coords = os.path.join(self.datadir, "points_file_import.coords") diff --git a/tests/testdata/setaria_small_plant_mask.png b/tests/testdata/setaria_small_plant_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..2c990da73bd78bdb552898ea2b8cf603f9a87325 GIT binary patch literal 940 zcmeAS@N?(olHy`uVBq!ia0y~yV4MKN{v1F72Dd%OUokK+7kj!mhEy=VJ=hq?Y`}2D zp!)wpXKtR4CnG2MOdN%nA+TI_XOGR>6RA&9-Sd?7t&5~9Z#d>D^II3mR$g~pr~KaP zMCp@A_9s@K)->O7k1KU}r@Y=u;rqmJ*$KZ-Ean5U++~4c{ZShy8av<`w@?4WIN~X5SO9PxB5>kMX=$ z{9}^#$!fm%BGt+Z51rSk@LScKnEJ$#eS-DLZoU(9pSZF=@%yxhxk$b8BIC}=~fgtq_y literal 0 HcmV?d00001 From 4c6d5ae40b138c6f56d706c05f1b39916a93efb3 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Wed, 6 Mar 2024 14:17:42 -0600 Subject: [PATCH 33/96] Create test_annotate_get_centroids.py --- tests/test_annotate_get_centroids.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/test_annotate_get_centroids.py diff --git a/tests/test_annotate_get_centroids.py b/tests/test_annotate_get_centroids.py new file mode 100644 index 0000000..54c4db3 --- /dev/null +++ b/tests/test_annotate_get_centroids.py @@ -0,0 +1,12 @@ +import cv2 +from plantcv.annotate.get_centroids import get_centroids + + +def test_get_centroids(test_data): + """Test for PlantCV.""" + # Read in test data + mask = cv2.imread(test_data.small_bin_img, -1) + + coor = get_centroids(bin_img=mask) + + assert coor == [(166, 214)] From b276a17d071d5be72785372d26fe4e7b6f18e110 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Wed, 6 Mar 2024 14:30:57 -0600 Subject: [PATCH 34/96] change input variable name back to "filename" --- plantcv/annotate/classes.py | 6 +++--- tests/test_annotate_points.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py index 62945f9..1a66e78 100644 --- a/plantcv/annotate/classes.py +++ b/plantcv/annotate/classes.py @@ -99,15 +99,15 @@ def onclick(self, event): self.sample_labels.pop(idx_remove) self.fig.canvas.draw() - def print_coords(self, outfile): + def print_coords(self, filename): """Save collected coordinates to a file. Input variables: - outfile = Name of the file to save collected coordinate + filename = Name of the file to save collected coordinate :param filename: str :return: """ # Open the file for writing - with open(outfile, "w") as fp: + with open(filename, "w") as fp: # Save the data in JSON format with indentation json.dump(obj=self.coords, fp=fp, indent=4) diff --git a/tests/test_annotate_points.py b/tests/test_annotate_points.py index 1373f11..f3b91e1 100644 --- a/tests/test_annotate_points.py +++ b/tests/test_annotate_points.py @@ -72,7 +72,7 @@ def test_points_print_coords(test_data, tmpdir): drawer_rgb.onclick(e2) # Save collected coords out - drawer_rgb.print_coords(outfile=filename) + drawer_rgb.print_coords(filename) assert os.path.exists(filename) def test_points_import_list(test_data): From bd1a4c9794b3272c90419bdab7f9b374d0dce8e5 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Thu, 7 Mar 2024 09:39:07 -0600 Subject: [PATCH 35/96] deepsource issues --- plantcv/annotate/get_centroids.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plantcv/annotate/get_centroids.py b/plantcv/annotate/get_centroids.py index 70b1fd7..8fbf46b 100644 --- a/plantcv/annotate/get_centroids.py +++ b/plantcv/annotate/get_centroids.py @@ -1,7 +1,8 @@ -# Get centroids. region props method (WILL Merge into PCV.annotate, opening PR this week) +# Get centroids from mask objects from skimage.measure import label, regionprops + def get_centroids(bin_img): """Get the coordinates (row,column) of the centroid of each connected region in a binary image. @@ -18,12 +19,10 @@ def get_centroids(bin_img): labeled_img = label(bin_img) # measure regions obj_measures = regionprops(labeled_img) - coords = [] for obj in obj_measures: # Convert coord values to int coord = tuple(map(int, obj.centroid)) coords.append(coord) - return coords From dc3d385be5cb876bb4ab2fecb8300318a43c44f2 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Mon, 11 Mar 2024 12:55:17 -0500 Subject: [PATCH 36/96] Update __init__.py add get_centroids --- plantcv/annotate/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plantcv/annotate/__init__.py b/plantcv/annotate/__init__.py index 2c0c221..f8c85ee 100644 --- a/plantcv/annotate/__init__.py +++ b/plantcv/annotate/__init__.py @@ -1,5 +1,7 @@ from plantcv.annotate.classes import Points +from plantcv.annotate.get_centroids import get_centroids __all__ = [ - "Points" + "Points", + get_centroids ] From dfee8aa76e75880aa89ee4adf838eab4c4eea046 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Fri, 15 Mar 2024 16:00:14 -0500 Subject: [PATCH 37/96] Update __init__.py --- plantcv/annotate/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plantcv/annotate/__init__.py b/plantcv/annotate/__init__.py index f8c85ee..1f70211 100644 --- a/plantcv/annotate/__init__.py +++ b/plantcv/annotate/__init__.py @@ -3,5 +3,5 @@ __all__ = [ "Points", - get_centroids + "get_centroids" ] From adfb0d24e882de191a13a92a9848133f1e64f97e Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Fri, 29 Mar 2024 11:25:27 -0500 Subject: [PATCH 38/96] add doc page for get_centroid --- docs/get_centroids.md | 44 +++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 45 insertions(+) create mode 100644 docs/get_centroids.md diff --git a/docs/get_centroids.md b/docs/get_centroids.md new file mode 100644 index 0000000..87dbb08 --- /dev/null +++ b/docs/get_centroids.md @@ -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) diff --git a/mkdocs.yml b/mkdocs.yml index 7d71deb..649a459 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,6 +18,7 @@ nav: - 'PlantCV Namespace': - 'Annotation Tools': - Points: Points.md + - Get Centroids: get_centroids.md markdown_extensions: - toc: permalink: True From 95ab0e8e412cd1efcbd0aeea979f2b41793a4c14 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Fri, 29 Mar 2024 11:25:30 -0500 Subject: [PATCH 39/96] Create discs_mask.png --- .../get_centroids/discs_mask.png | Bin 0 -> 2726 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/img/documentation_images/get_centroids/discs_mask.png diff --git a/docs/img/documentation_images/get_centroids/discs_mask.png b/docs/img/documentation_images/get_centroids/discs_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..357fc236abc29c6c60fea4e9bd0bd619c6925514 GIT binary patch literal 2726 zcmV;X3R(4uP)phZf|fcERI1iGEteT@e`srU3S;Z2omQtPGa_23tyUN< zl~S~ER4NvYs92+d5s8Ed=Kk#;kn}`FPR?cbJ=y0o!!YNPJ?}ohJ^SwN*?recB(O$U zPle!)>`Hd+IYT)(<5GZ)%E1}01N>PzIODqj`%@K!E7G^Yp*YqfWM${~QmnC=cs;k{W`A70&{$V5a$L_|bHL_|cq)9YkqaBLZc zUVmc$ebf(4vXuIG_DQMFH$Cg|%v}ilG^xsQb0f8rJo>D&fTiBf-pS3dGMVae0>D9! zK5Q>A$Kx4X$*oEC1-T!9B5!(-fe+OE!t*(tmRudkB>?pIv!m~!jh%200yq2k`700A z+^i~uo3(V7kDoppV3?}ljFG^0#o&+&YYq-j3Qj4$K^eFuO%aHQbWO(vJ*VH&_HNK- zzVMe<F_Yd5us_$yx&BT7HycU-;7n~mb*T?YoZ2J*r7LSk3Q}Kh zW@eu#YiGL78ueUy7{trklW5fKp)5fKsj8Z&g0BbWRHxJN&|I-{TlV2Xn9&^Ipr zqYKQ;&QVIP6yy{$Gs{y-agy01#&Zy}p9k7{E6Ipm{7nPLUH{>pCu6*?Wd6guAG{#a zHy4!^1iT!f+kw8vdbJoJ=CK7cBYxw@4b2VL#PU&>|1sHntBJ1x3y(Q}0Gm7;4)tLG zN}`{@DqwAL7vwa6E06W+B!CAzYs`^rZJ&wy?@L+$PIM{XtjG5*?^-XszP0sG$JabG zedUx+=a8&wYV`0*%OeGee&mo6_b#6P5AamiI+4;bDII5&-vg}gRM5d*E^oB}EQo#r zn}GLSs>cL?15y8ZzeeEWEZ>?xA7Do;n<&e+-|5yCHeLRP9No8@m}6!`8i0e@{x!dR z!{+7PYb$nIHUEy{#O{*a?5;Tam0#=cfxzj1U7XXcN)pX*Jn0A8Z*#u%sn7hqj87Ax-LeJL-lwm@jH2v4#frPifJk=pg ziO&akfZLAsnm1SBhTPhLuk!cfr+SC4X-kDPQ?|UUT*F4jAZ;Ty9x(nKE$6Eme;z1c%H5R_kufo^53v z*A-^Aqd~=RMpiYo`HCS@mn=#6!O@J~J(Y8W(msTip+BWzP}&Up0=-~#2~ z-#6q-o~|x?=A#`7&KViHY+Xb|L_|bHL_|bHL_~aZ)tmoR2o5<V^ zK0MO|GqZFJ^z4iYhk*TODF{~-esHMyL#?qK63Q>lR0<*@Nx!Gwqi3i^k+>tLrgfJF z2X*Kh(wH5}x960JGkfa0+IO7A5qqiAsi7{(R5?UML_|bHL_|a+LY|-7J8t};I>qjc z^wq$9DGI_B4!dhfz)^M(J_pcz+*4&iw|3|rPW|lrjcj@uX)ol=VYwp}; zU~SR&6^vh=1_-+|_rM=NvLHvX_~%iePA5~lqpTUUf^td5>3u!iMEUmb?^Wau>A#0p zl6)4@x>Z5;T{UccTqZPV;=J1H8nuKw<3^x52z%bAFI8&CKjX0}nw zWxVtt=39*aZ0G$+w)d3xqnXaOZ4^Lxuol7rwJ^<6PAM-9b$%f|H%+HHuE1Rx_O~mKc*RilQJ*8{1kRQ+Dx|(K|OSb6I6ne>-w?yno!t z8d@0edK6dDy3<~CcGrKm> z1$ocR_7smAk5$x>`P9tJ&WO*%`mGtJ zL}RoNfY;+Pt+BvZMH-`h0Jp|vQfq(;B^sl?f2Z>D_)Myz8y#Tnd7@v3t>d^qoA#&I zmUNSPOsH#MSvQ!EZgaxuT%dJe{1*CA=V@IWlX9Rwen-VOLcQ*78k1!(E&3J3nk{#R zKfVay4+=FV`O)vnW*-LL3|oC)3-GWq^-Bd`8s}gP0R5b5WbiIw4m-pB(%4_EhSQ3@%B{(wJQmr)5bIM6w+{tzYH=#o&&dhE}h(+H_u4 zWM7e#eI8xUq0TQ7hc8d9I;>{fW;WEpS( Date: Mon, 15 Apr 2024 13:34:36 -0500 Subject: [PATCH 40/96] Fix minor typos --- docs/napari_join_labels.md | 4 ++-- docs/napari_label_classes.md | 2 +- plantcv/annotate/napari_join_labels.py | 4 ++-- plantcv/annotate/napari_label_classes.py | 3 ++- plantcv/annotate/napari_open.py | 4 ++-- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/napari_join_labels.md b/docs/napari_join_labels.md index 1a118ca..b8cd44e 100644 --- a/docs/napari_join_labels.md +++ b/docs/napari_join_labels.md @@ -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) diff --git a/docs/napari_label_classes.md b/docs/napari_label_classes.md index b97735f..6b112af 100644 --- a/docs/napari_label_classes.md +++ b/docs/napari_label_classes.md @@ -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*) diff --git a/plantcv/annotate/napari_join_labels.py b/plantcv/annotate/napari_join_labels.py index c01dc38..1848258 100755 --- a/plantcv/annotate/napari_join_labels.py +++ b/plantcv/annotate/napari_join_labels.py @@ -20,7 +20,7 @@ def napari_join_labels(img, viewer): labeled with values (e.g. labeled mask, output of kmeans clustering). viewer = Napari Viewer with classes labeled (e.g viewer from napari_label_classes) - show = if show is True the viewer is launched. This opetion is useful for + show = if show is True the viewer is launched. This option is useful for running tests without triggering the viewer. Returns: @@ -29,7 +29,7 @@ def napari_join_labels(img, viewer): :param img: numpy.ndarray :param viewer: Napari Viewer object - :return labeled_imd: numpy.ndarray + :return labeled_img: numpy.ndarray :return mask_dict: dict of numpy.ndarray """ diff --git a/plantcv/annotate/napari_label_classes.py b/plantcv/annotate/napari_label_classes.py index 918032d..81e37dd 100755 --- a/plantcv/annotate/napari_label_classes.py +++ b/plantcv/annotate/napari_label_classes.py @@ -19,13 +19,14 @@ def napari_label_classes(img, classes, show=True): is run. If all classes have points labeled, any clusters not labeled will default to the last class in the list when napari_join_labels is run. - show = if show is True the viewer is launched. This opetion is useful for + show = if show is True the viewer is launched. This option is useful for running tests without triggering the viewer. Returns: viewer = Napari viewer object :param img: numpy.ndarray + :param classes: list :return viewer: napari viewer object """ diff --git a/plantcv/annotate/napari_open.py b/plantcv/annotate/napari_open.py index ab75436..59fa317 100755 --- a/plantcv/annotate/napari_open.py +++ b/plantcv/annotate/napari_open.py @@ -8,12 +8,12 @@ def napari_open(img, show=True): """ - open img in napari and label classes + open img in napari Inputs: img = img (grayimg, rgbimg, or hyperspectral image array data e.g. hyperspectraldata.array_data) - show = if show is True the viewer is launched. This opetion is useful for + show = if show is True the viewer is launched. This option is useful for running tests without triggering the viewer. Returns: From 52ce0fdd7065b83d614efd94c0a6c774319e2ed9 Mon Sep 17 00:00:00 2001 From: Noah Fahlgren Date: Tue, 16 Apr 2024 11:58:33 -0500 Subject: [PATCH 41/96] Bump python versions --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 1e6bc6d..e0904dc 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -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 }} From 102ff91ad4106e74289b9da209542e43c9f63898 Mon Sep 17 00:00:00 2001 From: Noah Fahlgren Date: Tue, 16 Apr 2024 11:58:45 -0500 Subject: [PATCH 42/96] Set up automatic versioning --- plantcv/annotate/__init__.py | 3 +++ pyproject.toml | 10 +++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/plantcv/annotate/__init__.py b/plantcv/annotate/__init__.py index e69de29..9013604 100644 --- a/plantcv/annotate/__init__.py +++ b/plantcv/annotate/__init__.py @@ -0,0 +1,3 @@ +from importlib.metadata import version +# Auto versioning +__version__ = version("annotate") diff --git a/pyproject.toml b/pyproject.toml index f6f6afe..f0e393e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools >= 61.0"] +requires = ["setuptools >= 64.0", "setuptools_scm>=8"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] @@ -26,7 +26,15 @@ classifiers = [ "Intended Audience :: Science/Research", ] +[project.optional-dependencies] +test = [ + "pytest", + "pytest-cov", +] + [project.urls] Homepage = "https://plantcv.org" Documentation = "https://plantcv.readthedocs.io" Repository = "https://github.com/danforthcenter/plantcv-annotate" + +[tool.setuptools_scm] From da46c43a702efbe088309fbf51435122d541d66a Mon Sep 17 00:00:00 2001 From: Noah Fahlgren Date: Tue, 16 Apr 2024 13:56:53 -0500 Subject: [PATCH 43/96] Fix package name --- plantcv/annotate/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plantcv/annotate/__init__.py b/plantcv/annotate/__init__.py index 9013604..219e92a 100644 --- a/plantcv/annotate/__init__.py +++ b/plantcv/annotate/__init__.py @@ -1,3 +1,3 @@ from importlib.metadata import version # Auto versioning -__version__ = version("annotate") +__version__ = version("plantcv-annotate") From fad8ed42a49dbccbf73ab039e01b29e23d8e2caa Mon Sep 17 00:00:00 2001 From: Noah Fahlgren Date: Thu, 18 Apr 2024 10:48:48 -0500 Subject: [PATCH 44/96] Reformat docstrings to numpy format --- plantcv/annotate/classes.py | 100 ++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 43 deletions(-) diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py index 1a66e78..503c20a 100644 --- a/plantcv/annotate/classes.py +++ b/plantcv/annotate/classes.py @@ -10,16 +10,16 @@ def _view(self, label="default", color="c", view_all=False): - """ - View the label for a specific class label - Inputs: - label = (optional) class label, by default label="total" - color = desired color, by default color="c" - view_all = indicator of whether view all classes, by default view_all=False - :param label: string - :param color: string - :param view_all: boolean - :return: + """View the label for a specific class label. + + Parameters + ---------- + label : str, optional + class label, by default "default" + color : str, optional + marker color, by default "c" + 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, " @@ -57,15 +57,20 @@ def _view(self, label="default", color="c", view_all=False): 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 + right click removes the closest collected point. """ def __init__(self, img, figsize=(12, 6), label="default"): - """Initialization - :param img: image data - :param figsize: desired figure size, (12,6) by default - :param label: current label for group of annotations, similar to pcv.params.sample_label - :attribute coords: list of points as (x,y) coordinates tuples + """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.coords = {} # dictionary of all coordinates per group label @@ -81,7 +86,14 @@ def __init__(self, img, figsize=(12, 6), label="default"): _view(self, label=label, color="r", view_all=True) def onclick(self, event): - """Handle mouse click events.""" + """Handle mouse click events + + Parameters + ---------- + event : matplotlib.backend_bases.MouseEvent + matplotlib MouseEvent object + """ + print(type(event)) self.events.append(event) if event.button == 1: @@ -101,10 +113,11 @@ def onclick(self, event): def print_coords(self, filename): """Save collected coordinates to a file. - Input variables: - filename = Name of the file to save collected coordinate - :param filename: str - :return: + + Parameters + ---------- + filename : str + output filename """ # Open the file for writing with open(filename, "w") as fp: @@ -112,13 +125,14 @@ def print_coords(self, filename): json.dump(obj=self.coords, fp=fp, indent=4) def import_list(self, coords, label="default"): - """Import center coordinates of already detected objects - Inputs: - coords = list of center coordinates of already detected objects. - label = class label for imported coordinates, by default label="default". - :param coords: list - :param label: string - :return: + """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] = [] @@ -130,12 +144,12 @@ def import_list(self, coords, label="default"): warn(f"{label} already included and counted, nothing is imported!") def import_file(self, filename): - """Method to import coordinates from file to Points object + """Import coordinates from a file. - Inputs: - filename = filename of stored coordinates and classes - :param filename: str - :return: + Parameters + ---------- + filename : str + JSON file containing Points annotations """ with open(filename, "r") as fp: coords = json.load(fp) @@ -148,15 +162,15 @@ def import_file(self, filename): self.import_list(keycoor, label=key) def view(self, label="default", color="c", view_all=False): - """Method to view current annotations - - Inputs: - label = (optional) class label, by default label="total" - color = desired color, by default color="c" - view_all = indicator of whether view all classes, by default view_all=False - :param label: string - :param color: string - :param view_all: boolean - :return: + """View current annotations. + + Parameters + ---------- + label : str, optional + class label, by default "default" + color : str, optional + marker color, by default "c" + view_all : bool, optional + view all classes or a single class, by default False """ _view(self, label=label, color=color, view_all=view_all) From f500605e138c70c04f1bbe52c0f9b6bea5400593 Mon Sep 17 00:00:00 2001 From: Noah Fahlgren Date: Thu, 18 Apr 2024 11:09:35 -0500 Subject: [PATCH 45/96] Simplify code and remove private funciton --- plantcv/annotate/classes.py | 98 ++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 57 deletions(-) diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py index 503c20a..455701e 100644 --- a/plantcv/annotate/classes.py +++ b/plantcv/annotate/classes.py @@ -9,58 +9,13 @@ from plantcv.plantcv import warn -def _view(self, label="default", color="c", view_all=False): - """View the label for a specific class label. - - Parameters - ---------- - label : str, optional - class label, by default "default" - color : str, optional - marker color, by default "c" - 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!") - if label is not None: - self.label = label - self.color = color - self.view_all = view_all - - if label not in self.coords: - self.coords[self.label] = [] - self.count[self.label] = 0 - self.colors[self.label] = 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 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=color) - - 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"): + def __init__(self, img, figsize=(12, 6), label="default", color="r", view_all=False): """Points initialization method. Parameters @@ -73,17 +28,17 @@ def __init__(self, img, figsize=(12, 6), label="default"): 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.label = label # current label self.sample_labels = [] # list of all sample labels, one to one with points collected - self.view_all = None # a flag indicating whether or not view all labels - self.color = None # current color self.colors = {} # all used colors - self.figsize = figsize - _view(self, label=label, color="r", view_all=True) + self.view(label=self.label, color=self.color, view_all=self.view_all) def onclick(self, event): """Handle mouse click events @@ -96,7 +51,7 @@ def onclick(self, event): 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 @@ -139,7 +94,7 @@ class label, by default "default" for (y, x) in coords: self.coords[label].append((x, y)) self.count[label] = len(self.coords[label]) - _view(self, label=label, color=self.color, view_all=False) + self.view(label=label, color=self.color, view_all=False) else: warn(f"{label} already included and counted, nothing is imported!") @@ -161,16 +116,45 @@ def import_file(self, filename): keycoor = list(map(lambda sub: (sub[1], sub[0]), keycoor)) self.import_list(keycoor, label=key) - def view(self, label="default", color="c", view_all=False): - """View current annotations. + 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 "c" + marker color, by default "r" view_all : bool, optional view all classes or a single class, by default False """ - _view(self, label=label, color=color, view_all=view_all) + 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) From 8ff04cc6c9a20062f15a9e17483b339226866569 Mon Sep 17 00:00:00 2001 From: Noah Fahlgren Date: Thu, 18 Apr 2024 11:21:31 -0500 Subject: [PATCH 46/96] Fix x and y assignments --- plantcv/annotate/classes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py index 455701e..5134742 100644 --- a/plantcv/annotate/classes.py +++ b/plantcv/annotate/classes.py @@ -91,7 +91,7 @@ class label, by default "default" """ if label not in self.coords: self.coords[label] = [] - for (y, x) in coords: + for (x, y) 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) From 3dab75c0ea9b965aca44a6ad10c16a616d942584 Mon Sep 17 00:00:00 2001 From: Noah Fahlgren Date: Mon, 6 May 2024 11:14:58 -0500 Subject: [PATCH 47/96] Omit temp files created during opencv install --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..6e6d5a1 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +omit = + config.py + config-3.py From 6db3130590495720ed3ad6ddeec5dec1b1c27cf0 Mon Sep 17 00:00:00 2001 From: Noah Fahlgren Date: Mon, 6 May 2024 11:23:29 -0500 Subject: [PATCH 48/96] Add missing section header --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 6e6d5a1..4c3586e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,4 @@ +[run] omit = config.py config-3.py From 3460b84c423db3285be9ddc79eb3f3432823cc51 Mon Sep 17 00:00:00 2001 From: Noah Fahlgren Date: Mon, 6 May 2024 16:27:51 -0500 Subject: [PATCH 49/96] Restore license --- LICENSE | 373 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. From 73ce25580bdcc055890a22d6c2b3587346d18f2e Mon Sep 17 00:00:00 2001 From: Noah Fahlgren Date: Mon, 6 May 2024 16:36:08 -0500 Subject: [PATCH 50/96] Minor formatting changes --- tests/test_annotate_points.py | 21 ++++++++++++++------- tests/test_napari_join_labels.py | 1 + 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/test_annotate_points.py b/tests/test_annotate_points.py index f3b91e1..c74769b 100644 --- a/tests/test_annotate_points.py +++ b/tests/test_annotate_points.py @@ -1,5 +1,5 @@ """Tests for annotate.Points.""" -import os +import os import cv2 import matplotlib from plantcv.annotate.classes import Points @@ -47,6 +47,7 @@ def test_points(test_data): assert drawer_rgb.coords["default"][0] == point1 + def test_points_print_coords(test_data, tmpdir): """Test for plantcv-annotate.""" cache_dir = tmpdir.mkdir("cache") @@ -71,10 +72,11 @@ def test_points_print_coords(test_data, tmpdir): e2.xdata, e2.ydata = (300, 200) drawer_rgb.onclick(e2) - # Save collected coords out + # Save collected coords out drawer_rgb.print_coords(filename) assert os.path.exists(filename) + def test_points_import_list(test_data): """Test for plantcv-annotate.""" # Read in a test image @@ -82,13 +84,15 @@ def test_points_import_list(test_data): # initialize interactive tool drawer_rgb = Points(img, figsize=(12, 6), label="default") totalpoints1 = [(158, 531), (361, 112), (500, 418), (269.25303806488864, 385.69839981447126), - (231.21964288863632, 445.995245825603), (293.37177646934134, 448.778177179963), (240.49608073650273, 277.1640769944342), - (279.4571196975417, 240.05832560296852), (77.23077461405376, 165.84682282003712), (420, 364), - (509.5127783246289, 353.2308673469388), (527.1380102355752, 275.3087894248609), (445.50535717435065, 138.94515306122452)] + (231.21964288863632, 445.995245825603), (293.37177646934134, 448.778177179963), + (240.49608073650273, 277.1640769944342), (279.4571196975417, 240.05832560296852), + (77.23077461405376, 165.84682282003712), (420, 364), (509.5127783246289, 353.2308673469388), + (527.1380102355752, 275.3087894248609), (445.50535717435065, 138.94515306122452)] drawer_rgb.import_list(coords=totalpoints1, label="imported") assert len(drawer_rgb.coords["imported"]) == 13 + def test_points_import_list_warn(test_data): """Test for plantcv-annotate.""" # Read in a test image @@ -100,15 +104,17 @@ def test_points_import_list_warn(test_data): assert len(drawer_rgb.coords["default"]) == 0 + def test_points_import_file(test_data): """Test for plantcv-annotate.""" img = cv2.imread(test_data.small_rgb_img) counter = Points(img, figsize=(8, 6)) - file = test_data.pollen_coords + file = test_data.pollen_coords counter.import_file(file) assert counter.count['total'] == 70 + def test_points_view(test_data): """Test for plantcv-annotate.""" # Read in a test grayscale image @@ -133,12 +139,13 @@ def test_points_view(test_data): assert str(drawer_rgb.fig) == "Figure(1200x600)" + def test_points_view_warn(test_data): """Test for plantcv-annotate.""" # Read in a test grayscale image img = cv2.imread(test_data.small_rgb_img) - # initialize interactive tool, implied default label and "r" color + # initialize interactive tool, implied default label and "r" color drawer_rgb = Points(img, figsize=(12, 6)) # simulate mouse clicks, event 1=left click to add point diff --git a/tests/test_napari_join_labels.py b/tests/test_napari_join_labels.py index 408f82e..269465d 100644 --- a/tests/test_napari_join_labels.py +++ b/tests/test_napari_join_labels.py @@ -5,6 +5,7 @@ from plantcv.annotate.napari_join_labels import napari_join_labels from plantcv.plantcv import params + def test_napari_join_labels(test_data): """Test for PlantCV.Annotate""" # Read in test data From 29a2d9cfe9907210454d54c09c53cd62156ac9b5 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Tue, 7 May 2024 10:23:07 -0500 Subject: [PATCH 51/96] import_list bugfix coordinate order --- plantcv/annotate/classes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plantcv/annotate/classes.py b/plantcv/annotate/classes.py index 5134742..455701e 100644 --- a/plantcv/annotate/classes.py +++ b/plantcv/annotate/classes.py @@ -91,7 +91,7 @@ class label, by default "default" """ if label not in self.coords: self.coords[label] = [] - for (x, y) in coords: + 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) From 20bec6b9c9e7a1022384268bfb6d80db5159c11c Mon Sep 17 00:00:00 2001 From: Noah Fahlgren Date: Wed, 8 May 2024 17:01:41 -0500 Subject: [PATCH 52/96] Try making figure sizes smaller --- tests/test_annotate_points.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_annotate_points.py b/tests/test_annotate_points.py index c74769b..d7828b3 100644 --- a/tests/test_annotate_points.py +++ b/tests/test_annotate_points.py @@ -11,7 +11,7 @@ def test_points(test_data): img = cv2.imread(test_data.small_rgb_img) # initialize interactive tool - drawer_rgb = Points(img, figsize=(12, 6)) + drawer_rgb = Points(img, figsize=(5, 5)) # simulate mouse clicks # event 1, left click to add point @@ -56,7 +56,7 @@ def test_points_print_coords(test_data, tmpdir): img = cv2.imread(test_data.small_rgb_img) # initialize interactive tool - drawer_rgb = Points(img, figsize=(12, 6)) + drawer_rgb = Points(img, figsize=(5, 5)) # simulate mouse clicks # event 1, left click to add point @@ -82,7 +82,7 @@ def test_points_import_list(test_data): # Read in a test image img = cv2.imread(test_data.small_rgb_img) # initialize interactive tool - drawer_rgb = Points(img, figsize=(12, 6), label="default") + drawer_rgb = Points(img, figsize=(5, 5), label="default") totalpoints1 = [(158, 531), (361, 112), (500, 418), (269.25303806488864, 385.69839981447126), (231.21964288863632, 445.995245825603), (293.37177646934134, 448.778177179963), (240.49608073650273, 277.1640769944342), (279.4571196975417, 240.05832560296852), @@ -98,7 +98,7 @@ def test_points_import_list_warn(test_data): # Read in a test image img = cv2.imread(test_data.small_rgb_img) # initialize interactive tool - drawer_rgb = Points(img, figsize=(12, 6), label="default") + drawer_rgb = Points(img, figsize=(5, 5), label="default") totalpoints1 = [(158, 531), (361, 112), (500, 418), (445.50535717435065, 138.94515306122452)] drawer_rgb.import_list(coords=totalpoints1) @@ -108,7 +108,7 @@ def test_points_import_list_warn(test_data): def test_points_import_file(test_data): """Test for plantcv-annotate.""" img = cv2.imread(test_data.small_rgb_img) - counter = Points(img, figsize=(8, 6)) + counter = Points(img, figsize=(5, 5)) file = test_data.pollen_coords counter.import_file(file) @@ -121,7 +121,7 @@ def test_points_view(test_data): img = cv2.imread(test_data.small_rgb_img) # initialize interactive tool - drawer_rgb = Points(img, figsize=(12, 6)) + drawer_rgb = Points(img, figsize=(5, 5)) # simulate mouse clicks # event 1, left click to add point @@ -137,7 +137,7 @@ def test_points_view(test_data): drawer_rgb.onclick(e2) drawer_rgb.view(view_all=False) - assert str(drawer_rgb.fig) == "Figure(1200x600)" + assert str(drawer_rgb.fig) == "Figure(500x500)" def test_points_view_warn(test_data): @@ -146,7 +146,7 @@ def test_points_view_warn(test_data): img = cv2.imread(test_data.small_rgb_img) # initialize interactive tool, implied default label and "r" color - drawer_rgb = Points(img, figsize=(12, 6)) + drawer_rgb = Points(img, figsize=(5, 5)) # simulate mouse clicks, event 1=left click to add point e1 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, @@ -156,4 +156,4 @@ def test_points_view_warn(test_data): drawer_rgb.onclick(e1) drawer_rgb.view(label="new", color='r') - assert str(drawer_rgb.fig) == "Figure(1200x600)" + assert str(drawer_rgb.fig) == "Figure(500x500)" From 1bff4557b5df2311d0ac4a35da2d8a74c89b9085 Mon Sep 17 00:00:00 2001 From: Noah Fahlgren Date: Fri, 10 May 2024 11:20:17 -0500 Subject: [PATCH 53/96] Increase setup delay --- .github/workflows/continuous-integration.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index f64c7e5..ef86ea4 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -50,6 +50,7 @@ jobs: - name: Tests uses: aganders3/headless-gui@v2 with: + linux-setup-delay: 2000 run: pytest --cov-report=xml --cov=./ - name: Upload coverage to Deepsource uses: deepsourcelabs/test-coverage-action@master From 6e027527f87de977c2e107b847568ae444d93988 Mon Sep 17 00:00:00 2001 From: Noah Fahlgren Date: Fri, 10 May 2024 11:24:05 -0500 Subject: [PATCH 54/96] Increase delay to 10s --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index ef86ea4..e54e9f0 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -50,7 +50,7 @@ jobs: - name: Tests uses: aganders3/headless-gui@v2 with: - linux-setup-delay: 2000 + linux-setup-delay: 10000 run: pytest --cov-report=xml --cov=./ - name: Upload coverage to Deepsource uses: deepsourcelabs/test-coverage-action@master From 8788098012e28510d65301c5094fd76dceee91a0 Mon Sep 17 00:00:00 2001 From: Noah Fahlgren Date: Fri, 10 May 2024 11:30:35 -0500 Subject: [PATCH 55/96] Change screen resolution --- .github/workflows/continuous-integration.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index e54e9f0..206ba25 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -51,6 +51,7 @@ jobs: uses: aganders3/headless-gui@v2 with: linux-setup-delay: 10000 + xvfb-screen-size: 1280x720x24 run: pytest --cov-report=xml --cov=./ - name: Upload coverage to Deepsource uses: deepsourcelabs/test-coverage-action@master From f05f741e231e9d74f91f56e98b61241638469e2b Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Tue, 14 May 2024 19:07:40 -0500 Subject: [PATCH 56/96] Update continuous-integration.yml remove xvfb --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 206ba25..b18a740 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -31,7 +31,7 @@ jobs: - 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 From df608d0449d59dffb4355145dc1761ecce310e38 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Tue, 14 May 2024 20:01:07 -0500 Subject: [PATCH 57/96] Update continuous-integration.yml https://pytest-qt.readthedocs.io/en/latest/troubleshooting.html#xvfb-assertionerror-timeouterror-when-using-waituntil-waitexposed-and-ui-events --- .github/workflows/continuous-integration.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b18a740..7048801 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -34,6 +34,7 @@ jobs: 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 + /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names @@ -47,6 +48,7 @@ jobs: pip install . pip uninstall -y opencv-python pip install opencv-python-headless + /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX - name: Tests uses: aganders3/headless-gui@v2 with: From d162ec9c06781c18ff6fa8b16b197a0ee8aa1c9e Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Tue, 14 May 2024 20:04:48 -0500 Subject: [PATCH 58/96] Update continuous-integration.yml --- .github/workflows/continuous-integration.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 7048801..b18a740 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -34,7 +34,6 @@ jobs: 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 - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names @@ -48,7 +47,6 @@ jobs: pip install . pip uninstall -y opencv-python pip install opencv-python-headless - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX - name: Tests uses: aganders3/headless-gui@v2 with: From 6340f0d9c96781d846e42c377211ba41a9524850 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Tue, 14 May 2024 20:29:20 -0500 Subject: [PATCH 59/96] Update continuous-integration.yml --- .github/workflows/continuous-integration.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b18a740..2874b9a 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -30,7 +30,6 @@ 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 python -m pip install --upgrade pip pip install flake8 pytest pytest-cov pytest-qt pytest-xvfb ipython anyio From 74c778ccf5b598f76cb33c2b0dbba55ed8658801 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Tue, 14 May 2024 21:10:10 -0500 Subject: [PATCH 60/96] Update continuous-integration.yml --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 2874b9a..c2b77ca 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -47,7 +47,7 @@ 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: linux-setup-delay: 10000 xvfb-screen-size: 1280x720x24 From d62b2130019c34011735ced9c27a71fa3b34ed92 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 09:43:30 -0500 Subject: [PATCH 61/96] Update continuous-integration.yml saw something about specifically using python 3.10.12 --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index c2b77ca..97c7096 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.9', '3.10', '3.11'] + python-version: ['3.9', '3.10.12', '3.11'] os: [ubuntu-latest] env: OS: ${{ matrix.os }} From d5afb93e3f451c5383c28aa5c261aa1cd7c9d1ae Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 09:54:52 -0500 Subject: [PATCH 62/96] Update continuous-integration.yml seeing if the problem is consistently with 3.10 --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 97c7096..9f1c81f 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.9', '3.10.12', '3.11'] + python-version: ['3.9', '3.11'] os: [ubuntu-latest] env: OS: ${{ matrix.os }} From 93640aa2e6331c1c99b9fd62e3cd1d8a61e03add Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 09:58:25 -0500 Subject: [PATCH 63/96] Update continuous-integration.yml 3.10 isn't the problem. getting same error on 3.11. --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 9f1c81f..288d098 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.9', '3.11'] + python-version: ['3.9','3.10' '3.11'] os: [ubuntu-latest] env: OS: ${{ matrix.os }} From 4dc2d7835022cfa4b39ab1dd8b64212ff94b1697 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 10:56:14 -0500 Subject: [PATCH 64/96] Update continuous-integration.yml --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 288d098..42ef749 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -51,7 +51,7 @@ jobs: with: linux-setup-delay: 10000 xvfb-screen-size: 1280x720x24 - run: pytest --cov-report=xml --cov=./ + run: pytest -vv --cov-report=xml --cov=./ - name: Upload coverage to Deepsource uses: deepsourcelabs/test-coverage-action@master with: From 0cf7668ed004f56348f7a437316da0da6a156664 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 11:02:43 -0500 Subject: [PATCH 65/96] Update continuous-integration.yml omg I broke github actions.... --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 42ef749..1f23f09 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -51,7 +51,7 @@ jobs: with: linux-setup-delay: 10000 xvfb-screen-size: 1280x720x24 - run: pytest -vv --cov-report=xml --cov=./ + run: pytest -v --cov-report=xml --cov=./ - name: Upload coverage to Deepsource uses: deepsourcelabs/test-coverage-action@master with: From f9909129e5c639168b9172bea0232ae68e365c35 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 11:06:32 -0500 Subject: [PATCH 66/96] Update continuous-integration.yml missing comma --- .github/workflows/continuous-integration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 1f23f09..ed57f0e 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.9','3.10' '3.11'] + python-version: ['3.9','3.10', '3.11'] os: [ubuntu-latest] env: OS: ${{ matrix.os }} @@ -51,7 +51,7 @@ jobs: with: linux-setup-delay: 10000 xvfb-screen-size: 1280x720x24 - run: pytest -v --cov-report=xml --cov=./ + run: pytest -vv --cov-report=xml --cov=./ - name: Upload coverage to Deepsource uses: deepsourcelabs/test-coverage-action@master with: From 3afe91b9079725da9271ccde98e9b9d1d37247d6 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 11:15:10 -0500 Subject: [PATCH 67/96] test testing to see if there is a specific test breaking github actions. --- tests/test_annotate_get_centroids.py | 12 -- tests/test_annotate_points.py | 159 --------------------------- tests/test_napari_classes.py | 18 --- tests/test_napari_join_labels.py | 79 ------------- tests/test_napari_label_classes.py | 15 --- tests/test_napari_open.py | 40 ------- 6 files changed, 323 deletions(-) delete mode 100644 tests/test_annotate_get_centroids.py delete mode 100644 tests/test_annotate_points.py delete mode 100644 tests/test_napari_classes.py delete mode 100644 tests/test_napari_join_labels.py delete mode 100644 tests/test_napari_label_classes.py delete mode 100644 tests/test_napari_open.py diff --git a/tests/test_annotate_get_centroids.py b/tests/test_annotate_get_centroids.py deleted file mode 100644 index 54c4db3..0000000 --- a/tests/test_annotate_get_centroids.py +++ /dev/null @@ -1,12 +0,0 @@ -import cv2 -from plantcv.annotate.get_centroids import get_centroids - - -def test_get_centroids(test_data): - """Test for PlantCV.""" - # Read in test data - mask = cv2.imread(test_data.small_bin_img, -1) - - coor = get_centroids(bin_img=mask) - - assert coor == [(166, 214)] diff --git a/tests/test_annotate_points.py b/tests/test_annotate_points.py deleted file mode 100644 index d7828b3..0000000 --- a/tests/test_annotate_points.py +++ /dev/null @@ -1,159 +0,0 @@ -"""Tests for annotate.Points.""" -import os -import cv2 -import matplotlib -from plantcv.annotate.classes import Points - - -def test_points(test_data): - """Test for plantcv-annotate.""" - # Read in a test grayscale image - img = cv2.imread(test_data.small_rgb_img) - - # initialize interactive tool - drawer_rgb = Points(img, figsize=(5, 5)) - - # simulate mouse clicks - # event 1, left click to add point - e1 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, - x=0, y=0, button=1) - point1 = (200, 200) - e1.xdata, e1.ydata = point1 - drawer_rgb.onclick(e1) - - # event 2, left click to add point - e2 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, - x=0, y=0, button=1) - e2.xdata, e2.ydata = (300, 200) - drawer_rgb.onclick(e2) - - # event 3, left click to add point - e3 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, - x=0, y=0, button=1) - e3.xdata, e3.ydata = (50, 50) - drawer_rgb.onclick(e3) - - # event 4, right click to remove point with exact coordinates - e4 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, - x=0, y=0, button=3) - e4.xdata, e4.ydata = (50, 50) - drawer_rgb.onclick(e4) - - # event 5, right click to remove point with coordinates close but not equal - e5 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, - x=0, y=0, button=3) - e5.xdata, e5.ydata = (301, 200) - drawer_rgb.onclick(e5) - - assert drawer_rgb.coords["default"][0] == point1 - - -def test_points_print_coords(test_data, tmpdir): - """Test for plantcv-annotate.""" - cache_dir = tmpdir.mkdir("cache") - filename = os.path.join(cache_dir, 'plantcv_print_coords.txt') - # Read in a test image - img = cv2.imread(test_data.small_rgb_img) - - # initialize interactive tool - drawer_rgb = Points(img, figsize=(5, 5)) - - # simulate mouse clicks - # event 1, left click to add point - e1 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, - x=0, y=0, button=1) - point1 = (200, 200) - e1.xdata, e1.ydata = point1 - drawer_rgb.onclick(e1) - - # event 2, left click to add point - e2 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, - x=0, y=0, button=1) - e2.xdata, e2.ydata = (300, 200) - drawer_rgb.onclick(e2) - - # Save collected coords out - drawer_rgb.print_coords(filename) - assert os.path.exists(filename) - - -def test_points_import_list(test_data): - """Test for plantcv-annotate.""" - # Read in a test image - img = cv2.imread(test_data.small_rgb_img) - # initialize interactive tool - drawer_rgb = Points(img, figsize=(5, 5), label="default") - totalpoints1 = [(158, 531), (361, 112), (500, 418), (269.25303806488864, 385.69839981447126), - (231.21964288863632, 445.995245825603), (293.37177646934134, 448.778177179963), - (240.49608073650273, 277.1640769944342), (279.4571196975417, 240.05832560296852), - (77.23077461405376, 165.84682282003712), (420, 364), (509.5127783246289, 353.2308673469388), - (527.1380102355752, 275.3087894248609), (445.50535717435065, 138.94515306122452)] - drawer_rgb.import_list(coords=totalpoints1, label="imported") - - assert len(drawer_rgb.coords["imported"]) == 13 - - -def test_points_import_list_warn(test_data): - """Test for plantcv-annotate.""" - # Read in a test image - img = cv2.imread(test_data.small_rgb_img) - # initialize interactive tool - drawer_rgb = Points(img, figsize=(5, 5), label="default") - totalpoints1 = [(158, 531), (361, 112), (500, 418), (445.50535717435065, 138.94515306122452)] - drawer_rgb.import_list(coords=totalpoints1) - - assert len(drawer_rgb.coords["default"]) == 0 - - -def test_points_import_file(test_data): - """Test for plantcv-annotate.""" - img = cv2.imread(test_data.small_rgb_img) - counter = Points(img, figsize=(5, 5)) - file = test_data.pollen_coords - counter.import_file(file) - - assert counter.count['total'] == 70 - - -def test_points_view(test_data): - """Test for plantcv-annotate.""" - # Read in a test grayscale image - img = cv2.imread(test_data.small_rgb_img) - - # initialize interactive tool - drawer_rgb = Points(img, figsize=(5, 5)) - - # simulate mouse clicks - # event 1, left click to add point - e1 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, - x=0, y=0, button=1) - point1 = (200, 200) - e1.xdata, e1.ydata = point1 - drawer_rgb.onclick(e1) - drawer_rgb.view(label="new", view_all=True) - e2 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, - x=0, y=0, button=1) - e2.xdata, e2.ydata = (300, 200) - drawer_rgb.onclick(e2) - drawer_rgb.view(view_all=False) - - assert str(drawer_rgb.fig) == "Figure(500x500)" - - -def test_points_view_warn(test_data): - """Test for plantcv-annotate.""" - # Read in a test grayscale image - img = cv2.imread(test_data.small_rgb_img) - - # initialize interactive tool, implied default label and "r" color - drawer_rgb = Points(img, figsize=(5, 5)) - - # simulate mouse clicks, event 1=left click to add point - e1 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, - x=0, y=0, button=1) - point1 = (200, 200) - e1.xdata, e1.ydata = point1 - drawer_rgb.onclick(e1) - drawer_rgb.view(label="new", color='r') - - assert str(drawer_rgb.fig) == "Figure(500x500)" diff --git a/tests/test_napari_classes.py b/tests/test_napari_classes.py deleted file mode 100644 index f4050a9..0000000 --- a/tests/test_napari_classes.py +++ /dev/null @@ -1,18 +0,0 @@ -import numpy as np -from plantcv.annotate import napari_classes - - -def test_napari_classes(make_napari_viewer): - """Test for PlantCV.Annotate""" - # Read in test data - viewer = make_napari_viewer(show=False) - img = np.zeros((100, 100)) - coor = [(25, 25), (50, 50)] - viewer.add_image(img) - viewer.add_points(np.array(coor), symbol="o", name="total", - face_color="red", size=30) - viewer.add_points(np.array(coor), symbol="o", name="test", - face_color="red", size=30) - keys = napari_classes(viewer) - - assert keys == ['total', 'test'] diff --git a/tests/test_napari_join_labels.py b/tests/test_napari_join_labels.py deleted file mode 100644 index 269465d..0000000 --- a/tests/test_napari_join_labels.py +++ /dev/null @@ -1,79 +0,0 @@ -import numpy as np -from plantcv.annotate.napari_open import napari_open -from plantcv.annotate.napari_label_classes import napari_label_classes -from plantcv.plantcv import readimage -from plantcv.annotate.napari_join_labels import napari_join_labels -from plantcv.plantcv import params - - -def test_napari_join_labels(test_data): - """Test for PlantCV.Annotate""" - # Read in test data - img, _, _ = readimage(test_data.kmeans_seed_gray_img) - viewer = napari_label_classes(img, ["seed"], show=False) - labeled, _ = napari_join_labels(img, viewer) - - assert np.shape(labeled) == (576, 537) - - -def test_napari_join_allclass(test_data): - """Test for PlantCV.Annotate""" - # Read in test data - img, _, _ = readimage(test_data.kmeans_seed_gray_img) - viewer = napari_open(img, show=False) - background = [(54, 143), (77, 246)] - viewer.add_points(np.array(background), symbol="o", name='background', - face_color="red", size=1) - wing = [(275, 54)] - viewer.add_points(np.array(wing), symbol="o", name='wing', - face_color="red", size=1) - seed = [(280, 218)] - viewer.add_points(np.array(seed), symbol="o", name='seed', - face_color="red", size=1) - - labeled, _ = napari_join_labels(img, viewer) - - assert np.shape(labeled) == (576, 537) - - -def test_napari_join_warn(test_data): - """Test for PlantCV.Annotate""" - # Read in test data - img, _, _ = readimage(test_data.kmeans_seed_gray_img) - viewer = napari_open(img, show=False) - background = [(54, 143), (77, 246)] - viewer.add_points(np.array(background), symbol="o", name='background', - face_color="red", size=1) - wing = [(275, 54)] - viewer.add_points(np.array(wing), symbol="o", name='wing', - face_color="red", size=1) - seed = [(275, 54)] - viewer.add_points(np.array(seed), symbol="o", name='seed', - face_color="red", size=1) - - labeled, _ = napari_join_labels(img, viewer) - - assert np.shape(labeled) == (576, 537) - - -def test_napari_join_print(test_data, tmpdir): - """Test for PlantCV.Annotate""" - params.debug = 'print' - cache_dir = tmpdir.mkdir("cache") - params.debug_outdir = cache_dir - # Read in test data - img, _, _ = readimage(test_data.kmeans_seed_gray_img) - viewer = napari_open(img, show=False) - background = [(54, 143), (77, 246)] - viewer.add_points(np.array(background), symbol="o", name='background', - face_color="red", size=1) - wing = [(280, 218)] - viewer.add_points(np.array(wing), symbol="o", name='wing', - face_color="red", size=1) - seed = [(275, 54)] - viewer.add_points(np.array(seed), symbol="o", name='seed', - face_color="red", size=1) - - labeled, _ = napari_join_labels(img, viewer) - - assert np.shape(labeled) == (576, 537) diff --git a/tests/test_napari_label_classes.py b/tests/test_napari_label_classes.py deleted file mode 100644 index ca49b8f..0000000 --- a/tests/test_napari_label_classes.py +++ /dev/null @@ -1,15 +0,0 @@ -import numpy as np -from plantcv.annotate import napari_label_classes -from plantcv.plantcv import readimage - - -def test_napari_label_classes_gray(test_data): - """Test for PlantCV.Annotate""" - # Read in test data - img, _, _ = readimage(test_data.kmeans_seed_gray_img) - viewer = napari_label_classes(img, ['seed'], show=False) - coor = [(25, 25)] - viewer.add_points(np.array(coor), symbol="o", name='background', - face_color="red", size=1) - - assert len(viewer.layers['background'].data) == 1 diff --git a/tests/test_napari_open.py b/tests/test_napari_open.py deleted file mode 100644 index cfb9d25..0000000 --- a/tests/test_napari_open.py +++ /dev/null @@ -1,40 +0,0 @@ -import numpy as np -from plantcv.annotate import napari_open -from plantcv.plantcv import readimage - - -def test_napari_open_rgb(test_data): - """Test for PlantCV.Annotate""" - # Read in test data - img, _, _ = readimage(test_data.small_rgb_img) - viewer = napari_open(img, show=False) - coor = [(25, 25), (50, 50)] - viewer.add_points(np.array(coor), symbol="o", name="total", - face_color="red", size=1) - - assert len(viewer.layers['total'].data) == 2 - - -def test_napari_open_gray(test_data): - """Test for PlantCV.Annotate""" - # Read in test data - img, _, _ = readimage(test_data.kmeans_seed_gray_img) - viewer = napari_open(img, show=False) - coor = [(25, 25), (50, 50)] - viewer.add_points(np.array(coor), symbol="o", name="total", - face_color="red", size=1) - - assert len(viewer.layers['total'].data) == 2 - - -def test_napari_open_envi(test_data): - """Test for PlantCV.Annotate""" - # Read in test data - img = readimage(test_data.envi_sample_data, mode='envi') - img = img.array_data - viewer = napari_open(img, show=False) - coor = [(25, 25), (50, 50)] - viewer.add_points(np.array(coor), symbol="o", name="total", - face_color="red", size=1) - - assert len(viewer.layers['total'].data) == 2 From cf8b3a431e3cb9611ebc5663c532f7418921f2ef Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 11:20:36 -0500 Subject: [PATCH 68/96] Create test_annotate_points.py add points tests --- tests/test_annotate_points.py | 159 ++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 tests/test_annotate_points.py diff --git a/tests/test_annotate_points.py b/tests/test_annotate_points.py new file mode 100644 index 0000000..d7828b3 --- /dev/null +++ b/tests/test_annotate_points.py @@ -0,0 +1,159 @@ +"""Tests for annotate.Points.""" +import os +import cv2 +import matplotlib +from plantcv.annotate.classes import Points + + +def test_points(test_data): + """Test for plantcv-annotate.""" + # Read in a test grayscale image + img = cv2.imread(test_data.small_rgb_img) + + # initialize interactive tool + drawer_rgb = Points(img, figsize=(5, 5)) + + # simulate mouse clicks + # event 1, left click to add point + e1 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=1) + point1 = (200, 200) + e1.xdata, e1.ydata = point1 + drawer_rgb.onclick(e1) + + # event 2, left click to add point + e2 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=1) + e2.xdata, e2.ydata = (300, 200) + drawer_rgb.onclick(e2) + + # event 3, left click to add point + e3 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=1) + e3.xdata, e3.ydata = (50, 50) + drawer_rgb.onclick(e3) + + # event 4, right click to remove point with exact coordinates + e4 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=3) + e4.xdata, e4.ydata = (50, 50) + drawer_rgb.onclick(e4) + + # event 5, right click to remove point with coordinates close but not equal + e5 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=3) + e5.xdata, e5.ydata = (301, 200) + drawer_rgb.onclick(e5) + + assert drawer_rgb.coords["default"][0] == point1 + + +def test_points_print_coords(test_data, tmpdir): + """Test for plantcv-annotate.""" + cache_dir = tmpdir.mkdir("cache") + filename = os.path.join(cache_dir, 'plantcv_print_coords.txt') + # Read in a test image + img = cv2.imread(test_data.small_rgb_img) + + # initialize interactive tool + drawer_rgb = Points(img, figsize=(5, 5)) + + # simulate mouse clicks + # event 1, left click to add point + e1 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=1) + point1 = (200, 200) + e1.xdata, e1.ydata = point1 + drawer_rgb.onclick(e1) + + # event 2, left click to add point + e2 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=1) + e2.xdata, e2.ydata = (300, 200) + drawer_rgb.onclick(e2) + + # Save collected coords out + drawer_rgb.print_coords(filename) + assert os.path.exists(filename) + + +def test_points_import_list(test_data): + """Test for plantcv-annotate.""" + # Read in a test image + img = cv2.imread(test_data.small_rgb_img) + # initialize interactive tool + drawer_rgb = Points(img, figsize=(5, 5), label="default") + totalpoints1 = [(158, 531), (361, 112), (500, 418), (269.25303806488864, 385.69839981447126), + (231.21964288863632, 445.995245825603), (293.37177646934134, 448.778177179963), + (240.49608073650273, 277.1640769944342), (279.4571196975417, 240.05832560296852), + (77.23077461405376, 165.84682282003712), (420, 364), (509.5127783246289, 353.2308673469388), + (527.1380102355752, 275.3087894248609), (445.50535717435065, 138.94515306122452)] + drawer_rgb.import_list(coords=totalpoints1, label="imported") + + assert len(drawer_rgb.coords["imported"]) == 13 + + +def test_points_import_list_warn(test_data): + """Test for plantcv-annotate.""" + # Read in a test image + img = cv2.imread(test_data.small_rgb_img) + # initialize interactive tool + drawer_rgb = Points(img, figsize=(5, 5), label="default") + totalpoints1 = [(158, 531), (361, 112), (500, 418), (445.50535717435065, 138.94515306122452)] + drawer_rgb.import_list(coords=totalpoints1) + + assert len(drawer_rgb.coords["default"]) == 0 + + +def test_points_import_file(test_data): + """Test for plantcv-annotate.""" + img = cv2.imread(test_data.small_rgb_img) + counter = Points(img, figsize=(5, 5)) + file = test_data.pollen_coords + counter.import_file(file) + + assert counter.count['total'] == 70 + + +def test_points_view(test_data): + """Test for plantcv-annotate.""" + # Read in a test grayscale image + img = cv2.imread(test_data.small_rgb_img) + + # initialize interactive tool + drawer_rgb = Points(img, figsize=(5, 5)) + + # simulate mouse clicks + # event 1, left click to add point + e1 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=1) + point1 = (200, 200) + e1.xdata, e1.ydata = point1 + drawer_rgb.onclick(e1) + drawer_rgb.view(label="new", view_all=True) + e2 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=1) + e2.xdata, e2.ydata = (300, 200) + drawer_rgb.onclick(e2) + drawer_rgb.view(view_all=False) + + assert str(drawer_rgb.fig) == "Figure(500x500)" + + +def test_points_view_warn(test_data): + """Test for plantcv-annotate.""" + # Read in a test grayscale image + img = cv2.imread(test_data.small_rgb_img) + + # initialize interactive tool, implied default label and "r" color + drawer_rgb = Points(img, figsize=(5, 5)) + + # simulate mouse clicks, event 1=left click to add point + e1 = matplotlib.backend_bases.MouseEvent(name="button_press_event", canvas=drawer_rgb.fig.canvas, + x=0, y=0, button=1) + point1 = (200, 200) + e1.xdata, e1.ydata = point1 + drawer_rgb.onclick(e1) + drawer_rgb.view(label="new", color='r') + + assert str(drawer_rgb.fig) == "Figure(500x500)" From b1e1812a07e9e7210dd4b37b641e3f26c5731411 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 11:23:48 -0500 Subject: [PATCH 69/96] Create test_annotate_get_centroids.py add centroid tests --- tests/test_annotate_get_centroids.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/test_annotate_get_centroids.py diff --git a/tests/test_annotate_get_centroids.py b/tests/test_annotate_get_centroids.py new file mode 100644 index 0000000..54c4db3 --- /dev/null +++ b/tests/test_annotate_get_centroids.py @@ -0,0 +1,12 @@ +import cv2 +from plantcv.annotate.get_centroids import get_centroids + + +def test_get_centroids(test_data): + """Test for PlantCV.""" + # Read in test data + mask = cv2.imread(test_data.small_bin_img, -1) + + coor = get_centroids(bin_img=mask) + + assert coor == [(166, 214)] From ecfbc68ac768937253f14b7a08f95cd06ce35f85 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 11:27:26 -0500 Subject: [PATCH 70/96] Create test_napari_open.py napari open --- tests/test_napari_open.py | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/test_napari_open.py diff --git a/tests/test_napari_open.py b/tests/test_napari_open.py new file mode 100644 index 0000000..cfb9d25 --- /dev/null +++ b/tests/test_napari_open.py @@ -0,0 +1,40 @@ +import numpy as np +from plantcv.annotate import napari_open +from plantcv.plantcv import readimage + + +def test_napari_open_rgb(test_data): + """Test for PlantCV.Annotate""" + # Read in test data + img, _, _ = readimage(test_data.small_rgb_img) + viewer = napari_open(img, show=False) + coor = [(25, 25), (50, 50)] + viewer.add_points(np.array(coor), symbol="o", name="total", + face_color="red", size=1) + + assert len(viewer.layers['total'].data) == 2 + + +def test_napari_open_gray(test_data): + """Test for PlantCV.Annotate""" + # Read in test data + img, _, _ = readimage(test_data.kmeans_seed_gray_img) + viewer = napari_open(img, show=False) + coor = [(25, 25), (50, 50)] + viewer.add_points(np.array(coor), symbol="o", name="total", + face_color="red", size=1) + + assert len(viewer.layers['total'].data) == 2 + + +def test_napari_open_envi(test_data): + """Test for PlantCV.Annotate""" + # Read in test data + img = readimage(test_data.envi_sample_data, mode='envi') + img = img.array_data + viewer = napari_open(img, show=False) + coor = [(25, 25), (50, 50)] + viewer.add_points(np.array(coor), symbol="o", name="total", + face_color="red", size=1) + + assert len(viewer.layers['total'].data) == 2 From cb848b5910d7aa6c25352251116d6440119478b4 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 11:35:59 -0500 Subject: [PATCH 71/96] Create test_napari_classes.py napari classes --- tests/test_napari_classes.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/test_napari_classes.py diff --git a/tests/test_napari_classes.py b/tests/test_napari_classes.py new file mode 100644 index 0000000..f4050a9 --- /dev/null +++ b/tests/test_napari_classes.py @@ -0,0 +1,18 @@ +import numpy as np +from plantcv.annotate import napari_classes + + +def test_napari_classes(make_napari_viewer): + """Test for PlantCV.Annotate""" + # Read in test data + viewer = make_napari_viewer(show=False) + img = np.zeros((100, 100)) + coor = [(25, 25), (50, 50)] + viewer.add_image(img) + viewer.add_points(np.array(coor), symbol="o", name="total", + face_color="red", size=30) + viewer.add_points(np.array(coor), symbol="o", name="test", + face_color="red", size=30) + keys = napari_classes(viewer) + + assert keys == ['total', 'test'] From 9a0fa6e064e70c280281cc9d3643501c3e1ebcdd Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 11:42:28 -0500 Subject: [PATCH 72/96] Create test_napari_label_classes.py napari label classes --- tests/test_napari_label_classes.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/test_napari_label_classes.py diff --git a/tests/test_napari_label_classes.py b/tests/test_napari_label_classes.py new file mode 100644 index 0000000..ca49b8f --- /dev/null +++ b/tests/test_napari_label_classes.py @@ -0,0 +1,15 @@ +import numpy as np +from plantcv.annotate import napari_label_classes +from plantcv.plantcv import readimage + + +def test_napari_label_classes_gray(test_data): + """Test for PlantCV.Annotate""" + # Read in test data + img, _, _ = readimage(test_data.kmeans_seed_gray_img) + viewer = napari_label_classes(img, ['seed'], show=False) + coor = [(25, 25)] + viewer.add_points(np.array(coor), symbol="o", name='background', + face_color="red", size=1) + + assert len(viewer.layers['background'].data) == 1 From c23e57b9ea057e00a096aa94d6e360bb2cf1b79e Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 11:49:18 -0500 Subject: [PATCH 73/96] viwer close testing viewer close on napari classes --- tests/test_napari_classes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_napari_classes.py b/tests/test_napari_classes.py index f4050a9..5e21ddf 100644 --- a/tests/test_napari_classes.py +++ b/tests/test_napari_classes.py @@ -16,3 +16,5 @@ def test_napari_classes(make_napari_viewer): keys = napari_classes(viewer) assert keys == ['total', 'test'] + + viewer.close() From c6035cb5314538ce134affd60b8980708e6d154a Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 11:53:02 -0500 Subject: [PATCH 74/96] Update test_napari_label_classes.py testing viewer close --- tests/test_napari_label_classes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_napari_label_classes.py b/tests/test_napari_label_classes.py index ca49b8f..80561fa 100644 --- a/tests/test_napari_label_classes.py +++ b/tests/test_napari_label_classes.py @@ -13,3 +13,5 @@ def test_napari_label_classes_gray(test_data): face_color="red", size=1) assert len(viewer.layers['background'].data) == 1 + + viewer.close() From 4f3555ab093c9eda11f5cbde76297a5e4176ed7d Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 12:00:34 -0500 Subject: [PATCH 75/96] Update test_napari_open.py add viewer.close() to napari.open --- tests/test_napari_open.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_napari_open.py b/tests/test_napari_open.py index cfb9d25..602e3ef 100644 --- a/tests/test_napari_open.py +++ b/tests/test_napari_open.py @@ -13,6 +13,7 @@ def test_napari_open_rgb(test_data): face_color="red", size=1) assert len(viewer.layers['total'].data) == 2 + viewer.close() def test_napari_open_gray(test_data): @@ -25,6 +26,7 @@ def test_napari_open_gray(test_data): face_color="red", size=1) assert len(viewer.layers['total'].data) == 2 + viewer.close() def test_napari_open_envi(test_data): @@ -38,3 +40,4 @@ def test_napari_open_envi(test_data): face_color="red", size=1) assert len(viewer.layers['total'].data) == 2 + viewer.close() From dbb565ff87f5278ded9efea5c90a7a44fafdee3c Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 12:06:25 -0500 Subject: [PATCH 76/96] Create test_napari_join_labels.py added napari join labels tests with viewer close --- tests/test_napari_join_labels.py | 83 ++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/test_napari_join_labels.py diff --git a/tests/test_napari_join_labels.py b/tests/test_napari_join_labels.py new file mode 100644 index 0000000..f3ae04b --- /dev/null +++ b/tests/test_napari_join_labels.py @@ -0,0 +1,83 @@ +import numpy as np +from plantcv.annotate.napari_open import napari_open +from plantcv.annotate.napari_label_classes import napari_label_classes +from plantcv.plantcv import readimage +from plantcv.annotate.napari_join_labels import napari_join_labels +from plantcv.plantcv import params + + +def test_napari_join_labels(test_data): + """Test for PlantCV.Annotate""" + # Read in test data + img, _, _ = readimage(test_data.kmeans_seed_gray_img) + viewer = napari_label_classes(img, ["seed"], show=False) + labeled, _ = napari_join_labels(img, viewer) + + assert np.shape(labeled) == (576, 537) + viewer.close() + + +def test_napari_join_allclass(test_data): + """Test for PlantCV.Annotate""" + # Read in test data + img, _, _ = readimage(test_data.kmeans_seed_gray_img) + viewer = napari_open(img, show=False) + background = [(54, 143), (77, 246)] + viewer.add_points(np.array(background), symbol="o", name='background', + face_color="red", size=1) + wing = [(275, 54)] + viewer.add_points(np.array(wing), symbol="o", name='wing', + face_color="red", size=1) + seed = [(280, 218)] + viewer.add_points(np.array(seed), symbol="o", name='seed', + face_color="red", size=1) + + labeled, _ = napari_join_labels(img, viewer) + + assert np.shape(labeled) == (576, 537) + viewer.close() + + +def test_napari_join_warn(test_data): + """Test for PlantCV.Annotate""" + # Read in test data + img, _, _ = readimage(test_data.kmeans_seed_gray_img) + viewer = napari_open(img, show=False) + background = [(54, 143), (77, 246)] + viewer.add_points(np.array(background), symbol="o", name='background', + face_color="red", size=1) + wing = [(275, 54)] + viewer.add_points(np.array(wing), symbol="o", name='wing', + face_color="red", size=1) + seed = [(275, 54)] + viewer.add_points(np.array(seed), symbol="o", name='seed', + face_color="red", size=1) + + labeled, _ = napari_join_labels(img, viewer) + + assert np.shape(labeled) == (576, 537) + viewer.close() + + +def test_napari_join_print(test_data, tmpdir): + """Test for PlantCV.Annotate""" + params.debug = 'print' + cache_dir = tmpdir.mkdir("cache") + params.debug_outdir = cache_dir + # Read in test data + img, _, _ = readimage(test_data.kmeans_seed_gray_img) + viewer = napari_open(img, show=False) + background = [(54, 143), (77, 246)] + viewer.add_points(np.array(background), symbol="o", name='background', + face_color="red", size=1) + wing = [(280, 218)] + viewer.add_points(np.array(wing), symbol="o", name='wing', + face_color="red", size=1) + seed = [(275, 54)] + viewer.add_points(np.array(seed), symbol="o", name='seed', + face_color="red", size=1) + + labeled, _ = napari_join_labels(img, viewer) + + assert np.shape(labeled) == (576, 537) + viewer.close() From b170a9a2ca7d5e5a509328c680ac65c47f962784 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 12:09:22 -0500 Subject: [PATCH 77/96] Delete test_napari_join_labels.py --- tests/test_napari_join_labels.py | 83 -------------------------------- 1 file changed, 83 deletions(-) delete mode 100644 tests/test_napari_join_labels.py diff --git a/tests/test_napari_join_labels.py b/tests/test_napari_join_labels.py deleted file mode 100644 index f3ae04b..0000000 --- a/tests/test_napari_join_labels.py +++ /dev/null @@ -1,83 +0,0 @@ -import numpy as np -from plantcv.annotate.napari_open import napari_open -from plantcv.annotate.napari_label_classes import napari_label_classes -from plantcv.plantcv import readimage -from plantcv.annotate.napari_join_labels import napari_join_labels -from plantcv.plantcv import params - - -def test_napari_join_labels(test_data): - """Test for PlantCV.Annotate""" - # Read in test data - img, _, _ = readimage(test_data.kmeans_seed_gray_img) - viewer = napari_label_classes(img, ["seed"], show=False) - labeled, _ = napari_join_labels(img, viewer) - - assert np.shape(labeled) == (576, 537) - viewer.close() - - -def test_napari_join_allclass(test_data): - """Test for PlantCV.Annotate""" - # Read in test data - img, _, _ = readimage(test_data.kmeans_seed_gray_img) - viewer = napari_open(img, show=False) - background = [(54, 143), (77, 246)] - viewer.add_points(np.array(background), symbol="o", name='background', - face_color="red", size=1) - wing = [(275, 54)] - viewer.add_points(np.array(wing), symbol="o", name='wing', - face_color="red", size=1) - seed = [(280, 218)] - viewer.add_points(np.array(seed), symbol="o", name='seed', - face_color="red", size=1) - - labeled, _ = napari_join_labels(img, viewer) - - assert np.shape(labeled) == (576, 537) - viewer.close() - - -def test_napari_join_warn(test_data): - """Test for PlantCV.Annotate""" - # Read in test data - img, _, _ = readimage(test_data.kmeans_seed_gray_img) - viewer = napari_open(img, show=False) - background = [(54, 143), (77, 246)] - viewer.add_points(np.array(background), symbol="o", name='background', - face_color="red", size=1) - wing = [(275, 54)] - viewer.add_points(np.array(wing), symbol="o", name='wing', - face_color="red", size=1) - seed = [(275, 54)] - viewer.add_points(np.array(seed), symbol="o", name='seed', - face_color="red", size=1) - - labeled, _ = napari_join_labels(img, viewer) - - assert np.shape(labeled) == (576, 537) - viewer.close() - - -def test_napari_join_print(test_data, tmpdir): - """Test for PlantCV.Annotate""" - params.debug = 'print' - cache_dir = tmpdir.mkdir("cache") - params.debug_outdir = cache_dir - # Read in test data - img, _, _ = readimage(test_data.kmeans_seed_gray_img) - viewer = napari_open(img, show=False) - background = [(54, 143), (77, 246)] - viewer.add_points(np.array(background), symbol="o", name='background', - face_color="red", size=1) - wing = [(280, 218)] - viewer.add_points(np.array(wing), symbol="o", name='wing', - face_color="red", size=1) - seed = [(275, 54)] - viewer.add_points(np.array(seed), symbol="o", name='seed', - face_color="red", size=1) - - labeled, _ = napari_join_labels(img, viewer) - - assert np.shape(labeled) == (576, 537) - viewer.close() From 6070954064823990783fd375db7f4a7a873ba178 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 12:12:27 -0500 Subject: [PATCH 78/96] Create test_napari_join_labels.py --- tests/test_napari_join_labels.py | 83 ++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/test_napari_join_labels.py diff --git a/tests/test_napari_join_labels.py b/tests/test_napari_join_labels.py new file mode 100644 index 0000000..f3ae04b --- /dev/null +++ b/tests/test_napari_join_labels.py @@ -0,0 +1,83 @@ +import numpy as np +from plantcv.annotate.napari_open import napari_open +from plantcv.annotate.napari_label_classes import napari_label_classes +from plantcv.plantcv import readimage +from plantcv.annotate.napari_join_labels import napari_join_labels +from plantcv.plantcv import params + + +def test_napari_join_labels(test_data): + """Test for PlantCV.Annotate""" + # Read in test data + img, _, _ = readimage(test_data.kmeans_seed_gray_img) + viewer = napari_label_classes(img, ["seed"], show=False) + labeled, _ = napari_join_labels(img, viewer) + + assert np.shape(labeled) == (576, 537) + viewer.close() + + +def test_napari_join_allclass(test_data): + """Test for PlantCV.Annotate""" + # Read in test data + img, _, _ = readimage(test_data.kmeans_seed_gray_img) + viewer = napari_open(img, show=False) + background = [(54, 143), (77, 246)] + viewer.add_points(np.array(background), symbol="o", name='background', + face_color="red", size=1) + wing = [(275, 54)] + viewer.add_points(np.array(wing), symbol="o", name='wing', + face_color="red", size=1) + seed = [(280, 218)] + viewer.add_points(np.array(seed), symbol="o", name='seed', + face_color="red", size=1) + + labeled, _ = napari_join_labels(img, viewer) + + assert np.shape(labeled) == (576, 537) + viewer.close() + + +def test_napari_join_warn(test_data): + """Test for PlantCV.Annotate""" + # Read in test data + img, _, _ = readimage(test_data.kmeans_seed_gray_img) + viewer = napari_open(img, show=False) + background = [(54, 143), (77, 246)] + viewer.add_points(np.array(background), symbol="o", name='background', + face_color="red", size=1) + wing = [(275, 54)] + viewer.add_points(np.array(wing), symbol="o", name='wing', + face_color="red", size=1) + seed = [(275, 54)] + viewer.add_points(np.array(seed), symbol="o", name='seed', + face_color="red", size=1) + + labeled, _ = napari_join_labels(img, viewer) + + assert np.shape(labeled) == (576, 537) + viewer.close() + + +def test_napari_join_print(test_data, tmpdir): + """Test for PlantCV.Annotate""" + params.debug = 'print' + cache_dir = tmpdir.mkdir("cache") + params.debug_outdir = cache_dir + # Read in test data + img, _, _ = readimage(test_data.kmeans_seed_gray_img) + viewer = napari_open(img, show=False) + background = [(54, 143), (77, 246)] + viewer.add_points(np.array(background), symbol="o", name='background', + face_color="red", size=1) + wing = [(280, 218)] + viewer.add_points(np.array(wing), symbol="o", name='wing', + face_color="red", size=1) + seed = [(275, 54)] + viewer.add_points(np.array(seed), symbol="o", name='seed', + face_color="red", size=1) + + labeled, _ = napari_join_labels(img, viewer) + + assert np.shape(labeled) == (576, 537) + viewer.close() From 272f0825b241e392e082b7c909d2f0836a7151a7 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 12:27:03 -0500 Subject: [PATCH 79/96] line returns. remove some line returns for consistency. --- tests/test_napari_classes.py | 1 - tests/test_napari_label_classes.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/test_napari_classes.py b/tests/test_napari_classes.py index 5e21ddf..e71826d 100644 --- a/tests/test_napari_classes.py +++ b/tests/test_napari_classes.py @@ -16,5 +16,4 @@ def test_napari_classes(make_napari_viewer): keys = napari_classes(viewer) assert keys == ['total', 'test'] - viewer.close() diff --git a/tests/test_napari_label_classes.py b/tests/test_napari_label_classes.py index 80561fa..a2ba4f9 100644 --- a/tests/test_napari_label_classes.py +++ b/tests/test_napari_label_classes.py @@ -13,5 +13,4 @@ def test_napari_label_classes_gray(test_data): face_color="red", size=1) assert len(viewer.layers['background'].data) == 1 - viewer.close() From 252ef77ba470fe70ebdab20fb4a975a3809ce707 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 12:31:41 -0500 Subject: [PATCH 80/96] fixing viewer.close --- tests/test_napari_classes.py | 1 + tests/test_napari_join_labels.py | 4 ++++ tests/test_napari_label_classes.py | 1 + tests/test_napari_open.py | 3 +++ tests/test_napari_save_coor.py | 1 + 5 files changed, 10 insertions(+) diff --git a/tests/test_napari_classes.py b/tests/test_napari_classes.py index f4050a9..e71826d 100644 --- a/tests/test_napari_classes.py +++ b/tests/test_napari_classes.py @@ -16,3 +16,4 @@ def test_napari_classes(make_napari_viewer): keys = napari_classes(viewer) assert keys == ['total', 'test'] + viewer.close() diff --git a/tests/test_napari_join_labels.py b/tests/test_napari_join_labels.py index 269465d..f3ae04b 100644 --- a/tests/test_napari_join_labels.py +++ b/tests/test_napari_join_labels.py @@ -14,6 +14,7 @@ def test_napari_join_labels(test_data): labeled, _ = napari_join_labels(img, viewer) assert np.shape(labeled) == (576, 537) + viewer.close() def test_napari_join_allclass(test_data): @@ -34,6 +35,7 @@ def test_napari_join_allclass(test_data): labeled, _ = napari_join_labels(img, viewer) assert np.shape(labeled) == (576, 537) + viewer.close() def test_napari_join_warn(test_data): @@ -54,6 +56,7 @@ def test_napari_join_warn(test_data): labeled, _ = napari_join_labels(img, viewer) assert np.shape(labeled) == (576, 537) + viewer.close() def test_napari_join_print(test_data, tmpdir): @@ -77,3 +80,4 @@ def test_napari_join_print(test_data, tmpdir): labeled, _ = napari_join_labels(img, viewer) assert np.shape(labeled) == (576, 537) + viewer.close() diff --git a/tests/test_napari_label_classes.py b/tests/test_napari_label_classes.py index ca49b8f..a2ba4f9 100644 --- a/tests/test_napari_label_classes.py +++ b/tests/test_napari_label_classes.py @@ -13,3 +13,4 @@ def test_napari_label_classes_gray(test_data): face_color="red", size=1) assert len(viewer.layers['background'].data) == 1 + viewer.close() diff --git a/tests/test_napari_open.py b/tests/test_napari_open.py index cfb9d25..602e3ef 100644 --- a/tests/test_napari_open.py +++ b/tests/test_napari_open.py @@ -13,6 +13,7 @@ def test_napari_open_rgb(test_data): face_color="red", size=1) assert len(viewer.layers['total'].data) == 2 + viewer.close() def test_napari_open_gray(test_data): @@ -25,6 +26,7 @@ def test_napari_open_gray(test_data): face_color="red", size=1) assert len(viewer.layers['total'].data) == 2 + viewer.close() def test_napari_open_envi(test_data): @@ -38,3 +40,4 @@ def test_napari_open_envi(test_data): face_color="red", size=1) assert len(viewer.layers['total'].data) == 2 + viewer.close() diff --git a/tests/test_napari_save_coor.py b/tests/test_napari_save_coor.py index 3f3f9c0..fafc359 100644 --- a/tests/test_napari_save_coor.py +++ b/tests/test_napari_save_coor.py @@ -22,3 +22,4 @@ def test_napari_save_coor(test_data, tmpdir): _ = napari_save_coor(viewer, filename) assert os.path.exists(filename) + viewer.close() From 730cb8c8aa21d84a26406a11de69b103494e1c9c Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Wed, 15 May 2024 12:43:18 -0500 Subject: [PATCH 81/96] viewer.close() added to check to make sure tests are okay. --- tests/test_napari_classes.py | 1 + tests/test_napari_join_labels.py | 5 +++++ tests/test_napari_label_classes.py | 1 + tests/test_napari_open.py | 1 + tests/test_napari_save_coor.py | 1 + 5 files changed, 9 insertions(+) diff --git a/tests/test_napari_classes.py b/tests/test_napari_classes.py index f4050a9..e71826d 100644 --- a/tests/test_napari_classes.py +++ b/tests/test_napari_classes.py @@ -16,3 +16,4 @@ def test_napari_classes(make_napari_viewer): keys = napari_classes(viewer) assert keys == ['total', 'test'] + viewer.close() diff --git a/tests/test_napari_join_labels.py b/tests/test_napari_join_labels.py index 408f82e..f3ae04b 100644 --- a/tests/test_napari_join_labels.py +++ b/tests/test_napari_join_labels.py @@ -5,6 +5,7 @@ from plantcv.annotate.napari_join_labels import napari_join_labels from plantcv.plantcv import params + def test_napari_join_labels(test_data): """Test for PlantCV.Annotate""" # Read in test data @@ -13,6 +14,7 @@ def test_napari_join_labels(test_data): labeled, _ = napari_join_labels(img, viewer) assert np.shape(labeled) == (576, 537) + viewer.close() def test_napari_join_allclass(test_data): @@ -33,6 +35,7 @@ def test_napari_join_allclass(test_data): labeled, _ = napari_join_labels(img, viewer) assert np.shape(labeled) == (576, 537) + viewer.close() def test_napari_join_warn(test_data): @@ -53,6 +56,7 @@ def test_napari_join_warn(test_data): labeled, _ = napari_join_labels(img, viewer) assert np.shape(labeled) == (576, 537) + viewer.close() def test_napari_join_print(test_data, tmpdir): @@ -76,3 +80,4 @@ def test_napari_join_print(test_data, tmpdir): labeled, _ = napari_join_labels(img, viewer) assert np.shape(labeled) == (576, 537) + viewer.close() diff --git a/tests/test_napari_label_classes.py b/tests/test_napari_label_classes.py index ca49b8f..a2ba4f9 100644 --- a/tests/test_napari_label_classes.py +++ b/tests/test_napari_label_classes.py @@ -13,3 +13,4 @@ def test_napari_label_classes_gray(test_data): face_color="red", size=1) assert len(viewer.layers['background'].data) == 1 + viewer.close() diff --git a/tests/test_napari_open.py b/tests/test_napari_open.py index cfb9d25..0a445ee 100644 --- a/tests/test_napari_open.py +++ b/tests/test_napari_open.py @@ -38,3 +38,4 @@ def test_napari_open_envi(test_data): face_color="red", size=1) assert len(viewer.layers['total'].data) == 2 + viewer.close() diff --git a/tests/test_napari_save_coor.py b/tests/test_napari_save_coor.py index 3f3f9c0..fafc359 100644 --- a/tests/test_napari_save_coor.py +++ b/tests/test_napari_save_coor.py @@ -22,3 +22,4 @@ def test_napari_save_coor(test_data, tmpdir): _ = napari_save_coor(viewer, filename) assert os.path.exists(filename) + viewer.close() From 3daac8b63fe1bb5d22c63ef8e0ac8fd9aeca7eed Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Thu, 16 May 2024 09:38:01 -0500 Subject: [PATCH 82/96] Update continuous-integration.yml remove verbose and delay --- .github/workflows/continuous-integration.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index ed57f0e..e33b067 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -49,9 +49,8 @@ jobs: - name: Tests uses: aganders3/headless-gui@v2.2 with: - linux-setup-delay: 10000 xvfb-screen-size: 1280x720x24 - run: pytest -vv --cov-report=xml --cov=./ + run: pytest --cov-report=xml --cov=./ - name: Upload coverage to Deepsource uses: deepsourcelabs/test-coverage-action@master with: From 19129c4ef4ea011cf1df6f9bb355b90fcb0080c4 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Thu, 16 May 2024 09:46:07 -0500 Subject: [PATCH 83/96] Update continuous-integration.yml --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index e33b067..ca30610 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.9','3.10', '3.11'] + python-version: ['3.9', '3.10', '3.11'] os: [ubuntu-latest] env: OS: ${{ matrix.os }} From 22814516a14c4bc3dc62df256b030ae471f26a1f Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Thu, 22 Aug 2024 21:10:59 -0500 Subject: [PATCH 84/96] update napari_open update napari_open function tests and docs so the default behavior is not to colorize gray images by adding a mode option. --- docs/napari_open.md | 5 +++-- plantcv/annotate/napari_open.py | 10 ++++++---- tests/test_napari_open.py | 13 +++++++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/docs/napari_open.md b/docs/napari_open.md index c699a85..30fef49 100644 --- a/docs/napari_open.md +++ b/docs/napari_open.md @@ -2,12 +2,13 @@ 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. -**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) + - mode - 'native' or 'colorize'. If 'colorized' is selected grayimages will be colorized. - show - if show = True, viewer is launched. False setting is useful for test purposes. - **Context:** @@ -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 diff --git a/plantcv/annotate/napari_open.py b/plantcv/annotate/napari_open.py index 59fa317..e2c56ff 100755 --- a/plantcv/annotate/napari_open.py +++ b/plantcv/annotate/napari_open.py @@ -6,13 +6,14 @@ import napari -def napari_open(img, show=True): +def napari_open(img, mode='native', show=True): """ open img in napari Inputs: img = img (grayimg, rgbimg, or hyperspectral image array data e.g. hyperspectraldata.array_data) + mode = 'native or 'colorize' show = if show is True the viewer is launched. This option is useful for running tests without triggering the viewer. @@ -25,9 +26,10 @@ def napari_open(img, show=True): """ shape = np.shape(img) if len(shape) == 2: - colorful = label2rgb(img) - img = (255*colorful).astype(np.uint8) - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + if mode == 'colorize': + colorful = label2rgb(img) + img = (255*colorful).astype(np.uint8) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) if len(shape) == 3: if shape[2] == 3: img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) diff --git a/tests/test_napari_open.py b/tests/test_napari_open.py index 602e3ef..c6d1376 100644 --- a/tests/test_napari_open.py +++ b/tests/test_napari_open.py @@ -29,6 +29,19 @@ def test_napari_open_gray(test_data): viewer.close() +def test_napari_open_gray1(test_data): + """Test for PlantCV.Annotate""" + # Read in test data + img, _, _ = readimage(test_data.kmeans_seed_gray_img) + viewer = napari_open(img, mode='colorize', show=False) + coor = [(25, 25), (50, 50)] + viewer.add_points(np.array(coor), symbol="o", name="total", + face_color="red", size=1) + + assert len(viewer.layers['total'].data) == 2 + viewer.close() + + def test_napari_open_envi(test_data): """Test for PlantCV.Annotate""" # Read in test data From fd8c66f70bc205f500158d7a33eb3add80e0b5e3 Mon Sep 17 00:00:00 2001 From: Malia Gehan Date: Thu, 22 Aug 2024 21:15:22 -0500 Subject: [PATCH 85/96] deepsource if statement combined if statements for deepsource --- plantcv/annotate/napari_open.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plantcv/annotate/napari_open.py b/plantcv/annotate/napari_open.py index e2c56ff..5c08e5d 100755 --- a/plantcv/annotate/napari_open.py +++ b/plantcv/annotate/napari_open.py @@ -25,11 +25,10 @@ def napari_open(img, mode='native', show=True): """ shape = np.shape(img) - if len(shape) == 2: - if mode == 'colorize': - colorful = label2rgb(img) - img = (255*colorful).astype(np.uint8) - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + if len(shape) == 2 and mode == 'colorize': + colorful = label2rgb(img) + img = (255*colorful).astype(np.uint8) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) if len(shape) == 3: if shape[2] == 3: img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) From 5be390037b196c39cc796eb6f35d404ad5e2dbbb Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Tue, 3 Sep 2024 09:52:36 -0500 Subject: [PATCH 86/96] initial docs for changelog --- docs/changelog.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 820a7f1..cb9e2bd 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,27 @@ ## 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.Points + +* v0.1dev: viewer = **annotate.Points**(*img, figsize=(12,6), label="dafault"*) From 5678c688d65cce15236f632ba23d369fb3a9390b Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Tue, 3 Sep 2024 10:12:25 -0500 Subject: [PATCH 87/96] minor updates to docs page --- docs/napari_open.md | 8 ++++---- plantcv/annotate/napari_open.py | 26 +++++++++++++------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/napari_open.md b/docs/napari_open.md index 30fef49..395cc98 100644 --- a/docs/napari_open.md +++ b/docs/napari_open.md @@ -1,14 +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, mode = 'native', 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) - - mode - 'native' or 'colorize'. If 'colorized' is selected grayimages will be colorized. + - 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:** diff --git a/plantcv/annotate/napari_open.py b/plantcv/annotate/napari_open.py index 5c08e5d..a229e50 100755 --- a/plantcv/annotate/napari_open.py +++ b/plantcv/annotate/napari_open.py @@ -7,22 +7,22 @@ def napari_open(img, mode='native', show=True): - """ - open img in napari + """Open an image with a napari interactive viewer - Inputs: - img = img (grayimg, rgbimg, or hyperspectral image array data e.g. - hyperspectraldata.array_data) - mode = 'native or 'colorize' - show = if show is True the viewer is launched. This option is useful for + Parameters + ---------- + img : numpy.ndarray + Image to be opened, img can be gray, rgb, or multispectral + mode: str + Viewing mode, either 'native' (default) or 'colorize' + show: bool + if show is True the viewer is launched. This option is useful for running tests without triggering the viewer. - Returns: - viewer = napari viewer object - - :param img: numpy.ndarray - :return viewer: napari viewer object - + Returns + ------- + napari.viewer.Viewer + Napari viewer object """ shape = np.shape(img) if len(shape) == 2 and mode == 'colorize': From 4a99ed00333df0f9bf89d0d0761e80ce04bac310 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Tue, 3 Sep 2024 10:14:40 -0500 Subject: [PATCH 88/96] remove extra * in changelog.md --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index cb9e2bd..fa2561f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented below. -#### annotate.get_centroids** +#### annotate.get_centroids * v0.1dev: coords_list = **annotate.get_centroids**(*bin_img*) From 5e8fbbd69fa8b709f4e05ec289c67bc57fcdbc30 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Thu, 12 Sep 2024 11:53:12 -0500 Subject: [PATCH 89/96] Update changelog.md --- docs/changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index fa2561f..ba71ffd 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -22,6 +22,10 @@ All notable changes to this project will be documented below. * 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"*) From 2a0281518f026ed6c69fe259f393faec68c2bd13 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Thu, 12 Sep 2024 11:53:27 -0500 Subject: [PATCH 90/96] minor clarifications --- docs/napari_save_coor.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/napari_save_coor.md b/docs/napari_save_coor.md index f23815b..6ea22e1 100644 --- a/docs/napari_save_coor.md +++ b/docs/napari_save_coor.md @@ -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 @@ -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) From fbffb29ef85da8accd71797d7e85b02bbb6110d6 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Thu, 12 Sep 2024 11:55:30 -0500 Subject: [PATCH 91/96] Remove close, the with context manager handles this already --- plantcv/annotate/napari_save_coor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plantcv/annotate/napari_save_coor.py b/plantcv/annotate/napari_save_coor.py index 2041141..5011000 100755 --- a/plantcv/annotate/napari_save_coor.py +++ b/plantcv/annotate/napari_save_coor.py @@ -36,6 +36,5 @@ def napari_save_coor(viewer, filepath): filepath = str(filepath)+"_1.txt" with open(filepath, 'w') as fp: json.dump(datadict, fp) - fp.close() return datadict From 8cb260af6d07f036647f893f68b29c058bbaa37b Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Thu, 12 Sep 2024 12:00:08 -0500 Subject: [PATCH 92/96] Update PULL_REQUEST_TEMPLATE.md changelog instead of updating.md in this repo --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0f9f940..f483803 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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 From 8b99b636b0fd8d420060f39cee2fa173349fa258 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Thu, 12 Sep 2024 13:22:10 -0500 Subject: [PATCH 93/96] Update changelog.md --- docs/changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index ba71ffd..7f8ac31 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -22,6 +22,10 @@ All notable changes to this project will be documented below. * v0.1dev: viewer = **annotate.napari_open**(*img, mode = 'native', show=True*) +#### annotate.napari_read_coor + +* v0.1dev: data = **annotate.napari_read_coor**(*coor, dataformat='yx'*) + #### annotate.napari_save_coor * v0.1dev: datadict = **annotate.napari_save_coor**(*viewer, filepath*) From 46e55f12b13dd685b3b27f43b8239001c17ca70a Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Thu, 12 Sep 2024 13:22:30 -0500 Subject: [PATCH 94/96] clarify where dict input upstream since get centroid not compat --- docs/napari_read_coor.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/napari_read_coor.md b/docs/napari_read_coor.md index 668cc91..2d22fc9 100644 --- a/docs/napari_read_coor.md +++ b/docs/napari_read_coor.md @@ -7,14 +7,14 @@ Save Points Labeled in Napari to a File **returns** dictionary of points labeled by class - **Parameters:** - - coor - dictionary object of coordinates or a path to json datafile with dictionary of point coordinates + - coor - dictionary object of coordinates, or a path to json datafile with dictionary of point coordinates - dataformat - either 'yx' or 'xy', Napari takes data as y,x format. If data is 'xy' data is converted from x,y to y,x - **Context:** - - Import data from a file and convert to Napari format data if necessary + - Import previously labeled points, or points from other functions (e.g. [`pcvan.napari_read_coor`](napari_read_coor.md)) - **Example use:** - - Import previously labeled points, or points from other functions (e.g. detect_centroid) + - Below ```python From 8dd136b5e05ae3355dc0e1aaefb08df2eccd6905 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Thu, 12 Sep 2024 15:14:41 -0500 Subject: [PATCH 95/96] Update napari_label_classes.md --- docs/napari_label_classes.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/napari_label_classes.md b/docs/napari_label_classes.md index db8ee17..6fab410 100644 --- a/docs/napari_label_classes.md +++ b/docs/napari_label_classes.md @@ -1,26 +1,26 @@ ## 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. +This function opens an image in Napari and then defines a set of Points layers with the user-defined labels called `classes`. A random `shape` of the annotation symbol is assigned to each of the `classes`. Image can be annotated as long as viewer is open. -**plantcv.annotate.napari_label_classes*(*img, classes, size, shape =10, 'square', importdata=False, show=True*) +**plantcv.annotate.napari_label_classes*(*img, classes, size=10, shape='square', importdata=False, 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) - classes - list of classes to label. This option is not necessary if data is data is imported. - - size - integer pixel size of label - - shape - can be 'o', 'arrow', 'clobber', 'cross', 'diamond', 'disc', 'hbar', 'ring', 'square', 'star', 'tailed_arrow', - 'triangle_down', 'triangle_up', 'vbar', 'x'. + - size - integer pixel size of label (also adjustable from the interactive Napari viewer) + - shape - shape of the annotation symbol. Can be 'o', 'arrow', 'clobber', 'cross', 'diamond', 'disc', 'hbar', 'ring', 'square' (default), 'star', 'tailed_arrow', + 'triangle_down', 'triangle_up', 'vbar', or 'x' (also adjustable from the interactive Napari viewer) - importdata - dictionary of data, data saved from napari_save_coor or data imported from napari_read_coor - - show - if show = True, viewer is launched. False setting is useful for test purposes. + - show - if `show=True`, viewer is launched. `False` setting is useful for test purposes. - **Context:** - - Adding class labels to images. Works best on an image that has objects segmented/classified with contours/clusters labeled with values (e.g. labeled mask, output of kmeans clustering). + - Adding one or more classes of points layer for annotation of the image. - **Example use:** - - Labeling output of kmeans clustering into classes. Labeling points. + - Ground truth counting, labeling classes of objects of interest. ```python @@ -31,9 +31,8 @@ import napari # Create an instance of the Points class img, path, name = pcv.readimage("./grayimg.png") -viewer = pcvan.napari_label_classes(img=img, classes=['background', 'wing','seed'], size = 30) - -# Should open interactive napari viewer +# Opens interactive napari viewer +viewer = pcvan.napari_label_classes(img=img, classes=['background', 'wing','seed'], size=30) ``` From 526fb46c9f320d62a499a5a6b95f6ed11eea71f7 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Thu, 12 Sep 2024 15:18:55 -0500 Subject: [PATCH 96/96] Update changelog.md --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 7f8ac31..4d0ef44 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -16,7 +16,7 @@ All notable changes to this project will be documented below. #### annotate.napari_label_classes -* v0.1dev: viewer = **annotate.napari_label_classes**(*img, classes, show=True*) +* v0.1dev: viewer = **annotate.napari_label_classes**(*img, classes, size=10, shape='square', importdata=False, show=True*) #### annotate.napari_open