From 3d618754ab66ef53fa67166b2bf18ccf899424ac Mon Sep 17 00:00:00 2001 From: k034b363 Date: Tue, 3 Dec 2024 15:28:30 -0600 Subject: [PATCH 1/8] Add function for saving napari points to text file --- docs/changelog.md | 6 ++- docs/napari_save_points.md | 39 ++++++++++++++ mkdocs.yml | 1 + plantcv/geospatial/__init__.py | 4 +- plantcv/geospatial/napari_save_points.py | 65 ++++++++++++++++++++++++ 5 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 docs/napari_save_points.md create mode 100644 plantcv/geospatial/napari_save_points.py diff --git a/docs/changelog.md b/docs/changelog.md index 72803a0..a6af88e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,9 +2,13 @@ All notable changes to this project will be documented below. +#### geospatial.napari_save_points + +* v0.1dev: **geospatial.napari_save_points**(*images, num_points, outdir="./", bands="R,G,B"*) + #### geospatial.points_to_geojson -* v0.1dev: **geosptial.points_to_geojson**(*img, viewer, out_path*) +* v0.1dev: **geospatial.points_to_geojson**(*img, viewer, out_path*) #### geospatial.read_geotif diff --git a/docs/napari_save_points.md b/docs/napari_save_points.md new file mode 100644 index 0000000..3ed07bd --- /dev/null +++ b/docs/napari_save_points.md @@ -0,0 +1,39 @@ +## Save Napari clicked points + +Uses Napari to open a list of images one at a time. The user then clicks a defined number of points on each image, which are then saved to a text file with the same name as the image. To be used upstream of warp for geospatial images that are not georeferenced. + +**geospatial.napari_save_points**(*images, num_points, outdir="./", bands="R,G,B"*) + +- **Parameters:** + - images - Either a list of image paths or a directory where images are stored. Each image will be opened in sequence. + - num_points - Integer of expected number of clicked points. For warping downstream, this should be minimally 4. + - outdir - Directory to save output text files with saved points for each image. Defaults to "./". + - bands - If input images are geotifs, this is the required band order for `plantcv.geospatial.read_geotif`. Not required if image type is not geotif. + +- **Outputs:** + - redo_list - list of images to redo because the number of clicks did not match num_points. Useful if the user makes a mistake and would like to retry for a subset of images. + +- **Context:** + - Saved files can be used to warp all images to a specified reference image to bring all images to the same frame. This is useful for geospatial images that have not been georeferenced, as this approximates a version of what using ground control points achieves. + +- **Example use:** + - below to click field corners + + +```python +import plantcv.geospatial as geo + +# Make a list of images, can also be a directory +img_list = ["./image1.jpg", "./image2.jpg", "./image3.jpg", "./image4.jpg"] + +# Opens a Napari window with a points layer for each image +# User should click reference points equal to num_points and then close the window, prompting the next one to open + +redo_list = geo.napari_save_points(img_list, num_points=4, outdir="./") + +# Text files with points are saved to outdir +# Output is a list of any images the user should redo + +``` + +**Source Code:** [Here](https://github.com/danforthcenter/plantcv-geospatial/blob/main/plantcv/geospatial/napari_save_points.py) \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 2d49917..a53bb44 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,6 +21,7 @@ nav: - Transform coordinate points: transform_points.md - Transform coordinate polygons: transform_polygons.md - Save clicked points as geojson: points_to_geojson.md + - Save Napari points to text file: napari_save_points.md markdown_extensions: - toc: permalink: True diff --git a/plantcv/geospatial/__init__.py b/plantcv/geospatial/__init__.py index f6ef044..c1eafde 100644 --- a/plantcv/geospatial/__init__.py +++ b/plantcv/geospatial/__init__.py @@ -3,6 +3,7 @@ from plantcv.geospatial.transform_points import transform_points from plantcv.geospatial.transform_polygons import transform_polygons from plantcv.geospatial.points_to_geojson import points_to_geojson +from plantcv.geospatial.napari_save_points import napari_save_points # Auto versioning __version__ = version("plantcv-geospatial") @@ -11,5 +12,6 @@ "read_geotif", "transform_points", "transform_polygons", - "points_to_geojson" + "points_to_geojson", + "napari_save_points" ] diff --git a/plantcv/geospatial/napari_save_points.py b/plantcv/geospatial/napari_save_points.py new file mode 100644 index 0000000..47ec977 --- /dev/null +++ b/plantcv/geospatial/napari_save_points.py @@ -0,0 +1,65 @@ +# Function to save out clicked points collected in napari + +import plantcv.plantcv as pcv +from plantcv.plantcv import warn +from plantcv.geospatial import read_geotif +import napari +import os + + +def napari_save_points(images, num_points, outdir="./", bands="R,G,B"): + """Opens a set of images one at a time in a Napari window, waits for users + to click points and then saves those points to a file with the same name as the image. + + Args: + images (list or str): Either a list of image paths or a directory name. + num_points (int): Number of points expected. If number of clicks received is different, + the image path is added to redo_list and returned. + outdir (str, optional): Directory to save text files with points. Defaults to "./". + bands (str, optional): Band list if input images are geotifs. Defaults to "R,G,B". + + Returns: + list: List of images to be redone due to a different number of clicked points than expected. + """ + # store debug + debug = pcv.params.debug + + pcv.params.debug = None + # Store images with mistakes to a new list + redo_list = [] + + image_paths = images + if not isinstance(images, list): + image_paths = [os.path.join(images, i) for i in os.listdir(images)] + # Loop over each image + for image_path in image_paths: + # Load the current image + img_type = (image_path.split("/")[-1]).split(".")[-1] + if img_type == "tif": + geo_image = read_geotif(image_path, bands=bands) + image = geo_image.pseudo_rgb + else: + image, _, _ = pcv.readimage(image_path) + # Save image name for output file + img_name = (image_path.split("/")[-1]).split(".")[:-1] + + viewer = napari.Viewer() + + # Add the image and points layer + viewer.add_image(image) + viewer.add_points(name="points") + viewer.show(block=True) + + # Save file if correct number of points + if len(viewer.layers["points"].data) == num_points: + with open(os.path.join(outdir, img_name[0]+"_warp.txt"), "w") as output: + for i in viewer.layers["points"].data: + point = [i[1], i[0]] + output.write(str(point) + '\t') + else: + redo_list.append(image_path) + warn('Image ' + str(image_path) + ' collected incorrect number of points. ' + + 'Added to redo list.') + # Reset debug + pcv.params.debug = debug + return redo_list From 27a73447c5b4d32f06cfa86df5610eb9d1258f36 Mon Sep 17 00:00:00 2001 From: k034b363 Date: Wed, 4 Dec 2024 10:23:25 -0600 Subject: [PATCH 2/8] Adding extra parameters necessary for tests --- docs/changelog.md | 2 +- docs/napari_save_points.md | 4 +++- plantcv/geospatial/napari_save_points.py | 8 +++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index a6af88e..3100471 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented below. #### geospatial.napari_save_points -* v0.1dev: **geospatial.napari_save_points**(*images, num_points, outdir="./", bands="R,G,B"*) +* v0.1dev: **geospatial.napari_save_points**(*images, num_points, outdir="./", bands="R,G,B", block=True, show=True*) #### geospatial.points_to_geojson diff --git a/docs/napari_save_points.md b/docs/napari_save_points.md index 3ed07bd..a356401 100644 --- a/docs/napari_save_points.md +++ b/docs/napari_save_points.md @@ -2,13 +2,15 @@ Uses Napari to open a list of images one at a time. The user then clicks a defined number of points on each image, which are then saved to a text file with the same name as the image. To be used upstream of warp for geospatial images that are not georeferenced. -**geospatial.napari_save_points**(*images, num_points, outdir="./", bands="R,G,B"*) +**geospatial.napari_save_points**(*images, num_points, outdir="./", bands="R,G,B", block=True, show=True*) - **Parameters:** - images - Either a list of image paths or a directory where images are stored. Each image will be opened in sequence. - num_points - Integer of expected number of clicked points. For warping downstream, this should be minimally 4. - outdir - Directory to save output text files with saved points for each image. Defaults to "./". - bands - If input images are geotifs, this is the required band order for `plantcv.geospatial.read_geotif`. Not required if image type is not geotif. + - block - True/False whether to stop function from advancing before user closes the viewer window. + - show - True/False whether to show the Napari viewer. Necessary for tests. - **Outputs:** - redo_list - list of images to redo because the number of clicks did not match num_points. Useful if the user makes a mistake and would like to retry for a subset of images. diff --git a/plantcv/geospatial/napari_save_points.py b/plantcv/geospatial/napari_save_points.py index 47ec977..d022f7d 100644 --- a/plantcv/geospatial/napari_save_points.py +++ b/plantcv/geospatial/napari_save_points.py @@ -7,7 +7,7 @@ import os -def napari_save_points(images, num_points, outdir="./", bands="R,G,B"): +def napari_save_points(images, num_points, outdir="./", bands="R,G,B", block=True, show=True): """Opens a set of images one at a time in a Napari window, waits for users to click points and then saves those points to a file with the same name as the image. @@ -17,6 +17,8 @@ def napari_save_points(images, num_points, outdir="./", bands="R,G,B"): the image path is added to redo_list and returned. outdir (str, optional): Directory to save text files with points. Defaults to "./". bands (str, optional): Band list if input images are geotifs. Defaults to "R,G,B". + block (boolean): Whether to stop the function from advancing before user closes the viewer window. + show (boolean): Whether to show the Napari viewer. Necessary for tests. Returns: list: List of images to be redone due to a different number of clicked points than expected. @@ -43,12 +45,12 @@ def napari_save_points(images, num_points, outdir="./", bands="R,G,B"): # Save image name for output file img_name = (image_path.split("/")[-1]).split(".")[:-1] - viewer = napari.Viewer() + viewer = napari.Viewer(show=show) # Add the image and points layer viewer.add_image(image) viewer.add_points(name="points") - viewer.show(block=True) + viewer.show(block=block) # Save file if correct number of points if len(viewer.layers["points"].data) == num_points: From bd3ccdc5968b8b205ec4931cfbd52c5b6d0f4955 Mon Sep 17 00:00:00 2001 From: k034b363 Date: Wed, 4 Dec 2024 16:01:26 -0600 Subject: [PATCH 3/8] add tests for napari_save_points --- plantcv/geospatial/napari_save_points.py | 6 ++--- tests/test_geospatial_napari_save_points.py | 28 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 tests/test_geospatial_napari_save_points.py diff --git a/plantcv/geospatial/napari_save_points.py b/plantcv/geospatial/napari_save_points.py index d022f7d..f29adf0 100644 --- a/plantcv/geospatial/napari_save_points.py +++ b/plantcv/geospatial/napari_save_points.py @@ -12,13 +12,13 @@ def napari_save_points(images, num_points, outdir="./", bands="R,G,B", block=Tru to click points and then saves those points to a file with the same name as the image. Args: - images (list or str): Either a list of image paths or a directory name. + images (list or str): Either a list of image paths or a directory name. num_points (int): Number of points expected. If number of clicks received is different, the image path is added to redo_list and returned. outdir (str, optional): Directory to save text files with points. Defaults to "./". bands (str, optional): Band list if input images are geotifs. Defaults to "R,G,B". - block (boolean): Whether to stop the function from advancing before user closes the viewer window. - show (boolean): Whether to show the Napari viewer. Necessary for tests. + block (boolean): Whether to stop the function from advancing before user closes the viewer window. + show (boolean): Whether to show the Napari viewer. Necessary for tests. Returns: list: List of images to be redone due to a different number of clicked points than expected. diff --git a/tests/test_geospatial_napari_save_points.py b/tests/test_geospatial_napari_save_points.py new file mode 100644 index 0000000..1ea55a2 --- /dev/null +++ b/tests/test_geospatial_napari_save_points.py @@ -0,0 +1,28 @@ +"""Tests for geospatial.napari_save_points""" + +import pytest +import os +import napari +from plantcv.geospatial import napari_save_points, read_geotif +from plantcv.plantcv import print_image + +def test_geospatial_napari_save_points(test_data, tmpdir): + """Test for plantcv-geospatial.""" + cache_dir = tmpdir.mkdir("cache") + images = [test_data.rgb_tif] + redo_list = napari_save_points(images, num_points=4, outdir=cache_dir, block=False, show=False) + assert len(redo_list) == 1 + viewer.close() + + + +def test_geospatial_napari_save_points_output(test_data, tmpdir): + """Test for plantcv-geospatial.""" + cache_dir = tmpdir.mkdir("cache") + # test with a png by creating a temp version of the rgb tif in cache_dir + img = read_geotif(filename=test_data.rgb_tif, bands="R,G,B") + print_image(img.pseudo_rgb, os.path.join(cache_dir, "rgb.png")) + images = [os.path.join(cache_dir, "rgb.png"] + redo_list = napari_save_points(images, num_points=0, outdir=cache_dir, block=False, show=False) + assert os.path.exists(os.path.join(cache_dir, "rgb_warp.txt")) + viewer.close() From 90160d22c3269287062b602d611954e2c615d831 Mon Sep 17 00:00:00 2001 From: k034b363 Date: Wed, 4 Dec 2024 16:07:19 -0600 Subject: [PATCH 4/8] Fix unmatched parentheses in test --- tests/test_geospatial_napari_save_points.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_geospatial_napari_save_points.py b/tests/test_geospatial_napari_save_points.py index 1ea55a2..0de326f 100644 --- a/tests/test_geospatial_napari_save_points.py +++ b/tests/test_geospatial_napari_save_points.py @@ -1,11 +1,10 @@ """Tests for geospatial.napari_save_points""" -import pytest import os -import napari from plantcv.geospatial import napari_save_points, read_geotif from plantcv.plantcv import print_image + def test_geospatial_napari_save_points(test_data, tmpdir): """Test for plantcv-geospatial.""" cache_dir = tmpdir.mkdir("cache") @@ -15,14 +14,13 @@ def test_geospatial_napari_save_points(test_data, tmpdir): viewer.close() - def test_geospatial_napari_save_points_output(test_data, tmpdir): """Test for plantcv-geospatial.""" cache_dir = tmpdir.mkdir("cache") # test with a png by creating a temp version of the rgb tif in cache_dir img = read_geotif(filename=test_data.rgb_tif, bands="R,G,B") print_image(img.pseudo_rgb, os.path.join(cache_dir, "rgb.png")) - images = [os.path.join(cache_dir, "rgb.png"] - redo_list = napari_save_points(images, num_points=0, outdir=cache_dir, block=False, show=False) + images = [os.path.join(cache_dir, "rgb.png")] + _ = napari_save_points(images, num_points=0, outdir=cache_dir, block=False, show=False) assert os.path.exists(os.path.join(cache_dir, "rgb_warp.txt")) viewer.close() From c5ad26556840e0784f18ac889a5062e6e266f634 Mon Sep 17 00:00:00 2001 From: k034b363 Date: Thu, 5 Dec 2024 11:00:43 -0600 Subject: [PATCH 5/8] Change viewer close in napari save points --- plantcv/geospatial/napari_save_points.py | 3 +++ tests/test_geospatial_napari_save_points.py | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plantcv/geospatial/napari_save_points.py b/plantcv/geospatial/napari_save_points.py index f29adf0..cf9efc0 100644 --- a/plantcv/geospatial/napari_save_points.py +++ b/plantcv/geospatial/napari_save_points.py @@ -62,6 +62,9 @@ def napari_save_points(images, num_points, outdir="./", bands="R,G,B", block=Tru redo_list.append(image_path) warn('Image ' + str(image_path) + ' collected incorrect number of points. ' + 'Added to redo list.') + # Close the viewer in case it wasn't shown + if not show: + viewer.close() # Reset debug pcv.params.debug = debug return redo_list diff --git a/tests/test_geospatial_napari_save_points.py b/tests/test_geospatial_napari_save_points.py index 0de326f..d9a4ace 100644 --- a/tests/test_geospatial_napari_save_points.py +++ b/tests/test_geospatial_napari_save_points.py @@ -11,7 +11,6 @@ def test_geospatial_napari_save_points(test_data, tmpdir): images = [test_data.rgb_tif] redo_list = napari_save_points(images, num_points=4, outdir=cache_dir, block=False, show=False) assert len(redo_list) == 1 - viewer.close() def test_geospatial_napari_save_points_output(test_data, tmpdir): @@ -23,4 +22,3 @@ def test_geospatial_napari_save_points_output(test_data, tmpdir): images = [os.path.join(cache_dir, "rgb.png")] _ = napari_save_points(images, num_points=0, outdir=cache_dir, block=False, show=False) assert os.path.exists(os.path.join(cache_dir, "rgb_warp.txt")) - viewer.close() From c29784e5f216827559f644924db0817f1f89876a Mon Sep 17 00:00:00 2001 From: k034b363 Date: Thu, 5 Dec 2024 11:10:43 -0600 Subject: [PATCH 6/8] Try to fix tests not liking a blocked napari window --- plantcv/geospatial/napari_save_points.py | 9 +++++---- tests/test_geospatial_napari_save_points.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/plantcv/geospatial/napari_save_points.py b/plantcv/geospatial/napari_save_points.py index cf9efc0..f0990fc 100644 --- a/plantcv/geospatial/napari_save_points.py +++ b/plantcv/geospatial/napari_save_points.py @@ -7,7 +7,7 @@ import os -def napari_save_points(images, num_points, outdir="./", bands="R,G,B", block=True, show=True): +def napari_save_points(images, num_points, outdir="./", bands="R,G,B", show_window=True): """Opens a set of images one at a time in a Napari window, waits for users to click points and then saves those points to a file with the same name as the image. @@ -45,12 +45,13 @@ def napari_save_points(images, num_points, outdir="./", bands="R,G,B", block=Tru # Save image name for output file img_name = (image_path.split("/")[-1]).split(".")[:-1] - viewer = napari.Viewer(show=show) + viewer = napari.Viewer(show=show_window) # Add the image and points layer viewer.add_image(image) viewer.add_points(name="points") - viewer.show(block=block) + if show_window: + viewer.show(block=True) # Save file if correct number of points if len(viewer.layers["points"].data) == num_points: @@ -63,7 +64,7 @@ def napari_save_points(images, num_points, outdir="./", bands="R,G,B", block=Tru warn('Image ' + str(image_path) + ' collected incorrect number of points. ' + 'Added to redo list.') # Close the viewer in case it wasn't shown - if not show: + if not show_window: viewer.close() # Reset debug pcv.params.debug = debug diff --git a/tests/test_geospatial_napari_save_points.py b/tests/test_geospatial_napari_save_points.py index d9a4ace..00b59ed 100644 --- a/tests/test_geospatial_napari_save_points.py +++ b/tests/test_geospatial_napari_save_points.py @@ -9,7 +9,7 @@ def test_geospatial_napari_save_points(test_data, tmpdir): """Test for plantcv-geospatial.""" cache_dir = tmpdir.mkdir("cache") images = [test_data.rgb_tif] - redo_list = napari_save_points(images, num_points=4, outdir=cache_dir, block=False, show=False) + redo_list = napari_save_points(images, num_points=4, outdir=cache_dir, show=False) assert len(redo_list) == 1 @@ -20,5 +20,5 @@ def test_geospatial_napari_save_points_output(test_data, tmpdir): img = read_geotif(filename=test_data.rgb_tif, bands="R,G,B") print_image(img.pseudo_rgb, os.path.join(cache_dir, "rgb.png")) images = [os.path.join(cache_dir, "rgb.png")] - _ = napari_save_points(images, num_points=0, outdir=cache_dir, block=False, show=False) + _ = napari_save_points(images, num_points=0, outdir=cache_dir, show=False) assert os.path.exists(os.path.join(cache_dir, "rgb_warp.txt")) From 8b695880949785d10e9f3dd9bed6cc8159e6d011 Mon Sep 17 00:00:00 2001 From: k034b363 Date: Thu, 5 Dec 2024 11:13:06 -0600 Subject: [PATCH 7/8] Change parameter name in napari save points test --- tests/test_geospatial_napari_save_points.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_geospatial_napari_save_points.py b/tests/test_geospatial_napari_save_points.py index 00b59ed..bdfab42 100644 --- a/tests/test_geospatial_napari_save_points.py +++ b/tests/test_geospatial_napari_save_points.py @@ -9,7 +9,7 @@ def test_geospatial_napari_save_points(test_data, tmpdir): """Test for plantcv-geospatial.""" cache_dir = tmpdir.mkdir("cache") images = [test_data.rgb_tif] - redo_list = napari_save_points(images, num_points=4, outdir=cache_dir, show=False) + redo_list = napari_save_points(images, num_points=4, outdir=cache_dir, show_viewer=False) assert len(redo_list) == 1 @@ -20,5 +20,5 @@ def test_geospatial_napari_save_points_output(test_data, tmpdir): img = read_geotif(filename=test_data.rgb_tif, bands="R,G,B") print_image(img.pseudo_rgb, os.path.join(cache_dir, "rgb.png")) images = [os.path.join(cache_dir, "rgb.png")] - _ = napari_save_points(images, num_points=0, outdir=cache_dir, show=False) + _ = napari_save_points(images, num_points=0, outdir=cache_dir, show_viewer=False) assert os.path.exists(os.path.join(cache_dir, "rgb_warp.txt")) From 350b2c28759608f76357a880731888a56fef4c2e Mon Sep 17 00:00:00 2001 From: k034b363 Date: Thu, 5 Dec 2024 11:16:28 -0600 Subject: [PATCH 8/8] Change parameter name in test for napari_save_points --- tests/test_geospatial_napari_save_points.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_geospatial_napari_save_points.py b/tests/test_geospatial_napari_save_points.py index bdfab42..9eac50a 100644 --- a/tests/test_geospatial_napari_save_points.py +++ b/tests/test_geospatial_napari_save_points.py @@ -9,7 +9,7 @@ def test_geospatial_napari_save_points(test_data, tmpdir): """Test for plantcv-geospatial.""" cache_dir = tmpdir.mkdir("cache") images = [test_data.rgb_tif] - redo_list = napari_save_points(images, num_points=4, outdir=cache_dir, show_viewer=False) + redo_list = napari_save_points(images, num_points=4, outdir=cache_dir, show_window=False) assert len(redo_list) == 1 @@ -20,5 +20,5 @@ def test_geospatial_napari_save_points_output(test_data, tmpdir): img = read_geotif(filename=test_data.rgb_tif, bands="R,G,B") print_image(img.pseudo_rgb, os.path.join(cache_dir, "rgb.png")) images = [os.path.join(cache_dir, "rgb.png")] - _ = napari_save_points(images, num_points=0, outdir=cache_dir, show_viewer=False) + _ = napari_save_points(images, num_points=0, outdir=cache_dir, show_window=False) assert os.path.exists(os.path.join(cache_dir, "rgb_warp.txt"))