How to count the objects present in the image with PHP?

Asked

Viewed 1,150 times

41

The original photo of the bean is this:

objetos

It lowers the resolution of the photo, I’ve already applied a grey-scale filter and increased the contrast to maximum to turn black and white. Then I examined the color of each of the pixels and set up an array. If I iterate over this array and write a 0 for white pixels and an x for black, I get the following:

matriz

From there I’m trying to find a way to count how many portions of x there are in this matrix. Can anyone help me with this?

  • 3

    Anderson’s answer is more than enough. But if you want to go deeper, the solution to N types of objects can be in Hough transformed. There are transformed methods for red blood cell count and urine (rounded shapes) as well as finding rectangular shapes. And many other cases. Sample papers: "Automatic Red Blood Cell Counting Using Hough Transform", "Development Algorithm to Count Blood Cells inUrine Sediment using ANN and Hough Transform" and "Rectangle Detection based on a Windowed Hough Transform".

  • If the beans are together (touching) or superimposed, The Transform Watershed can be used. This Codegolf Challenge: Counting Grains of Rice shows some ways to count grains with image segmentation in various languages.

2 answers

47

It is worth mentioning that in this answer was more valued for the simplicity of the solution than the performance of the same.

Logic

The most natural thing to think about is, I’m going to go through all the values of my matrix to see if it’s 1, if it is, increase the amount of beans. A very obvious problem is that this way it will be considered, for each 1 of the beans, as a different bean. In the example given below, instead of resulting in 3 beans, it would result in 88 (total of 1 in the matrix). A simple way around this is, when found a value 1 in the matrix, replace it with 2 and check the peripheral values, that is, the four lateral values and the four diagonal values, searching for other values 1. If found, repeat the process, replacingby 2 and by checking their peripheral values. Thus, for each value 1 found in the loop, the area of the bean referred to this value will have its values updated to 2, so, when continuing with the loop running through the matrix, no more will find values 1 for the same bean and values 2, ignoring them.

Solution

Photo considered in the solution

It was considered a 50x50 photo containing 3 beans.

$photo = [
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
];

Defined the variable for quantity of beans:

$beans = 0;

We traverse the entire matrix, searching for values 1:

// Percorre as linhas da foto
for ($i = 0; $i < count($photo); $i++)
{

  // Percorre as colunas da foto
  for ($j = 0; $j < count($photo[$i]); $j++)
  {

    // Encontrou um feijão
    if ($photo[$i][$j] == 1)
    {
      // Incrementa a quantidade de feijões
      $beans++;

      // Atualiza a área do feijão por 2
      expandBeanArea($photo, $i, $j);
    }

  }
}

We will define the function expandBeanArea so that it is recursive and can reach the whole area in the matrix referring to a bean, replacing the values 1 by 2. Follows the function:

function expandBeanArea (&$photo, $i, $j)
{
  if ($i < 0 || $i >= count($photo)) return;
  if ($j < 0 || $j >= count($photo[0])) return;

  if ($photo[$i][$j] == 1)
    {
        $photo[$i][$j] = 2;

        expandBeanArea($photo, $i-1, $j-1);
        expandBeanArea($photo, $i-1, $j);
        expandBeanArea($photo, $i-1, $j+1);
        expandBeanArea($photo, $i, $j-1);
        expandBeanArea($photo, $i, $j+1);
        expandBeanArea($photo, $i+1, $j-1);
        expandBeanArea($photo, $i+1, $j);
        expandBeanArea($photo, $i+1, $j+1);
    }
}

If we set a way out:

echo "Encontrados {$beans} feijões na foto.";

We’ll have the message: Encontrados 3 feijões na foto.

See working on Phpfiddle.

Note 1: It is important for this solution that the parameter $photo of function expandBeanArea is passed by reference. This is due to the fact that the changes made about this variable should influence the next iterations of the executed loop.


Note 2: It is also indispensable that the loop that runs through the matrix be done with the instruction for, for the instruction foreach stores, somehow, the initial value of the matrix and uses it in all iterations and even if its value changes (including passing by reference, see Note 1), all iterations are based on the initial value, generating a wrong result.


Note 3: Following this logic, will be considered as a bean each value or region of 1 that is surrounded with values 0. This practice is not advisable for the system in production, because any noise present could generate false results. One way around this would be, in the process of replacing the values by 2, also count the amount of values 1 that are being replaced for the same bean and thus only consider that it is, in fact, a bean, when the amount passes a predefined limit value.


Eliminating Noises

As quoted in Note 3, the program is very sensitive to any noise present in the photo. So to implement the solution suggested in the same, to count the amount of 1 in each bean, in order to define a minimum area in the photo, we can do:

function expandBeanArea (&$photo, $i, $j)
{
  if ($i < 0 || $i >= count($photo)) return;
  if ($j < 0 || $j >= count($photo[0])) return;

  $area = 0;

  if ($photo[$i][$j] == 1)
    {
        $photo[$i][$j] = 2;

        $area++;

        $area += expandBeanArea($photo, $i-1, $j-1);
        $area += expandBeanArea($photo, $i-1, $j);
        $area += expandBeanArea($photo, $i-1, $j+1);
        $area += expandBeanArea($photo, $i, $j-1);
        $area += expandBeanArea($photo, $i, $j+1);
        $area += expandBeanArea($photo, $i+1, $j-1);
        $area += expandBeanArea($photo, $i+1, $j);
        $area += expandBeanArea($photo, $i+1, $j+1);
    }

  return $area;
}

