diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..4c3586e --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +[run] +omit = + config.py + config-3.py diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index e0904dc..f64c7e5 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -21,7 +21,7 @@ jobs: env: OS: ${{ matrix.os }} PYTHON: ${{ matrix.python-version }} - + DISPLAY: ':99.0' steps: - uses: actions/checkout@main - name: Set up Python ${{ matrix.python-version }} @@ -30,19 +30,27 @@ 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 xvfb python -m pip install --upgrade pip - pip install flake8 pytest pytest-cov ipython + pip install flake8 pytest pytest-cov pytest-qt pytest-xvfb ipython anyio - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test and generate coverage report + - uses: tlambert03/setup-qt-libs@v1 + - name: Install plantcv-annotate # Run coverage analysis on pytest tests run: | pip install . - py.test --cov-report=xml --cov=./ + pip uninstall -y opencv-python + pip install opencv-python-headless + - name: Tests + uses: aganders3/headless-gui@v2 + with: + run: pytest --cov-report=xml --cov=./ - name: Upload coverage to Deepsource uses: deepsourcelabs/test-coverage-action@master with: diff --git a/docs/Points.md b/docs/Points.md index a1fd3a4..0d1d169 100644 --- a/docs/Points.md +++ b/docs/Points.md @@ -1,6 +1,6 @@ ## Interactive Point Annotation Tool -Using [Jupyter Notebooks](jupyter.md) it is possible to interactively click to collect coordinates from an image, which can be used in various downstream applications. Left click on the image to collect a point. Right click removes the +Using [Jupyter Notebooks](https://plantcv.readthedocs.io/en/stable/jupyter/) it is possible to interactively click to collect coordinates from an image, which can be used in various downstream applications. Left click on the image to collect a point. Right click removes the closest collected point. **plantcv.annotate.Points**(*img, figsize=(12,6), label="dafault"*) diff --git a/docs/img/documentation_images/napari_join_labels/1_labeled_mask.png b/docs/img/documentation_images/napari_join_labels/1_labeled_mask.png new file mode 100644 index 0000000..f6df4db Binary files /dev/null and b/docs/img/documentation_images/napari_join_labels/1_labeled_mask.png differ diff --git a/docs/img/documentation_images/napari_label_classes/napari_label_classes.png b/docs/img/documentation_images/napari_label_classes/napari_label_classes.png new file mode 100644 index 0000000..1b63380 Binary files /dev/null and b/docs/img/documentation_images/napari_label_classes/napari_label_classes.png differ diff --git a/docs/img/documentation_images/napari_open/napari_open.png b/docs/img/documentation_images/napari_open/napari_open.png new file mode 100644 index 0000000..523fe02 Binary files /dev/null and b/docs/img/documentation_images/napari_open/napari_open.png differ diff --git a/docs/napari_classes.md b/docs/napari_classes.md new file mode 100644 index 0000000..b98966f --- /dev/null +++ b/docs/napari_classes.md @@ -0,0 +1,32 @@ +## Open Image with Napari + +Get class names from Napari Viewer Object. + +**plantcv.annotate.napari_classes**(*viewer*) + +**returns** list of napari classes + +- **Parameters:** + - viewer - Napari viewer object + +- **Context:** + - Get names of Napari classes. This is mainly an internal function but can be useful in other context. + +- **Example use:** + - Get names of Napari classes/labels. + + +```python +import plantcv.plantcv as pcv +import plantcv.annotate as pcvan + +# 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']) + +classes = pcvan.napari_classes(viewer) + +``` + +**Source Code:** [Here](https://github.com/danforthcenter/plantcv-annotate/blob/main/plantcv/annotate/napari_classes.py) diff --git a/docs/napari_join_labels.md b/docs/napari_join_labels.md new file mode 100644 index 0000000..b8cd44e --- /dev/null +++ b/docs/napari_join_labels.md @@ -0,0 +1,45 @@ +## Join Labels with Napari + +This function joins classes with the same label. This function would be run after classes are labeled with napari_label_classes. + +**plantcv.annotate.napari_join_labels*(*img, viewer*) + +**returns** relabeled mask, dictionary of masks for each class + +- **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) + - viewer - viewer with labeled classes. If no points are selected for a class, + data without labels will default to this class when napari_join_labels + is run. If all classes have points labeled, any clusters not labeled + will default to the last class in the list if napari_join_labels is + run. + +- **Context:** + - This function would be run after labeling classes in Napari is complete. + +- **Example use:** + - Joining classes labeled as the same, for example for joining classes from output of kmeans clustering + + +```python +import plantcv.plantcv as pcv +import plantcv.annotate as pcvan +import napari + +# Create an instance of the Points class +img, path, name = pcv.readimage("./grayimg.png") + +viewer = pcvan.napari_label_classes(img=img, ['background', 'wing','seed']) + +# 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) + +![Screenshot](img/documentation_images/napari_join_labels/1_labeled_mask.png) + + +**Source Code:** [Here](https://github.com/danforthcenter/plantcv-annotate/blob/main/plantcv/annotate/napari_label_classes.py) diff --git a/docs/napari_label_classes.md b/docs/napari_label_classes.md new file mode 100644 index 0000000..6b112af --- /dev/null +++ b/docs/napari_label_classes.md @@ -0,0 +1,43 @@ +## 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 annotated as long as viewer is open. + +**plantcv.annotate.napari_label_classes*(*img, classes, 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. If no points are selected for a class, + data without labels will default to this class when napari_join_labels + is run. If all classes have points labeled, any clusters not labeled + will default to the last class in the list if napari_join_labels is + run. + - 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). + +- **Example use:** + - Labeling output of kmeans clustering into classes. Labeling points. + + +```python +import plantcv.plantcv as pcv +import plantcv.annotate as pcvan +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']) + +# Should open interactive napari viewer + +``` + +![Screenshot](img/documentation_images/napari_label_classes/napari_label_classes.png) + + +**Source Code:** [Here](https://github.com/danforthcenter/plantcv-annotate/blob/main/plantcv/annotate/napari_label_classes.py) diff --git a/docs/napari_open.md b/docs/napari_open.md new file mode 100644 index 0000000..c699a85 --- /dev/null +++ b/docs/napari_open.md @@ -0,0 +1,36 @@ +## 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. + +**plantcv.annotate.napari_open**(*img, 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) + - show - if show = True, viewer is launched. False setting is useful for test purposes. + +- **Context:** + - Used to open image data with Napari. + +- **Example use:** + - Open image data to annotate it with other Napari functions (e.g. napari_label_classes) + + +```python +import plantcv.plantcv as pcv +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) + +# Should open interactive napari viewer + +``` + +![Screenshot](img/documentation_images/napari_open/napari_open.png) + + +**Source Code:** [Here](https://github.com/danforthcenter/plantcv-annotate/blob/main/plantcv/annotate/napari_open.py) diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..a6d4838 --- /dev/null +++ b/environment.yml @@ -0,0 +1,33 @@ +# run: conda env create --file environment.yml +# optionally, change channel name with -n {plantcv-dev} +name: plantcv +dependencies: + - python=3.10 + - matplotlib>=1.5 + - numpy>=1.11 + - pandas + - python-dateutil + - scipy + - scikit-image>=0.19 + - scikit-learn + - dask + - dask-jobqueue + - opencv + - statsmodels + - xarray>=2022.11.0 + - mkdocs + - pytest + - pytest-cov + - flake8 + - ipympl + - nodejs + - jupyterlab + - altair + - vl-convert-python + - napari + - pyqt + - pytest-qt + +channels: + - conda-forge + - defaults diff --git a/mkdocs.yml b/mkdocs.yml index 649a459..eff0192 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,6 +17,10 @@ nav: - 'Adding/Editing Documentation': documentation.md - 'PlantCV Namespace': - 'Annotation Tools': + - Napari Classes: napari_classes.md + - Napari Join: napari_join_labels.md + - Napari Label: napari_label_classes.md + - Napari Open: napari_open.md - Points: Points.md - Get Centroids: get_centroids.md markdown_extensions: diff --git a/plantcv/annotate/__init__.py b/plantcv/annotate/__init__.py index 2c3ef6e..395de61 100644 --- a/plantcv/annotate/__init__.py +++ b/plantcv/annotate/__init__.py @@ -1,11 +1,19 @@ from importlib.metadata import version from plantcv.annotate.classes import Points from plantcv.annotate.get_centroids import get_centroids +from plantcv.annotate.napari_classes import napari_classes +from plantcv.annotate.napari_open import napari_open +from plantcv.annotate.napari_label_classes import napari_label_classes +from plantcv.annotate.napari_join_labels import napari_join_labels # Auto versioning __version__ = version("plantcv-annotate") __all__ = [ "Points", - "get_centroids" + "get_centroids", + "napari_classes", + "napari_open", + "napari_label_classes", + "napari_join_labels" ] diff --git a/plantcv/annotate/napari_classes.py b/plantcv/annotate/napari_classes.py new file mode 100755 index 0000000..77067ff --- /dev/null +++ b/plantcv/annotate/napari_classes.py @@ -0,0 +1,31 @@ +# Get Napari Keys + +import re + + +def napari_classes(viewer): + """ + get names of napari keys + + Inputs: + viewer = napari viewer object + + Returns: + classes = napari class value names + + :param viewer: napari.viewer.Viewer + :return labels: numpy.ndarray, list + """ + keylist = list(viewer.layers) + keylist = ''.join(str(keylist)) + keylist = keylist.split(',') + + classes = [] + for x in keylist: + if re.search('Image layer', x): + pass + else: + y = x.split(" ") + classes.append(y[3].strip("\'")) + + return classes diff --git a/plantcv/annotate/napari_join_labels.py b/plantcv/annotate/napari_join_labels.py new file mode 100755 index 0000000..1848258 --- /dev/null +++ b/plantcv/annotate/napari_join_labels.py @@ -0,0 +1,100 @@ +# Use Napari to Label + +import numpy as np +import os +from plantcv.plantcv.warn import warn +from plantcv.annotate import napari_classes +from plantcv.plantcv import params +from plantcv.plantcv._debug import _debug +from skimage.color import label2rgb + + +def napari_join_labels(img, viewer): + """ + Join classes with the same label + + Inputs: + img = img (grayimg, rgbimg, or hyperspectral image array data + e.g. hyperspectraldata.array_data). Adding labels works best on an + image that has objects segmented/classified with contours/clusters + 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 option is useful for + running tests without triggering the viewer. + + Returns: + labeled_img = labeled image + mask_dict = dictionary of masks; mask for each class + + :param img: numpy.ndarray + :param viewer: Napari Viewer object + :return labeled_img: numpy.ndarray + :return mask_dict: dict of numpy.ndarray + + """ + classes = napari_classes(viewer) + lastclassvalue = len(classes) + + allmask = np.zeros((np.shape(img))) + maskdict = {} + valuesused = [] + classvalue = [] + + for i, classname in enumerate(classes): + data = viewer.layers[str(classname)].data + + if len(data) == 0: + classfinal = np.zeros((np.shape(img))) + classfinal = np.where(allmask == 0, classfinal == 0, + classfinal == 255) + totalmask = (classfinal.astype(int))*(i+1) + dictmask = classfinal.astype(int) + key = str(classname) + keylayer = key+"_layer" + maskdict.update({key: dictmask}) + allmask = np.add(allmask, totalmask) + viewer.add_labels(dictmask, blending='additive', name=keylayer) + else: + classmask = np.zeros((np.shape(img))) + classmask1 = np.zeros((np.shape(img))) + classfinal = np.zeros((np.shape(img))) + for point in data: + x = int(point[0]) + y = int(point[1]) + value = img[x, y] + if value in valuesused: + index = np.where(valuesused == value)[0] + if classvalue[index] != classname: + warning = ("A cluster in "+str(classname) + + " has been previously labeled as " + + str(classvalue[index]) + + ".Check point at position "+str(x)+"," + + str(y)) + warn(warning) + valuesused = np.append(valuesused, value) + classvalue = np.append(classvalue, classname) + classmask[img == value] = 1 + classmask1 = np.add(classmask, classmask1) + classfinal = np.where(classmask1 != 0, classfinal == 0, + classfinal == 255) + totalmask = (classfinal.astype(int))*(i+1) + dictmask = classfinal.astype(int) + key = str(classname) + keylayer = key+"_layer" + maskdict.update({key: dictmask}) + allmask[totalmask == (i+1)] = i+1 + viewer.add_labels(dictmask, blending='additive', name=keylayer) + + # set any zero values to last class value + allmask[allmask == 0] = lastclassvalue + if params.debug == 'print': + colorful = label2rgb(allmask) + outputimg = (255*colorful).astype(np.uint8) + _debug(visual=outputimg, filename=os.path.join(params.debug_outdir, + str(params.device) + + '_labeled_mask.png')) + else: + _debug(visual=allmask) + + return allmask, maskdict diff --git a/plantcv/annotate/napari_label_classes.py b/plantcv/annotate/napari_label_classes.py new file mode 100755 index 0000000..81e37dd --- /dev/null +++ b/plantcv/annotate/napari_label_classes.py @@ -0,0 +1,44 @@ +# Use Napari to Label + +import numpy as np +import random +from plantcv.annotate import napari_open + + +def napari_label_classes(img, classes, show=True): + """ + open img in napari and label classes + + Inputs: + img = img (grayimg, rgbimg, or hyperspectral image array data + e.g. hyperspectraldata.array_data). Adding labels works best on an + image that has objects segmented/classified with contours/clusters + labeled with values (e.g. labeled mask, output of kmeans clustering). + classes = list of labels or classes. If no points are selected for a class, + data without labels will default to this class when napari_join_labels + 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 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 + + """ + showcall = show + viewer = napari_open(img, show=showcall) + + symbols = ['arrow', 'clobber', 'cross', 'diamond', 'disc', 'hbar', 'ring', + 'square', 'star', 'tailed_arrow', 'triangle_down', + 'triangle_up', 'vbar'] + + for x in classes: + viewer.add_points(np.array([]), name=x, symbol=random.choice(symbols), + face_color='white', size=10) + + return viewer diff --git a/plantcv/annotate/napari_open.py b/plantcv/annotate/napari_open.py new file mode 100755 index 0000000..59fa317 --- /dev/null +++ b/plantcv/annotate/napari_open.py @@ -0,0 +1,39 @@ +# Use Napari to Label + +import cv2 +import numpy as np +from skimage.color import label2rgb +import napari + + +def napari_open(img, show=True): + """ + 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 option is useful for + running tests without triggering the viewer. + + Returns: + viewer = napari viewer object + + :param img: numpy.ndarray + :return viewer: napari viewer object + + """ + 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 len(shape) == 3: + if shape[2] == 3: + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + if shape[2] > 3: + img = img.transpose(2, 0, 1) + showcall = show + viewer = napari.Viewer(show=showcall) + viewer.add_image(img) + return viewer diff --git a/pyproject.toml b/pyproject.toml index f0e393e..5abfaf1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,8 @@ dynamic = ["version"] dependencies = [ "plantcv", "matplotlib", + "napari", + "PyQt5" ] requires-python = ">=3.6" authors = [ @@ -30,6 +32,7 @@ classifiers = [ test = [ "pytest", "pytest-cov", + "pytest-qt" ] [project.urls] diff --git a/tests/conftest.py b/tests/conftest.py index 1b83096..4023056 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ import pytest import matplotlib + # Disable plotting matplotlib.use("Template") @@ -19,8 +20,13 @@ def __init__(self): 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") + # Kmeans Clustered Gray image + self.kmeans_seed_gray_img = os.path.join(self.datadir, "silphium_seed_labeled_example.png") + # Small Hyperspectral image + self.envi_sample_data = os.path.join(self.datadir, "corn-kernel-hyperspectral.raw") + @pytest.fixture(scope="session") def test_data(): """Test data object for the main PlantCV package.""" - return TestData() \ No newline at end of file + return TestData() diff --git a/tests/test_annotate.py b/tests/test_annotate.py deleted file mode 100644 index 1554fa1..0000000 --- a/tests/test_annotate.py +++ /dev/null @@ -1,7 +0,0 @@ -from plantcv import annotate as an - - -def test_annotate(): - """PlantCV Test""" - assert an.__name__ == "plantcv.annotate" - \ No newline at end of file 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_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'] diff --git a/tests/test_napari_join_labels.py b/tests/test_napari_join_labels.py new file mode 100644 index 0000000..269465d --- /dev/null +++ b/tests/test_napari_join_labels.py @@ -0,0 +1,79 @@ +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 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 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 diff --git a/tests/testdata/corn-kernel-hyperspectral.hdr b/tests/testdata/corn-kernel-hyperspectral.hdr new file mode 100644 index 0000000..6142bc5 --- /dev/null +++ b/tests/testdata/corn-kernel-hyperspectral.hdr @@ -0,0 +1,591 @@ +ENVI +; this file was created using PlantCV version 3.14.1+743.gff91bfd +; original file: 4-22-22_right_same_B73 +interleave = bil +samples = 43 +lines = 31 +bands = 580 +data type = 12 +wavelength units = nm +wavelength = { +366.551, +367.656, +368.76, +369.865, +370.97, +372.076, +373.181, +374.287, +375.393, +376.5, +377.606, +378.713, +379.82, +380.928, +382.035, +383.143, +384.251, +385.36, +386.468, +387.577, +388.686, +389.796, +390.905, +392.015, +393.126, +394.236, +395.347, +396.458, +397.569, +398.68, +399.792, +400.904, +402.016, +403.128, +404.241, +405.354, +406.467, +407.581, +408.695, +409.809, +410.923, +412.037, +413.152, +414.267, +415.382, +416.498, +417.613, +418.729, +419.846, +420.962, +422.079, +423.196, +424.313, +425.431, +426.549, +427.667, +428.785, +429.904, +431.022, +432.141, +433.261, +434.38, +435.5, +436.62, +437.74, +438.861, +439.982, +441.103, +442.224, +443.346, +444.468, +445.59, +446.712, +447.835, +448.958, +450.081, +451.204, +452.328, +453.452, +454.576, +455.7, +456.825, +457.95, +459.075, +460.2, +461.326, +462.452, +463.578, +464.704, +465.831, +466.958, +468.085, +469.212, +470.34, +471.468, +472.596, +473.725, +474.853, +475.982, +477.112, +478.241, +479.371, +480.501, +481.631, +482.761, +483.892, +485.023, +486.154, +487.286, +488.418, +489.55, +490.682, +491.814, +492.947, +494.08, +495.213, +496.347, +497.481, +498.615, +499.749, +500.883, +502.018, +503.153, +504.289, +505.424, +506.56, +507.696, +508.832, +509.969, +511.106, +512.243, +513.38, +514.518, +515.655, +516.794, +517.932, +519.07, +520.209, +521.348, +522.488, +523.627, +524.767, +525.907, +527.048, +528.188, +529.329, +530.47, +531.612, +532.753, +533.895, +535.037, +536.18, +537.322, +538.465, +539.608, +540.752, +541.896, +543.039, +544.184, +545.328, +546.473, +547.618, +548.763, +549.908, +551.054, +552.2, +553.346, +554.493, +555.639, +556.786, +557.934, +559.081, +560.229, +561.377, +562.525, +563.673, +564.822, +565.971, +567.12, +568.27, +569.42, +570.57, +571.72, +572.87, +574.021, +575.172, +576.324, +577.475, +578.627, +579.779, +580.931, +582.084, +583.237, +584.39, +585.543, +586.696, +587.85, +589.004, +590.159, +591.313, +592.468, +593.623, +594.779, +595.934, +597.09, +598.246, +599.402, +600.559, +601.716, +602.873, +604.03, +605.188, +606.346, +607.504, +608.662, +609.821, +610.98, +612.139, +613.298, +614.458, +615.618, +616.778, +617.939, +619.099, +620.26, +621.421, +622.583, +623.744, +624.906, +626.069, +627.231, +628.394, +629.557, +630.72, +631.883, +633.047, +634.211, +635.375, +636.54, +637.704, +638.869, +640.035, +641.2, +642.366, +643.532, +644.698, +645.865, +647.031, +648.198, +649.366, +650.533, +651.701, +652.869, +654.037, +655.206, +656.375, +657.544, +658.713, +659.882, +661.052, +662.222, +663.393, +664.563, +665.734, +666.905, +668.076, +669.248, +670.42, +671.592, +672.764, +673.937, +675.11, +676.283, +677.456, +678.63, +679.804, +680.978, +682.152, +683.327, +684.502, +685.677, +686.852, +688.028, +689.204, +690.38, +691.556, +692.733, +693.91, +695.087, +696.264, +697.442, +698.62, +699.798, +700.976, +702.155, +703.334, +704.513, +705.693, +706.872, +708.052, +709.233, +710.413, +711.594, +712.775, +713.956, +715.137, +716.319, +717.501, +718.683, +719.866, +721.049, +722.232, +723.415, +724.598, +725.782, +726.966, +728.15, +729.335, +730.52, +731.705, +732.89, +734.075, +735.261, +736.447, +737.634, +738.82, +740.007, +741.194, +742.381, +743.569, +744.757, +745.945, +747.133, +748.322, +749.511, +750.7, +751.889, +753.079, +754.268, +755.458, +756.649, +757.839, +759.03, +760.221, +761.413, +762.604, +763.796, +764.988, +766.181, +767.373, +768.566, +769.759, +770.953, +772.147, +773.34, +774.535, +775.729, +776.924, +778.119, +779.314, +780.509, +781.705, +782.901, +784.097, +785.293, +786.49, +787.687, +788.884, +790.082, +791.28, +792.477, +793.676, +794.874, +796.073, +797.272, +798.471, +799.671, +800.87, +802.07, +803.271, +804.471, +805.672, +806.873, +808.074, +809.276, +810.477, +811.679, +812.882, +814.084, +815.287, +816.49, +817.693, +818.897, +820.101, +821.305, +822.509, +823.714, +824.919, +826.124, +827.329, +828.534, +829.74, +830.946, +832.153, +833.359, +834.566, +835.773, +836.981, +838.188, +839.396, +840.604, +841.813, +843.021, +844.23, +845.439, +846.649, +847.858, +849.068, +850.278, +851.489, +852.699, +853.91, +855.121, +856.333, +857.545, +858.756, +859.969, +861.181, +862.394, +863.607, +864.82, +866.033, +867.247, +868.461, +869.675, +870.89, +872.104, +873.319, +874.535, +875.75, +876.966, +878.182, +879.398, +880.614, +881.831, +883.048, +884.265, +885.483, +886.701, +887.919, +889.137, +890.355, +891.574, +892.793, +894.013, +895.232, +896.452, +897.672, +898.892, +900.113, +901.334, +902.555, +903.776, +904.998, +906.219, +907.441, +908.664, +909.886, +911.109, +912.332, +913.556, +914.779, +916.003, +917.227, +918.451, +919.676, +920.901, +922.126, +923.351, +924.577, +925.803, +927.029, +928.255, +929.482, +930.709, +931.936, +933.164, +934.391, +935.619, +936.847, +938.076, +939.304, +940.533, +941.762, +942.992, +944.222, +945.452, +946.682, +947.912, +949.143, +950.374, +951.605, +952.836, +954.068, +955.3, +956.532, +957.765, +958.998, +960.231, +961.464, +962.697, +963.931, +965.165, +966.399, +967.634, +968.869, +970.104, +971.339, +972.574, +973.81, +975.046, +976.282, +977.519, +978.756, +979.993, +981.23, +982.468, +983.705, +984.944, +986.182, +987.42, +988.659, +989.898, +991.138, +992.377, +993.617, +994.857, +996.097, +997.338, +998.579, +999.82, +1001.061, +1002.303, +1003.545, +1004.787, +1006.029, +1007.272, +1008.515, +1009.758, +1011.001, +1012.245, +1013.489, +1014.733, +1015.977, +1017.222, +1018.467, +1019.712, +1020.958, +1022.203, +1023.449, +1024.695, +1025.942, +1027.189, +1028.435, +1029.683, +1030.93, +1032.178, +1033.426, +1034.674, +1035.922, +1037.171, +1038.42, +1039.669, +1040.919, +1042.169, +1043.419, +1044.669, +1045.919, +1047.17, +1048.421 +} \ No newline at end of file diff --git a/tests/testdata/corn-kernel-hyperspectral.raw b/tests/testdata/corn-kernel-hyperspectral.raw new file mode 100644 index 0000000..1ba7ed8 Binary files /dev/null and b/tests/testdata/corn-kernel-hyperspectral.raw differ diff --git a/tests/testdata/silphium_seed_labeled_example.png b/tests/testdata/silphium_seed_labeled_example.png new file mode 100644 index 0000000..fc4dcc6 Binary files /dev/null and b/tests/testdata/silphium_seed_labeled_example.png differ