Skip to content

Commit

Permalink
Merge pull request #569 from danforthcenter/add-feature-to-pcv.visual…
Browse files Browse the repository at this point in the history
…ize.pseudocolor
  • Loading branch information
nfahlgren authored Apr 22, 2021
2 parents aa1f42e + 1858497 commit 79be68c
Show file tree
Hide file tree
Showing 13 changed files with 311 additions and 196 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions docs/mask_bad_threshold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## Mask bad pixels Threshold

Creates a binary image from a grayscale based on pixel values and the definition of "bad" pixels.
"Bad" pixels are invalid numeric data such as not a number (nan) or infinite (inf).

**plantcv.threshold.mask_bad(*float_img, bad_type="native"*)**

**returns** thresholded/binary image

- **Parameters:**
- float_img - Input float image data (most likely an image that is the result of some numeric calculation, e.g. a hyperspectral index image). The datatype should be "float".
- bad_type - The definition of "bad" pixels ("nan", "inf", or "native", default="native")
- **Context:**
- Used to threshold based on value of pixels. This can be useful to post-process calculated hyperspectral indices.
- **Example use:**
- Below (to be followed with "visualize.pseudocolor" to visualize the result).

```python

from plantcv import plantcv as pcv
# Mask all types of bad pixels out present in the original image (nan and inf)
bad_mask_1 = pcv.threshold.mask_bad(float_img=float_img, bad_type="native")

# Mask pixels with nan values (if any) in the original image out
bad_mask_2 = pcv.threshold.mask_bad(float_img=float_img, bad_type="nan")

# Mask pixels with inf values (if any) in the original image out
bad_mask_3 = pcv.threshold.mask_bad(float_img=float_img, bad_type="inf")

```

**Mask for bad pixels**

We can see that a mask indicating locations of "bad" pixels generated.

![Screenshot](img/documentation_images/mask_bad_threshold/bad_mask_both.png)

To visualize the original image with "bad" pixels highlighted, check [here](https://github.com/danforthcenter/plantcv/blob/master/docs/visualize_pseudocolor.md)

**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/master/plantcv/plantcv/threshold/threshold_methods.py)
42 changes: 29 additions & 13 deletions docs/visualize_pseudocolor.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,25 @@ pseudocolored image. Additionally, optional maximum and minimum values can be sp
then the image gets saved to `pcv.params.debug_outdir`, and`pcv.params.dpi` can be set for the image that gets saved. If
unaltered, the matplotlib default DPI is 100 pixels per inch.

**plantcv.visualize.pseudocolor**(*gray_img, obj=None, mask=None, background="image", cmap=None, min_value=0, max_value=255, axes=True, colorbar=True, obj_padding='auto'*)
**plantcv.visualize.pseudocolor**(*gray_img, obj=None, mask=None, background="image", cmap=None, min_value=0, max_value=255, axes=True, colorbar=True, obj_padding='auto', bad_mask=None, bad_color="red"*)

**returns** pseudocolored image (that can be saved with `pcv.print_image`)

