diff --git a/docs/img/documentation_images/visualize_tile/Tile_1.jpg b/docs/img/documentation_images/visualize_tile/Tile_1.jpg new file mode 100644 index 000000000..2d3d1ef58 Binary files /dev/null and b/docs/img/documentation_images/visualize_tile/Tile_1.jpg differ diff --git a/docs/img/documentation_images/visualize_tile/Tile_2.jpg b/docs/img/documentation_images/visualize_tile/Tile_2.jpg new file mode 100644 index 000000000..cc3f73662 Binary files /dev/null and b/docs/img/documentation_images/visualize_tile/Tile_2.jpg differ diff --git a/docs/img/documentation_images/visualize_tile/Tile_3.jpg b/docs/img/documentation_images/visualize_tile/Tile_3.jpg new file mode 100644 index 000000000..531011c2a Binary files /dev/null and b/docs/img/documentation_images/visualize_tile/Tile_3.jpg differ diff --git a/docs/img/documentation_images/visualize_tile/Tile_4.jpg b/docs/img/documentation_images/visualize_tile/Tile_4.jpg new file mode 100644 index 000000000..00c4865e9 Binary files /dev/null and b/docs/img/documentation_images/visualize_tile/Tile_4.jpg differ diff --git a/docs/img/documentation_images/visualize_tile/Tile_output.jpg b/docs/img/documentation_images/visualize_tile/Tile_output.jpg new file mode 100644 index 000000000..2b9aedd87 Binary files /dev/null and b/docs/img/documentation_images/visualize_tile/Tile_output.jpg differ diff --git a/docs/updating.md b/docs/updating.md index ca81a890f..957b1335f 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -1344,6 +1344,11 @@ pages for more details on the input and output variable types. * pre v4.0: NA * post v4.0: fig, ax = **pcv.visualize.pixel_scatter_plot**(*paths_to_imgs, x_channel, y_channel*) +#### plantcv.visualize.tile + +* pre v4.4: NA +* post v4.4: tile_img = **pcv.visualize.tile**(*img_list, ncol*) + #### plantcv.visualize.time_lapse_video * pre v4.0: NA diff --git a/docs/visualize_tile.md b/docs/visualize_tile.md new file mode 100644 index 000000000..fe813ab19 --- /dev/null +++ b/docs/visualize_tile.md @@ -0,0 +1,46 @@ +## Visualize composite of image tiles + +This is a plotting method used to examine several output versions, such as from different model fits with varying parameters, all at once. + +**plantcv.visualize.tile**(*images, ncol*) + +**returns** comp_img + +- **Parameters:** + - images - A list of numpy arrays to tile into a composite. + - ncol - Number of columns in composite output. Number of rows is calculated from the number of input images. + +- **Example use:** + - Below + + +```python + +from plantcv import plantcv as pcv +import os + +# Read in a list of images +images = [] +for i in os.listdir("./test_images/"): + images.append(pcv.readimage("./test_images/"+i)[0]) + +# Examine all images at once +composite = pcv.visualize.tile(images=images, ncol=2) + +``` + +**Input images** + +![Screenshot](img/documentation_images/visualize_tile/Tile_1.jpg) + +![Screenshot](img/documentation_images/visualize_tile/Tile_2.jpg) + +![Screenshot](img/documentation_images/visualize_tile/Tile_3.jpg) + +![Screenshot](img/documentation_images/visualize_tile/Tile_4.jpg) + +**Output** + +![Screenshot](img/documentation_images/visualize_tile/Tile_output.jpg) + +**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/main/plantcv/plantcv/visualize/tile.py) \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 62b1cd734..c4e0cb9a2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -209,6 +209,7 @@ nav: - 'Object Sizes': visualize_obj_sizes.md - 'Pixel Scatter Plot': 'visualize_pixel_scatter_vis.md' - 'Pseudocolor': visualize_pseudocolor.md + - 'Tile': visualize_tile.md - 'Time Lapse Video': visualize_time_lapse_video.md - 'Watershed Segmentation': watershed.md - 'White balance': white_balance.md diff --git a/plantcv/plantcv/visualize/__init__.py b/plantcv/plantcv/visualize/__init__.py index d73c10486..c304a09e6 100644 --- a/plantcv/plantcv/visualize/__init__.py +++ b/plantcv/plantcv/visualize/__init__.py @@ -11,7 +11,8 @@ from plantcv.plantcv.visualize.hyper_histogram import hyper_histogram from plantcv.plantcv.visualize.pixel_scatter_vis import pixel_scatter_plot from plantcv.plantcv.visualize.chlorophyll_fluorescence import chlorophyll_fluorescence +from plantcv.plantcv.visualize.tile import tile __all__ = ["pseudocolor", "colorize_masks", "histogram", "colorspaces", "auto_threshold_methods", "overlay_two_imgs", "colorize_label_img", "obj_size_ecdf", "obj_sizes", "hyper_histogram", - "pixel_scatter_plot", "time_lapse_video", "chlorophyll_fluorescence"] + "pixel_scatter_plot", "time_lapse_video", "chlorophyll_fluorescence", "tile"] diff --git a/plantcv/plantcv/visualize/tile.py b/plantcv/plantcv/visualize/tile.py new file mode 100644 index 000000000..1574703c1 --- /dev/null +++ b/plantcv/plantcv/visualize/tile.py @@ -0,0 +1,88 @@ +# Tile output images in a plot to visualize all at once + +import cv2 +import numpy as np +import os +from plantcv.plantcv import params +from plantcv.plantcv._debug import _debug + + +def _row_resize(row, ncol): + """Resizes and concatenates objects in a row. + + Parameters + ---------- + row : list of numpy.ndarray + List of images to concatenate. + ncol : int + Number of columns in desired composite image. + + Returns + ------- + numpy.ndarray + Image concatenated horizontally. + """ + h_min = min(img.shape[0] for img in row) + # Resizing each image so they're the same + row_resize = [cv2.resize(img, (int(img.shape[1] * h_min / img.shape[0]), h_min), + interpolation=cv2.INTER_CUBIC) for img in row] + # Add empty images to the end of the row so things still stay the same size + while len(row_resize) < ncol: + row_resize.append(np.zeros(row_resize[0].shape, dtype=np.uint8)) + # Concatenate horizontally + return cv2.hconcat(row_resize) + + +# Same as _row_resize but for columns +def _col_resize(col): + """Resized and concatenates objects in a column. + + Parameters + ---------- + col : list of numpy.ndarray + List of images to concatenate vertically. + + Returns + ------- + numpy.ndarray + Image concatenated vertically. + """ + w_min = min(img.shape[1] for img in col) + col_resize = [cv2.resize(img, (w_min, int(img.shape[0] * w_min / img.shape[1])), + interpolation=cv2.INTER_CUBIC) for img in col] + return cv2.vconcat(col_resize) + + +# The function that does the tiling +def tile(images, ncol): + """Tile a list of images into a composite with given dimensions. + + Parameters + ---------- + images : list of numpy.ndarray + List of images to tile. + ncol : int + Number of columns in desired composite image. + + Returns + ------- + numpy.ndarray + Tiled composite image. + """ + # Increment the device counter + params.device += 1 + + # Calculate number of rows - always rounds up + nrow = int(len(images) / ncol) + (len(images) % ncol > 0) + tracker = 0 + mat = [] + for _ in range(nrow): + row = [] + for _ in range(ncol): + if tracker <= (len(images) - 1): + row.append(images[tracker]) + tracker += 1 + mat.append(_row_resize(row, ncol)) + comp_img = _col_resize(mat) + _debug(visual=comp_img, filename=os.path.join(params.debug_outdir, f"{params.device}_tile.png")) + return comp_img diff --git a/tests/plantcv/visualize/conftest.py b/tests/plantcv/visualize/conftest.py index 8a39edc2f..86f749d62 100644 --- a/tests/plantcv/visualize/conftest.py +++ b/tests/plantcv/visualize/conftest.py @@ -22,6 +22,10 @@ def __init__(self): self.small_composed_contours_file = os.path.join(self.datadir, "setaria_small_plant_composed_contours.npz") # PlantCV hyperspectral image object self.hsi_file = os.path.join(self.datadir, "hsi.pkl") + # Tile image directory + self.tile_dir = os.path.join(self.datadir, "visualize_tile/") + # Tile image output + self.tile_out = os.path.join(self.datadir, "Tile_output.jpg") @staticmethod def load_hsi(pkl_file): diff --git a/tests/plantcv/visualize/test_tile.py b/tests/plantcv/visualize/test_tile.py new file mode 100644 index 000000000..1c7fc7435 --- /dev/null +++ b/tests/plantcv/visualize/test_tile.py @@ -0,0 +1,12 @@ +import cv2 +from plantcv.plantcv.visualize import tile + + +def test_tile(visualize_test_data): + """Test for PlantCV.""" + # Read in image list + images = [] + for _ in range(4): + images.append(cv2.imread(visualize_test_data.small_rgb_img)) + composite = tile(images=images, ncol=3) + assert composite.shape == (670, 1200, 3)