Example 6: Counting and measuring freshwater snails

In this example we will use thresholding and watershed algorithms to count freshwater snails.

Before

Input - Snails photographed from a camera stand. Variable brightness across the tray and snail clumping are the biggest challenges.

After

Results - After applying adaptive thresholding and a watershed algorithm, the snail separate well from the background. Now we can count them, and measure size, shape and colouration

Low throughput

First, we test a single image with the low throughput workflow.

[4]:
import phenopype as pp
import os

image_dir = "images/snails1.jpg"

ct = pp.load_image(image_dir,
                   cont=True,  # load as container
                   dirpath= "../_temp/output/ex6",  # specify save-directory
                   save_suffix = "snails1") # give files a save-suffix (e.g. "contours_ex6.csv")
Directory to save files set at - E:\git_repos\phenopype\_temp\output\ex6

The load method of a container attempts to load any previously saved results, masks, etc. from the specified dirpath. reset will only reset the modified images, as well as image and contour DataFrames. This will preserve any drawn masks or other data stemming from user interaction.

[2]:
ct.load()
ct.reset()
AUTOLOAD
- masks_snails1.csv

Begin by drawing a mask around the snails inside the tray by dragging a rectangle around them - finish with Enter, abort with Esc.

[5]:
pp.preprocessing.create_mask(ct)
- create mask

Now we need to measure the pixel-to-mm-ratio. The snail pictures don’t contain a scale that is appropriate for automatic detection - the millimeter-paper on the side does not contain enough unique keypoints to be detected by the AKAZE algorithm that powers find_scale. Therefore each image needs to be manually measured by dragging a line across the mm-paper, and entering the distance.

Note: It is better to measure a long distance than a short distance to minimize the measurement error - use the full length (~70 mm) here.

[6]:
pp.preprocessing.create_scale(ct)
- measure pixel-to-mm-ratio
Scale set
- add column length

For high data quality it is important to verify the ID of the specimen in the current picture. Often, the picture name contains the ID, but typically an label is placed inside the image. Using the enter_data tool we open the image and an entry prompt that will create a column with a name of our chosing inside all exported results.

[7]:
pp.preprocessing.enter_data(ct, columns="ID")
- add column ID

Next is segmentation. First we blur the image a little, convert it to a binary image, and look at the results:

[8]:
pp.segmentation.blur(ct)
pp.segmentation.threshold(ct,
                          method="adaptive",
                          blocksize=59, ## relatively low sensitivity
                          constant=10) ## relatively constant gets subtracted from the binary result
pp.show_image(ct) # `ct` automatically shows the last edit within the container, `ct.image` would also work
- include mask "mask1" pixels

The break up the clumping, we apply the watershed algorithm to the binarized image:

[9]:
pp.segmentation.watershed(ct, distance_cutoff=0.5) # , iterations=1, kernel_size=1
pp.show_image(ct)

With this image we can find the contours - for watershed we have to select the "ccomp" option.

[10]:
pp.segmentation.find_contours(ct,
                              retrieval="ccomp", # this finds the splitted inner rather than the outer contours
                              min_area=200,  # noise removal
                              subset="child") # needs to be "child" for watershed

With the contours we have acquired the primary information (counts, size, area) contained in the image. However, since we have the contour location, we get the colour for free. Note that we also measure the background whiteness of each scale (background=True). This is important, as the snails lie across a brightness gradient within the images.

[11]:
pp.measurement.colour_intensity(ct,
                                background=True) # this measures the whiteness of the area around each detected snails

Now we draw the contours…

[12]:
pp.visualization.select_canvas(ct, # onto which image should the contours be draw
                               canvas="raw") # raw = original image
pp.visualization.draw_contours(ct,
                               fill=0,
                               line_width=2,
                               watershed=True, # this flag needs to be added when using watershedding
                               bounding_box=True) # this indicates the area where the background was measured
pp.show_image(ct.canvas)
- raw image

… and save them, as well as the masks (if we need to redo this) and the canvas for quality control.

[13]:
pp.export.save_canvas(ct)
pp.export.save_masks(ct)
pp.export.save_contours(ct)
- canvas saved under ../_temp/output/ex6\canvas_snails1.jpg (overwritten).
- masks saved under ../_temp/output/ex6\masks_snails1.csv (overwritten).
- contours saved under ../_temp/output/ex6\contours_snails1.csv (overwritten).

Note: the countour csv contains only the inner, watershedded contours (separated=“child”), the outer (unseparated=“parent”) were removed with find_contours.

High throughput

As for the other examples I have created a preset (ex6) with appropriate settings for the example. The template can be passed to the pype using config_preset="ex6" - see below.

Adding a scale

Because there is a brightness gradient across the image, correcting the exposure across all images will not work. Instead we use the colour_intensity to return the local background whiteness (within the rectangle, excluding all snail pixels). Each snail will have an individual background brightness score that can be used to normalize the detected values, so that the colour intensity becomes a meaningful trait within and across images.

First, set some directories and inspect the preset.

[1]:
import phenopype as pp
import os

project_root = r"../_temp/output/ex6_proj"
images = r"images"

