diff --git a/docs/changelog.md b/docs/changelog.md index f5bf5be..72803a0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented below. +#### geospatial.points_to_geojson + +* v0.1dev: **geosptial.points_to_geojson**(*img, viewer, out_path*) + #### geospatial.read_geotif * v0.1dev: spectral = **geospatial.read_geotif**(*filename, bands="B,G,R", cropto=None*) diff --git a/docs/documentation_images/napari_clicks.png b/docs/documentation_images/napari_clicks.png new file mode 100644 index 0000000..731a78a Binary files /dev/null and b/docs/documentation_images/napari_clicks.png differ diff --git a/docs/points_to_geojson.md b/docs/points_to_geojson.md new file mode 100644 index 0000000..d83dedb --- /dev/null +++ b/docs/points_to_geojson.md @@ -0,0 +1,36 @@ +## Output clicked points as a geojson shapefile + +Using a Napari or PlantCV-annotate viewer object with clicked points, output a shapefile. + +**geospatial.points_to_geojson**(*img, viewer, out_path*) + +- **Parameters:** + - img - Spectral image object, likely read in with [`geo.read_geotif`](read_geotif.md) + - viewer - Napari viewer class object, possible created with PlantCV-Annotate. + - out_path - Path to save the geojson shapefile. Must be ".geojson" file type. + +- **Context:** + - Saved points can be used downstream for generating circular ROIs or circles for use with rasterstats. +- **Example use:** + - below to click plant locations + + +```python +import plantcv.geospatial as geo +import plantcv.annotate as an + +# Read geotif in +img = geo.read_geotif("../read_geotif/rgb.tif", bands="R,G,B") +viewer = an.napari_open(img=img.pseudo_rgb) +viewer.add_points() + +# A napari viewer window will pop up, use the points function to add clicks +``` +```python +# In a separate cell, save the output after clicking: +geo.points_to_geojson(img, viewer, out_path="./points_example.geojson") +``` + +![Screenshot](documentation_images/napari_clicks.png) + +**Source Code:** [Here](https://github.com/danforthcenter/plantcv-geospatial/blob/main/plantcv/geospatial/points_to_geojson.py) \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index cd22236..2d49917 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,6 +20,7 @@ nav: - Read Geo-tif Data: read_geotif.md - Transform coordinate points: transform_points.md - Transform coordinate polygons: transform_polygons.md + - Save clicked points as geojson: points_to_geojson.md markdown_extensions: - toc: permalink: True diff --git a/plantcv/geospatial/__init__.py b/plantcv/geospatial/__init__.py index b4af454..f6ef044 100644 --- a/plantcv/geospatial/__init__.py +++ b/plantcv/geospatial/__init__.py @@ -2,6 +2,7 @@ from plantcv.geospatial.read_geotif import read_geotif 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 # Auto versioning __version__ = version("plantcv-geospatial") @@ -9,5 +10,6 @@ __all__ = [ "read_geotif", "transform_points", - "transform_polygons" + "transform_polygons", + "points_to_geojson" ] diff --git a/plantcv/geospatial/points_to_geojson.py b/plantcv/geospatial/points_to_geojson.py new file mode 100644 index 0000000..f64eb86 --- /dev/null +++ b/plantcv/geospatial/points_to_geojson.py @@ -0,0 +1,41 @@ +# Save clicked points from Napari or PlantCV-annotate as a geojson points file. + +import geojson +import rasterio +from plantcv.plantcv import fatal_error + + +def points_to_geojson(img, viewer, out_path): + """Use clicks from a Napari or plantcv-annotate viewer to output a geojson shapefile. + + Parameters + ---------- + img : PlantCV spectral_data class object + The image used for clicking on points, should be from read_geotif. + viewer: Napari viewer class object or plantcv-annotate Points class object. + The viewer used to make the clicks. + out_path : str + Path to save to shapefile. Must have "geojson" file extension + """ + # Napari output, points must be reversed + if hasattr(viewer, 'layers'): + points = [(img.metadata["transform"]*reversed(i)) for i in viewer.layers["Points"].data] + # Annotate output + elif hasattr(viewer, 'coords'): + points = [(img.metadata["transform"]*i) for i in viewer.coords['default']] + else: + fatal_error("Viewer class type not recognized. Currently, Napari and PlantCV-annotate viewers supported.") + features = [geojson.Feature(geometry=geojson.Point((lon, lat))) for lon, lat in points] + feature_collection = geojson.FeatureCollection(features) + # Make sure the coordinate system is the same as the original image + feature_collection['crs'] = { + "type": "name", + "properties": { + "name": rasterio.crs.CRS.to_string(img.metadata["crs"]) + } + } + if ".geojson" in out_path: + with open(out_path, 'w') as f: + geojson.dump(feature_collection, f) + else: + fatal_error("File type not supported.") diff --git a/pyproject.toml b/pyproject.toml index 7617ecb..857d7cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,8 @@ dependencies = [ "rasterio", "fiona", "shapely", + "geojson", + "napari", ] requires-python = ">=3.6" authors = [ diff --git a/tests/test_geospatial_points_to_geojson.py b/tests/test_geospatial_points_to_geojson.py new file mode 100644 index 0000000..d06d1fe --- /dev/null +++ b/tests/test_geospatial_points_to_geojson.py @@ -0,0 +1,54 @@ +"""Tests for geospatial.points_to_geojson""" + +import pytest +import os +import napari +from plantcv.geospatial import read_geotif +from plantcv.geospatial import points_to_geojson + +# Set up fake class just for testing the annotate output +# Don't want to have to have annotate as a dependency + +class FakePoints: + def __init__(self): + self.coords = {} + +def test_geospatial_points_to_geojson_napari(test_data, tmpdir): + """Test for plantcv-geospatial.""" + cache_dir = tmpdir.mkdir("cache") + img = read_geotif(filename=test_data.rgb_tif, bands="R,G,B") + viewer = napari.Viewer(show=False) + viewer.add_image(img.pseudo_rgb) + viewer.add_points() + filename = os.path.join(cache_dir, 'test_out.geojson') + points_to_geojson(img, viewer, out_path=filename) + assert os.path.exists(filename) + +def test_geospatial_points_to_geojson_an(test_data, tmpdir): + """Test for plantcv-geospatial.""" + cache_dir = tmpdir.mkdir("cache") + img = read_geotif(filename=test_data.rgb_tif, bands="R,G,B") + viewer = FakePoints() + viewer.coords["default"] = [] + filename = os.path.join(cache_dir, 'test_out.geojson') + points_to_geojson(img, viewer, out_path=filename) + assert os.path.exists(filename) + +def test_geospatial_points_to_geojson_badviewer(test_data, tmpdir): + """Test for plantcv-geospatial.""" + cache_dir = tmpdir.mkdir("cache") + img = read_geotif(filename=test_data.rgb_tif, bands="R,G,B") + viewer = [] + filename = os.path.join(cache_dir, 'test_out.geojson') + with pytest.raises(RuntimeError): + points_to_geojson(img, viewer, out_path=filename) + +def test_geospatial_points_to_geojson_badfilename(test_data, tmpdir): + """Test for plantcv-geospatial.""" + cache_dir = tmpdir.mkdir("cache") + img = read_geotif(filename=test_data.rgb_tif, bands="R,G,B") + viewer = FakePoints() + viewer.coords["default"] = [] + filename = os.path.join(cache_dir, 'test_out.txt') + with pytest.raises(RuntimeError): + points_to_geojson(img, viewer, out_path=filename)