diff --git a/docs/changelog.md b/docs/changelog.md index 72803a0..3100471 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", block=True, show=True*) + #### 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..a356401 --- /dev/null +++ b/docs/napari_save_points.md @@ -0,0 +1,41 @@ +## 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", 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. + +- **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..f0990fc --- /dev/null +++ b/plantcv/geospatial/napari_save_points.py @@ -0,0 +1,71 @@ +# 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", 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. + + 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". + 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. + """ + # 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(show=show_window) + + # Add the image and points layer + viewer.add_image(image) + viewer.add_points(name="points") + if show_window: + 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.') + # Close the viewer in case it wasn't shown + if not show_window: + 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 new file mode 100644 index 0000000..9eac50a --- /dev/null +++ b/tests/test_geospatial_napari_save_points.py @@ -0,0 +1,24 @@ +"""Tests for geospatial.napari_save_points""" + +import os +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, show_window=False) + assert len(redo_list) == 1 + + +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")] + _ = 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"))