Notice the insertion of the variable $area being responsible for storing the amount of values 1 of each bean. Now, as we traverse the matrix, we make the condition:

// Percorre as linhas da foto
for ($i = 0; $i < count($photo); $i++)
{

  // Percorre as colunas da foto
  for ($j = 0; $j < count($photo[$i]); $j++)
  {

    // Encontrou um feijão
    if ($photo[$i][$j] == 1)
    {
      // Verifica a área do feijão na foto
      $area = expandBeanArea($photo, $i, $j);

      // Verifica se a área é maior que o limiar pré-definido
      if ($area >= 20)
      {
        // Incrementa a quantidade de feijões
        $beans++;
      }
    }

  }
}

Thus, if at least 20 values 1 are not found composing the same area of the photo, it is considered noise and not counted in the bean count.

Note 4: The value 20 is hypothetical in this example and should be defined according to the necessity of your project. Make an estimate of the average area occupied by the beans to obtain this value.

See the expansion process, replacing the values 1 by 2, in the animation below. Note that the bean count occurs only when the entire area is expanded, this will be considered the number of 1 found, counting as beans only if the area is equal to or greater than 20. I took advantage and included in the example some small noises, characterized by smaller areas of 1 spread by the photo.

inserir a descrição da imagem aqui

  • I took the liberty of creating an example in php fiddle, if you don’t mind :)

  • Executed? I tried here before and gave timeout. If yes, thank you.

  • 1

    One problem I found is that if you have any "dirt" in the photo, for example the matrix have a "1" lost anywhere, it increases the amount of beans. I could do this treatment for improvement.

  • Yes, see note 3 of the answer.

  • A minimum size of the expanding areaBeanArea maybe?

  • @Acklay, I implemented the solution in the answer.

  • @Andersoncarloswoss sorry, I didn’t read the noise question in note 3. I liked the explanation. + 1 Ps.:I made an app in 2014 that I worked with image processing but in JAVA. The idea is to calculate the amount of defoliation of a sheet. See and understand better: https://play.google.com/store/apps/details?id=upvision.bioleaf&hl=pt_BR

  • 4

    Very good answer. Won my +1. : ) Although you are absolutely right about the importance of noise treatment, my opinion is that this treatment should be done in the pre-processing of the image and not in the counting of beans. The reason is that AP may someday need to make other decisions about beans (for example, locating them in the image), and so it would need to replicate this noise treatment logic in another function.

  • 2

    There are ways to do this directly in the original image, using smoothing filters, morphological operations and even using other types of limiarization.

  • 4

    Absolutely. I would say that PHP would not be the ideal language for this type of process. Python would be a much better option, but depending on the need, PHP does the job.

Show 5 more comments

12

If you can install an extension it is possible to do with php-opencv.

First, with the image with name feijao.jpg sage, we can read it in shades of gray.

$image  = imread('feijao.jpg', IMREAD_GRAYSCALE);

As the beans are black, it is very easy to isolate them from the rest of the image by simply applying a Threshold with a very small value. In the example, I used 80, so that every pixel smaller than 80 became foreground and larger than 80 became background (the THRESH_BINARY_INV turns black into white and vice versa).

$binary = null;
threshold($image, $binary, 80, 255, THRESH_BINARY_INV);

We have as a result of this processing the following image:

Imagem preto e branco

Now let’s find all the beans using the function findContours opencv. In the library the function is called findContoursWithoutHierarchy because it does not return the third parameter that the original function returns, the hierarchies between the outlines.

$contours = null;
findContoursWithoutHierarchy($binary, $contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

With this we have a list of outlines. But each contour can have dozens of points, in this case, to make it easier to work, let’s calculate the rectangles that encompass each set of contours that we find. That’s why we use the function boundingRect.

$boxes = array_map(function ($contour) {
    return boundingRect($contour);
}, $contours);

Drawing the rectangles found, we have the following result. Imagem com bounding box nos objetos encontrados

But if we count the number of boxes we will see that the result is 9 (and not 8 as we expected). Looking at the boxes, there is 1 extremely small (with height and width of 1px). Let’s filter the results with very small pixels by guessing a minimum value.

$boxes = array_filter($boxes, function ($box) {
    return $box->width > 20 && $box->height > 20;
});

Here we remove everyone who is less than 20px high or wide. The height and average width of the correct rectangles is at 120px, so it’s a reasonable bet.

Now just print the result:

echo 'Encontrados ' . count($boxes) . ' feijões na foto.';

And we’ll get the right result:

Found 8 beans in the photo.

PS: I omitted some parts of the code to focus only on the essentials, I put the full code into that gist

Browser other questions tagged

You are not signed in. Login or sign up in order to post.