- **Parameters:**
- gray_img - Grayscale image data
- obj - ROI or plant contour object (optional) if provided, the pseudocolored image gets cropped down to the region of interest.
- mask - Binary mask made from selected contours (optional)
- background - Background color/type. Options are "image" (default), "white", or "black". A mask must be supplied in order to utilize this parameter.
- cmap - Custom colormap, see [here](https://matplotlib.org/tutorials/colors/colormaps.html) for tips on how to choose a colormap in Matplotlib.
- min_value - Minimum value (optional) for range of the colorbar. Default: 0
- max_value - Maximum value (optional) for range of the colorbar. Default: 255
- axes - If False then the title, x-axis, and y-axis won't be displayed (default axes=True).
- colorbar - If False then the colorbar won't be displayed (default colorbar=True)
- obj_padding - If "auto" (default), and an obj is supplied, then the image is cropped to an extent 20% larger in each dimension than the object. A single integer is also accepted to define the padding in pixels.
- title - The title for the pseudocolored image (default title=None)

- gray_img - Grayscale image data
- obj - ROI or plant contour object (optional) if provided, the pseudocolored image gets cropped down to the region of interest. "obj" can be the 1st output of the PlantCV region of interests functions, e.g. `pcv.roi.rectangle`.
- mask - Binary mask made from selected contours (optional)
- cmap - Custom colormap, see [here](https://matplotlib.org/tutorials/colors/colormaps.html) for tips on how to choose a colormap in Matplotlib.
- background - Background color/type. Options are "image" (default), "white", or "black". A mask must be supplied in order to utilize this parameter.
- min_value - Minimum value (optional) for range of the colorbar. Default: 0
- max_value - Maximum value (optional) for range of the colorbar. Default: 255
- axes - If False then the title, x-axis, and y-axis won't be displayed (default axes=True).
- colorbar - If False then the colorbar won't be displayed (default colorbar=True)
- obj_padding - if "auto" (default), and an obj is supplied, then the image is cropped to an extent 20% larger in each dimension than the object. A single integer is also accepted to define the padding in pixels.
- title - The title for the pseudocolored image (default title=None)
- bad_mask - binary mask of pixels with "bad" values, e.g. nan or inf or any other values considered to be not informative and should be excluded from analysis. default = None
- bad_color - The color that shows "bad" pixels in output pseudocolored image, default: "red"

- **Context:**
- Used to pseudocolor any grayscale image to custom colormap
- **Example use:**
Expand All @@ -35,6 +37,9 @@ unaltered, the matplotlib default DPI is 100 pixels per inch.

![Screenshot](img/documentation_images/pseudocolor/mask.jpg)

**Mask of "bad" values**
![Screenshot](img/documentation_images/pseudocolor/bad_mask.png)


```python

Expand Down Expand Up @@ -72,6 +77,11 @@ simple_pseudo_img = pcv.visualize.pseudocolor(gray_img=img, obj=None, mask=mask,
background="image", axes=False,
colorbar=False, cmap='viridis')

# When there are some user defined "bad" pixels indicated in array "bad_mask", and the red color is used to visualize them in the visualization.
pseudo_img_bad_mask = pcv.visualize.pseudocolor(gray_img=img, obj=None, mask=None, bad_mask=bad_mask, bad_color="red", axes=False, colorbar=False)

# When there are some user defined "bad" pixels indicated in array "bad_mask", and the red color is used to visualize them in the visualization.
pseudo_img_mask_obj_bad_mask = pcv.visualize.pseudocolor(gray_img=img, obj=obg, mask=mask, background="white", bad_mask=bad_mask, bad_color="red", axes=False, colorbar=True)
```

**Pseudocolored Image**
Expand All @@ -98,4 +108,10 @@ simple_pseudo_img = pcv.visualize.pseudocolor(gray_img=img, obj=None, mask=mask,

![Screenshot](img/documentation_images/pseudocolor/pseudo_onimage_simple.jpg)

**Pseudocolored, Pixels With User Defined "bad" Values Marked Using Red Color (no axes or colorbar)**
![Screenshot](img/documentation_images/pseudocolor/pseudocolored_mask_bad.png)

**Pseudocolored, Cropped, background="white", Pixels With User Defined "bad" Values Marked Using Red Color (no axes)**
![Screenshot](img/documentation_images/pseudocolor/pseudocolored_mask_bad_obj.png)

**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/master/plantcv/plantcv/visualize/pseudocolor.py)
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ nav:
- 'Binary Threshold': binary_threshold.md
- 'Custom Range Threshold': custom_range_threshold.md
- 'Gaussian Adaptive Threshold': gaussian_threshold.md
- 'Mask Bad Threshold': mask_bad_threshold.md
- 'Mean Adaptive Threshold': mean_threshold.md
- 'Otsu Auto Threshold': otsu_threshold.md
- 'Saturation Threshold': saturation_threshold.md
Expand Down
3 changes: 2 additions & 1 deletion plantcv/plantcv/threshold/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
from plantcv.plantcv.threshold.threshold_methods import texture
from plantcv.plantcv.threshold.threshold_methods import custom_range
from plantcv.plantcv.threshold.threshold_methods import saturation
from plantcv.plantcv.threshold.threshold_methods import mask_bad

__all__ = ["binary", "gaussian", "mean", "otsu", "triangle", "texture", "custom_range", "saturation"]
__all__ = ["binary", "gaussian", "mean", "otsu", "triangle", "texture", "custom_range", "saturation", "mask_bad"]
90 changes: 61 additions & 29 deletions plantcv/plantcv/threshold/threshold_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from plantcv.plantcv import params
from skimage.feature import greycomatrix, greycoprops
from scipy.ndimage import generic_filter
from plantcv.plantcv._debug import _debug


# Binary threshold
Expand Down Expand Up @@ -316,6 +317,9 @@ def calc_texture(inputs):

# Threshold so higher texture measurements stand out
bin_img = binary(gray_img=output, threshold=threshold, max_value=max_value, object_type='light')

_debug(visual=bin_img, filename=os.path.join(params.debug_outdir, str(params.device) + "_texture_mask.png"))

return bin_img


Expand Down Expand Up @@ -352,12 +356,12 @@ def custom_range(img, lower_thresh, upper_thresh, channel='gray'):

# Separate channels
hue = hsv_img[:, :, 0]
saturation = hsv_img[:, :, 1]
sat = hsv_img[:, :, 1]
value = hsv_img[:, :, 2]

# Make a mask for each channel
h_mask = cv2.inRange(hue, lower_thresh[0], upper_thresh[0])
s_mask = cv2.inRange(saturation, lower_thresh[1], upper_thresh[1])
s_mask = cv2.inRange(sat, lower_thresh[1], upper_thresh[1])
v_mask = cv2.inRange(value, lower_thresh[2], upper_thresh[2])

# Apply the masks to the image
Expand Down Expand Up @@ -448,18 +452,12 @@ def custom_range(img, lower_thresh, upper_thresh, channel='gray'):
fatal_error(str(channel) + " is not a valid colorspace. Channel must be either 'RGB', 'HSV', or 'gray'.")

# Auto-increment the device counter
params.device += 1

# Print or plot the binary image if debug is on
if params.debug == 'print':
print_image(masked_img, os.path.join(params.debug_outdir,
str(params.device) + channel + 'custom_thresh.png'))
print_image(mask, os.path.join(params.debug_outdir,
str(params.device) + channel + 'custom_thresh_mask.png'))
elif params.debug == 'plot':
plot_image(mask)
plot_image(masked_img)

_debug(visual=masked_img, filename=os.path.join(params.debug_outdir,
str(params.device) + channel + 'custom_thresh.png'))
_debug(visual=mask, filename=os.path.join(params.debug_outdir,
str(params.device) + channel + 'custom_thresh_mask.png'))
return mask, masked_img


Expand All @@ -472,11 +470,8 @@ def _call_threshold(gray_img, threshold, max_value, threshold_method, method_nam
bin_img = np.uint8(bin_img)

# Print or plot the binary image if debug is on
if params.debug == 'print':
print_image(bin_img, os.path.join(params.debug_outdir,
str(params.device) + method_name + str(threshold) + '.png'))
elif params.debug == 'plot':
plot_image(bin_img, cmap='gray')
_debug(visual=bin_img, filename=os.path.join(params.debug_outdir,
str(params.device) + method_name + str(threshold) + '.png'))

return bin_img

Expand All @@ -487,11 +482,7 @@ def _call_adaptive_threshold(gray_img, max_value, adaptive_method, threshold_met
bin_img = cv2.adaptiveThreshold(gray_img, max_value, adaptive_method, threshold_method, 11, 2)

# Print or plot the binary image if debug is on
if params.debug == 'print':
print_image(bin_img, os.path.join(params.debug_outdir,
str(params.device) + method_name + '.png'))
elif params.debug == 'plot':
plot_image(bin_img, cmap='gray')
_debug(visual=bin_img, filename=os.path.join(params.debug_outdir, str(params.device) + method_name + '.png'))

return bin_img

Expand Down Expand Up @@ -695,9 +686,6 @@ def saturation(rgb_img, threshold=255, channel="any"):
:param channel: str
:return masked_img: np.ndarray
"""

params.device += 1

# Mask red, green, and blue saturation separately
b, g, r = cv2.split(rgb_img)
b_saturated = cv2.inRange(b, threshold, 255)
Expand All @@ -719,8 +707,52 @@ def saturation(rgb_img, threshold=255, channel="any"):
# Invert "saturated" before returning, so saturated = black
bin_img = cv2.bitwise_not(saturated)

if params.debug == 'print':
print_image(bin_img, os.path.join(params.debug_outdir, str(params.device), '_saturation_threshold.png'))
elif params.debug == 'plot':
plot_image(bin_img, cmap='gray')
_debug(visual=bin_img, filename=os.path.join(params.debug_outdir, str(params.device), '_saturation_threshold.png'))
return bin_img


def mask_bad(float_img, bad_type='native'):
""" Create a mask with desired "bad" pixels of the input floaat image marked.
Inputs:
float_img = image represented by an nd-array (data type: float). Most probably, it is the result of some
calculation based on the original image. So the datatype is float, and it is possible to have some
"bad" values, i.e. nan and/or inf
bad_type = definition of "bad" type, can be 'nan', 'inf' or 'native'
Returns:
mask = A mask indicating the locations of "bad" pixels
:param float_img: numpy.ndarray
:param bad_type: str
:return mask: numpy.ndarray
"""
size_img = np.shape(float_img)
if len(size_img) != 2:
fatal_error('Input image is not a single channel image!')

mask = np.zeros(size_img, dtype='uint8')
idx_nan, idy_nan = np.where(np.isnan(float_img) == 1)
idx_inf, idy_inf = np.where(np.isinf(float_img) == 1)

# neither nan nor inf exists in the image, print out a message and the mask would just be all zero
if len(idx_nan) == 0 and len(idx_inf) == 0:
mask = mask
print('Neither nan nor inf appears in the current image.')
# at least one of the "bad" exists
# desired bad to mark is "native"
elif bad_type.lower() == 'native':
# mask[np.isnan(gray_img)] = 255
# mask[np.isinf(gray_img)] = 255
mask[idx_nan, idy_nan] = 255
mask[idx_inf, idy_inf] = 255
elif bad_type.lower() == 'nan' and len(idx_nan) >= 1:
mask[idx_nan, idy_nan] = 255
elif bad_type.lower() == 'inf' and len(idx_inf) >= 1:
mask[idx_inf, idy_inf] = 255
# "bad" exists but not the user desired bad type, return the all-zero mask
else:
mask = mask
print('{} does not appear in the current image.'.format(bad_type.lower()))

_debug(visual=mask, filename=os.path.join(params.debug_outdir, str(params.device) + "_bad_mask.png"))

return mask
36 changes: 30 additions & 6 deletions plantcv/plantcv/visualize/pseudocolor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
from matplotlib import pyplot as plt
from plantcv.plantcv import params
from plantcv.plantcv import fatal_error
from plantcv.plantcv.apply_mask import apply_mask


def pseudocolor(gray_img, obj=None, mask=None, cmap=None, background="image", min_value=0, max_value=255,
axes=True, colorbar=True, obj_padding="auto", title=None):
axes=True, colorbar=True, obj_padding="auto", title=None, bad_mask=None, bad_color="red"):
"""Pseudocolor any grayscale image to custom colormap
Inputs:
Expand All @@ -28,7 +29,9 @@ def pseudocolor(gray_img, obj=None, mask=None, cmap=None, background="image", mi
larger in each dimension than the object. An single integer is also accepted to define the padding
in pixels
title = (optional) custom title for the plot gets drawn if title is not None. default = None
bad_mask = (optional) binary mask of pixels with "bad" values, e.g. nan or inf or any other values considered
to be not informative and to be excluded from analysis. default = None
bad_color = (optional) desired color to show "bad" pixels. default = "red"
Returns:
pseudo_image = pseudocolored image
Expand All @@ -44,6 +47,8 @@ def pseudocolor(gray_img, obj=None, mask=None, cmap=None, background="image", mi
:param obj_padding: str, int
:param title: str
:return pseudo_image: numpy.ndarray
:param bad_mask: numpy.ndarray
:param bad_color: str
"""

# Auto-increment the device counter
Expand All @@ -56,6 +61,8 @@ def pseudocolor(gray_img, obj=None, mask=None, cmap=None, background="image", mi
if len(np.shape(gray_img)) != 2:
fatal_error("Image must be grayscale.")

bad_idx, bad_idy = [], []

# Apply the mask if given
if mask is not None:
if obj is not None:
Expand Down Expand Up @@ -91,6 +98,12 @@ def pseudocolor(gray_img, obj=None, mask=None, cmap=None, background="image", mi
mask = cv2.copyMakeBorder(crop_mask, offsety, offsety, offsetx, offsetx, cv2.BORDER_CONSTANT,
value=(0, 0, 0))

# Crop the bad mask if there is one
if bad_mask is not None:
crop_bad_mask = bad_mask[y:y + h, x:x + w]
bad_mask = cv2.copyMakeBorder(crop_bad_mask, offsety, offsety, offsetx, offsetx, cv2.BORDER_CONSTANT,
value=(0, 0, 0))

# Apply the mask
masked_img = np.ma.array(gray_img1, mask=~mask.astype(np.bool))

Expand All @@ -104,7 +117,7 @@ def pseudocolor(gray_img, obj=None, mask=None, cmap=None, background="image", mi
# Background is all 255 (white)
bkg_img = np.zeros(np.shape(gray_img1), dtype=np.uint8)
bkg_img += 255
bkg_cmap = "gray"
bkg_cmap = "gray_r"
elif background.upper() == "IMAGE":
# Set the background to the input gray image
bkg_img = gray_img1
Expand All @@ -113,11 +126,19 @@ def pseudocolor(gray_img, obj=None, mask=None, cmap=None, background="image", mi
fatal_error(
"Background type {0} is not supported. Please use 'white', 'black', or 'image'.".format(background))

if bad_mask is not None:
debug_mode = params.debug
params.debug = None
bad_mask = apply_mask(bad_mask, mask, mask_color='black')
bad_idx, bad_idy = np.where(bad_mask > 0)
params.debug = debug_mode

plt.figure()
# Pseudocolor the image, plot the background first
plt.imshow(bkg_img, cmap=bkg_cmap)
# Overlay the masked grayscale image with the user input colormap
plt.imshow(masked_img, cmap=cmap, vmin=min_value, vmax=max_value)
plt.plot(bad_idy, bad_idx, '.', color=bad_color)

if colorbar:
plt.colorbar(fraction=0.033, pad=0.04)
Expand All @@ -126,8 +147,6 @@ def pseudocolor(gray_img, obj=None, mask=None, cmap=None, background="image", mi
# Include image title
if title is not None:
plt.title(title)
else:
plt.title('Pseudocolored image')
else:
# Remove axes
plt.xticks([])
Expand All @@ -137,17 +156,22 @@ def pseudocolor(gray_img, obj=None, mask=None, cmap=None, background="image", mi
pseudo_img = plt.gcf()

else:

if bad_mask is not None:
bad_idx, bad_idy = np.where(bad_mask > 0)
plt.figure()
# Pseudocolor the image
plt.imshow(gray_img1, cmap=cmap, vmin=min_value, vmax=max_value)
plt.plot(bad_idy, bad_idx, '.', color=bad_color)

if colorbar:
# Include the colorbar
plt.colorbar(fraction=0.033, pad=0.04)

if axes:
# Include image title
plt.title('Pseudocolored image') # + os.path.splitext(filename)[0])
if title is not None:
plt.title(title)
else:
# Remove axes
plt.xticks([])
Expand Down
Loading

0 comments on commit 79be68c

Please sign in to comment.