Counting seeds with Python

Posted on 21 September 2015

On Sunday my daughter and I planted some cress seeds. The packet claimed that it contained an average of 500 seeds, so here's an attempt to validate that claim using Python. We planted the seeds on kitchen towel, so there is a good contrast between the seeds and the background (click for the full size image).

Cress seeds on kitchen towel

We'll count the seeds by adding up the number of pixels comprising the seeds and dividing by the average number of pixels per seed. First, let's extract a region for calibration (the below is a rotated region from the left hand side of the original image).

Calibration region

Now we'll use the Pillow imaging library, NumPy, and pylab to load and inspect the image, seeds_21.jpg.

In [1]: from PIL import Image
In [2]: import numpy as np
In [3]: import pylab
In [4]: calibration_image = Image.open('seeds_21.jpg')
In [5]: calibration_array = np.array(calibration_image)
In [6]: calibration_array.shape
Out[6]: (825, 162, 3)

The first two dimensions of the array here are the height and width of the (unrotated) image in pixels. The final dimension holds the pixels' red, green and blue values. The seeds are kind of reddish, so we'll use the values of the blue channel (index 2) which will be high for the white background and low for the seeds. Using pylab,

In [7]: import matplotlib.cm as cm
In [8]: pylab.imshow(calibration_array[...,2], cmap=cm.Greys)
In [9]: pylab.colorbar(orientation='horizontal')
In [10]: pylab.show()

Blue channel of the calibration image

Which values to assign to the seeds and which to the background is a matter of judgement. This is one of the (few) cases that the default matplotlib "jet" colormap is useful:

In [7]: import matplotlib.cm as cm
In [8]: pylab.imshow(calibration_array[...,2])
In [9]: pylab.colorbar(orientation='horizontal')
In [10]: pylab.show()

Blue channel of the calibration image with jet colormap

100 seems to be a decent threshold value, so given there are 21 seeds in this calibration image, we can calculate the grain size in pixels:

In [11]: grain_size = np.sum(calibration_array[...,2] < 100) / 21
In [12]: grain_size
Out[12]: 554.47619047619048

Then, with the original image, seeds_all.jpg:

In [13]: image = Image.open('seeds_all.jpg')
In [14]: arr = np.array(image)
In [15]: nseeds = np.sum(arr[...,2] < 100) / grain_size
In [16]: nseeds
Out[16]: 540.74639299209889

which is within the 10% of the claimed value of 500.