From b276a17d071d5be72785372d26fe4e7b6f18e110 Mon Sep 17 00:00:00 2001 From: HaleySchuhl Date: Wed, 6 Mar 2024 14:30:57 -0600 Subject: [PATCH 1/7] 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 52ce0fdd7065b83d614efd94c0a6c774319e2ed9 Mon Sep 17 00:00:00 2001 From: Noah Fahlgren Date: Tue, 16 Apr 2024 11:58:33 -0500 Subject: [PATCH 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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)