print(pp.presets.ex6)

preprocessing:
- create_mask
- create_scale
- enter_data
segmentation:
- blur:
    kernel_size: 3
- threshold:
    method: adaptive
    blocksize: 59
    constant: 10
    channel: gray
- watershed:
    distance_cutoff: 0.5
# - draw  # to separate snails
- find_contours:
    retrieval: ccomp # needs to be ccomp for watershed
    min_diameter: 0
    min_area: 200
    subset: child # needs to be child for watershed
measurement:
- colour_intensity:
    background: True
visualization:
- select_canvas:
    canvas: raw
- draw_contours:
    line_width: 2
    label_width: 1
    label_size: 1
    fill: 0
    watershed: true
    bounding_box: True
- draw_masks
export:
- save_contours:
    save_coords: False
- save_colours
- save_masks

Then create a project. As stated above in the low throughput instructions: defining a project wide scale will not work because the provided reference card is too homogeneous, i.e. does not contain enough keypoints for registration by find_scale. If you already have created the project, simply load it.

[2]:
if not os.path.isdir(project_root):
    proj = pp.project(root_dir=project_root, overwrite=True)
    proj.add_files(image_dir=images, include="snails", overwrite=True)
    proj.add_config(name = "v1", overwrite=True, preset="ex6")
    pp.project.save(proj, overwrite=True)
else:
    proj = pp.project.load(project_root)
--------------------------------------------
Project loaded from
E:\git_repos\phenopype\_temp\output\ex6_proj
--------------------------------------------

Now you can loop through all directories stored within the project object to detect the snails.

[3]:
for i in proj.dirpaths:
    p = pp.pype(i, name="v1")
E:\git_repos\phenopype\_temp\output\ex6_proj\data\0__snails1\pype_config_v1.yaml


------------+++ new pype iteration 2020:05:01 18:48:25 +++--------------


AUTOLOAD
- columns ID from attributes.yaml
- current scale information loaded from attributes.yaml
- masks_v1.csv
PREPROCESSING
create_mask
- mask with label mask1 already created (overwrite=False)
create_scale
- scale pixel-to-mm-ratio already measured (overwrite=False)
enter_data
- column ID already created (overwrite=False)
SEGMENTATION
blur
threshold
- include mask "mask1" pixels
watershed
find_contours
MEASUREMENT
colour_intensity
VISUALIZATION
select_canvas
- raw image
draw_contours
draw_masks
 - show mask: mask1.
EXPORT
save_contours
- contours saved under E:\git_repos\phenopype\_temp\output\ex6_proj\data\0__snails1\contours_v1.csv (overwritten).
save_colours
- colours saved under E:\git_repos\phenopype\_temp\output\ex6_proj\data\0__snails1\colours_v1.csv (overwritten).
save_masks
- masks saved under E:\git_repos\phenopype\_temp\output\ex6_proj\data\0__snails1\masks_v1.csv (overwritten).
AUTOSAVE
save_canvas
- canvas saved under E:\git_repos\phenopype\_temp\output\ex6_proj\data/0__snails1\canvas_v1.jpg (overwritten).
save_data_entry
- column ID not saved (overwrite=False)
save_scale
- save scale to attributes (overwriting)


TERMINATE
E:\git_repos\phenopype\_temp\output\ex6_proj\data\0__snails2\pype_config_v1.yaml


------------+++ new pype iteration 2020:05:01 18:48:27 +++--------------


AUTOLOAD
- columns ID from attributes.yaml
- current scale information loaded from attributes.yaml
- masks_v1.csv
PREPROCESSING
create_mask
- mask with label mask1 already created (overwrite=False)
create_scale
- scale pixel-to-mm-ratio already measured (overwrite=False)
enter_data
- column ID already created (overwrite=False)
SEGMENTATION
blur
threshold
- include mask "mask1" pixels
watershed
find_contours
MEASUREMENT
colour_intensity
VISUALIZATION
select_canvas
- raw image
draw_contours
draw_masks
 - show mask: mask1.
EXPORT
save_contours
- contours saved under E:\git_repos\phenopype\_temp\output\ex6_proj\data\0__snails2\contours_v1.csv (overwritten).
save_colours
- colours saved under E:\git_repos\phenopype\_temp\output\ex6_proj\data\0__snails2\colours_v1.csv (overwritten).
save_masks
- masks saved under E:\git_repos\phenopype\_temp\output\ex6_proj\data\0__snails2\masks_v1.csv (overwritten).
AUTOSAVE
save_canvas
- canvas saved under E:\git_repos\phenopype\_temp\output\ex6_proj\data\0__snails2\canvas_v1.jpg (overwritten).
save_data_entry
- column ID not saved (overwrite=False)
save_scale
- save scale to attributes (overwriting)


TERMINATE

In the end, use the collect_results method of proj to save all results to a folder in the root directory.

[1]:
proj.collect_results(name="v1",files=["contours"], folder="contours_ref", overwrite=True)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-1-b2cf852f4471> in <module>
----> 1 proj.collect_results(name="v1",files=["contours"], folder="contours_ref", overwrite=True)

NameError: name 'proj' is not defined
[ ]: