12
$\begingroup$

Given this image I would like to measure how much (in percentage relative to the square area) of each black delimited square is filled with green:

i = Import["http://i.imgur.com/62buGws.png"]

enter image description here

I am a bit lost with the usage of the image processing functions. It seems like MorphologicalPerimeterand PixelValuePositionsor ImageDatacould be useful.

If I can find a way to detect each square then it is a matter of calling ImageData on that area and count the number of green pixels.

Any tips please?

$\endgroup$
3
  • $\begingroup$ Does it need to count only the exact color defined by its RGB value? This could cause problems with jpg and other lossy formats. $\endgroup$
    – shrx
    Commented Dec 29, 2013 at 16:03
  • $\begingroup$ The original image is a PDF file but for some reason Mathematica gave me an error when using Import["image.pdf"] and trying to use the resulting image with the PixelValuePositions function. The error was "PixelValuePositions::imginv: Expecting an image or a graphics instead of...". So I converted it to a PNG. Is it possible to weight the pixels according to the intensity of green? $\endgroup$
    – dabd
    Commented Dec 29, 2013 at 16:44
  • $\begingroup$ It is certainly possible. But if the pdf consists of shapes, you might be able to extract the dimensions of green rectangles and calculate percentages from that. $\endgroup$
    – shrx
    Commented Dec 29, 2013 at 17:26

5 Answers 5

15
$\begingroup$

Update to account for multiple box sets on a single page

In order to extract the box information from a set of boxes, two tweaks were needed. First, I needed to explicitly state an ImageSize to get the desired resolution. Second, I needed to adjust the Binarize critereon so that the boxes, which are now surrounded by grey borders, could be identified by MorphologicalComponents.

i = Image[
   First@Import[
     "https://dl.dropboxusercontent.com/u/8003134/grid2.pdf", 
     "Pages"], ImageSize -> 2500];
boxes = ComponentMeasurements[
   MorphologicalComponents[
    Binarize[i, 
     And[#[[1]] < 0.75, #[[2]] < 0.75, #[[3]] < 
        0.75] &]], {"BoundingBox", "Centroid"}];
centers = (Range@Length@boxes /. boxes)[[All, 2]];
dims = (Range@Length@boxes /. boxes)[[All, 1]];
intensity = 
  1 - ImageMeasurements[
      ImageTake[i, 
       ImageDimensions[i][[2]] - {#[[2, 2]], #[[1, 2]]}, {#[[1, 
          1]], #[[2, 1]]}], "MeanIntensity"] & /@ dims;
intensity = Rescale[intensity, {Min[intensity], Max[intensity]}];
Show[i, Epilog -> 
  Table[Text[NumberForm[intensity[[i]], {1, 2}], centers[[i]]], {i, 
    Length@boxes}]]

The resulting image is too big to display. Instead, here is a cropped image showing the complete upper-left hand set of boxes and bits of the neighboring boxes to demonstrate that the code is working.

Mathematica graphics

Some additional tweaking of the code will be necessary if you want to get precision better than about 5%. My first suggestion would be to raise the image size even further and fine tune the Binarize criteria.

Note that replacing First with Last in the definition of i should get you the second page.

Old approach

This works for a pdf containing a single set of boxes

Here's another approach that makes use of ComponentMeasurements

i = Image[
   First@Import["https://dl.dropboxusercontent.com/u/8003134/img.pdf",
      "Pages"]];
boxes = ComponentMeasurements[
   MorphologicalComponents[
    Binarize[i, #[[1]] == #[[2]] == #[[3]] == 0 &]], {"BoundingBox", 
    "Centroid"}];
centers = (Range@Length@boxes /. boxes)[[All, 2]];
dims = (Range@Length@boxes /. boxes)[[All, 1]];
intensity = 
  1 - ImageMeasurements[
      ImageTake[i, 
       ImageDimensions[i][[2]] - {#[[2, 2]], #[[1, 2]]}, {#[[1, 
          1]], #[[2, 1]]}], "MeanIntensity"] & /@ dims;
intensity = Rescale[intensity, {Min[intensity], Max[intensity]}];
Show[i, Epilog -> 
  Table[Text[intensity[[i]], centers[[i]]], {i, Length@boxes}]]

Mathematica graphics

First, I find the squares in the image with MorphologicalComponents. I use the dimensions extracted from ComponentMeasurements to ImageTake each square. To determine the amount of green in the square, I use ImageMeasurements which won't give a 0 for the completely empty box since I've made no effort to remove the bounding black line. I resolve this issue by rescaling the intensity, assuming that there is a completely filled and completely empty box. Finally, since there are already answers showing how one extracts the values into a table, I use the "Centroid" from ComponentMeasurements to Show the values over the original image.

$\endgroup$
7
  • $\begingroup$ Thanks a lot! A looked at MorphologicalComponents and ComponentMeasurements but didn't know how to use it. $\endgroup$
    – dabd
    Commented Dec 29, 2013 at 22:00
  • $\begingroup$ I normally don't vote for answers I cannot test, and I don't have ComponentMeasurements in version 7, but I like the overlaid values so much you get a +1 anyway. $\endgroup$
    – Mr.Wizard
    Commented Dec 29, 2013 at 22:35
  • $\begingroup$ I tried to apply your code to this PDF: dl.dropboxusercontent.com/u/8003134/grid2.pdf but boxes will return an empty list. How can I apply the procedure to this file? Thanks. $\endgroup$
    – dabd
    Commented Dec 30, 2013 at 16:01
  • $\begingroup$ @dabd are you rasterizing the graphic with Image before you attempt the manipulation? $\endgroup$
    – Mr.Wizard
    Commented Dec 30, 2013 at 16:26
  • $\begingroup$ @Mr.Wizard Yes I am using the code as shown above where it calls Image. When I evaluate this sub-expression for the new pdf (dl.dropboxusercontent.com/u/8003134/grid2.pdf) MorphologicalComponents[ Binarize[i, #[[1]] == #[[2]] == #[[3]] == 0 &]] it returns a huge list of lists with only zeros. $\endgroup$
    – dabd
    Commented Dec 30, 2013 at 17:26
9
$\begingroup$

Since your image elements are of uniform size on a rectangular grid you can use ImageParition to split them apart, and then a combination of ImageCrop and ImageTake to get just the internal areas. From there you can use ImageData to access the raster, and Tally to count the pixels by color.

Example:

count = Tally[Join @@ ImageData @ ImageCrop @ #] &;

i = Import["http://i.imgur.com/62buGws.png"];

cells = ImagePartition[i, {48, 48}, {50, 50}];

Map[count, cells, {2}][[2, 3]]
{{{0.188235, 0.188235, 0.188235}, 2}, {{0., 0., 0.}, 176}, {{1., 1., 1.}, 

616}, {{0.74902, 1., 0.74902}, 44}, {{0., 1., 0.}, 1276}, {{0.0470588, 0.0627451, 0.0470588}, 2}}

[[2, 3]] is to look at the square on the second row, third column. (The first row is blank.)

These are the tallies of the RGB values for each cell, including the black border. {{0.74902, 1., 0.74902}, 44} is the row of light green pixels on the margin between green and white. You'll have to decide how you want to count these. You can then process the values accordingly. This is a general approach since it counts all colors in the cell; you can find specific ratios, etc., using that data.

To count green pixels, defined as RGB values where G equals 1, weighted by saturation, we could use:

green = Tr @ Cases[#, {{x_, o_ /; o == 1, x_}, c_} :> (1 - x) c] &;

The Condition o_ /; o == 1 is used rather than the simpler 1. to make the match more robust.

Now:

dat = Map[Composition[green, count], Rest @ cells, {2}];

dat // First
{0., 1936., 1287.04, 1936., 1936., 1440.96, 1243.04, 1452.,
    902.086, 956.957, 956.957, 924., 968.}

If we know a priori the inside dimensions of each cell to be 44*44 we can find the ratio of green to white with:

dat / 44^2 // MatrixForm

enter image description here

ybeltukov uses a similar method that is more optimized for this particular operation, but I feel that using the image processing functions such as ImagePartition and ImageCrop offer additional features that you may find valuable in other applications, for example if each cell were to have a different size.

Edit: actually ybeltukov's code is not presently accurate because it does not use e.g. ImageCrop and it is not counting only the pixels inside the box.

$\endgroup$
3
  • $\begingroup$ Regarding the counting of the light shaded green squares is it possible to count them with a weight proportional to the green intensity, where RGBColor(0,1,0) would weight 1? What is the meaning of the cropping specifications {2,-2} and {2,-2}? $\endgroup$
    – dabd
    Commented Dec 29, 2013 at 16:49
  • $\begingroup$ @dabd I had to leave right after I wrote this so I didn't really finish it. Yes, I will show how to add that weighting and complete the count. The ImageTake operation was used to remove the black border, but we could also do without that operation and use the ratio of green to white squares only. I'll see which makes for simpler code and update my answer. $\endgroup$
    – Mr.Wizard
    Commented Dec 29, 2013 at 18:17
  • $\begingroup$ Really appreciate the detailed explanations and solutions! $\endgroup$
    – dabd
    Commented Dec 29, 2013 at 21:56
7
$\begingroup$

Note that your comment about the file being a PDF instead of a PNG changes the question, in that there ways to process a PDF that you cannot do to PNG.

First import the "Pages" of the PDF and stored the first (and only) one in i:

i = First @ Import["https://dl.dropboxusercontent.com/u/8003134/img.pdf", "Pages"];

Now the image is in fact scalable Graphics and the black squares and green rectangles are stored as JoinedCurve and FilledCurve objects:

Cases[i, _JoinedCurve, Infinity, 1]
Cases[i, _FilledCurve, Infinity, 1]
(*
   {JoinedCurve[{{{0, 2, 0}, {0, 1, 0}, {0, 1, 0}}},
                {{{2., 647.}, {47., 647.}, {47., 602.}, {2., 602.}}},
                CurveClosed -> {1}]}
   {FilledCurve[{{{0, 2, 0}, {0, 1, 0}, {0, 1, 0}}},
                {{{2., 597.}, {47., 597.}, {47., 552.}, {2., 552.}}}]}
*)

Note that the coordinates of the squares and rectangles are in the second argument.

Also upon inspection, the squares are stored in order by columns. So we can extract the squares, Partition them, and map the coordinates of a corner to the index of the square. The green rectangles share two of the coordinates with its enclosing square, so we pick a corner in common to both (the 4th coordinate). We can get the area of a green rectangle by subtracting the coordinates of opposite corners and multiplying, and store then as rules mapping corner to areas. We can get the total area of a square the same way.

corner2idx = With[{squares = Cases[i, JoinedCurve[_, {rect_}, ___] :> rect, Infinity]},
   Flatten @ MapIndexed[#[[4]] -> Reverse[#2] &, Partition[squares, 13], {2}]];

corner2area = With[{green = Cases[i, FilledCurve[_, {rect_}, ___] :> rect, Infinity]},
   #[[4]] -> Times @@ First@Differences[#[[{2, 4}]]] & /@ green];

totalArea = 
 Times @@ First @ Differences[
   Cases[i, JoinedCurve[_, {rect_}, ___] :> rect, Infinity, 1][[1, {2, 4}]]]
(*
   2025.
*)

Now we can put it all together, using the rules corner2idx that map corner coordinates to indices to create a list of array rules that map the index to the corresponding area. Dividing by the total area gives the proportion of green in each square.

proportions = SparseArray[corner2area /. corner2idx, {13, 13}, 0.]/totalArea;

Round[proportions, 0.02] // MatrixForm

Mathematica graphics

$\endgroup$
1
  • $\begingroup$ Thanks for the literate programming style explanation. I will have to study your code because I am not familiar with some functions and syntax you are using. $\endgroup$
    – dabd
    Commented Dec 29, 2013 at 18:14
5
$\begingroup$

One can use PartitionMap here

a = ImageData[Import["http://i.imgur.com/62buGws.png"]][[50 ;;, All, 1]];

p = Developer`PartitionMap[Total[#, 2] &, a, {50, 50}, 50, 1, 0];
p = Max[p] - p;
p = Round[p/Max[p], 0.01];

p // MatrixForm

enter image description here

I use the red channel because the green channel is the same for green and white areas.

$\endgroup$
3
  • $\begingroup$ This looks excellent! Thanks. One question: my original file is in PDF but if I use your code with it Mathematica issues an error. dl.dropboxusercontent.com/u/8003134/img.pdf How can I import it? Thanks. $\endgroup$
    – dabd
    Commented Dec 29, 2013 at 16:58
  • $\begingroup$ This does not appear to be accurate; unless I am mistaken you are not counting only the pixels inside the boxes. $\endgroup$
    – Mr.Wizard
    Commented Dec 29, 2013 at 19:18
  • $\begingroup$ @Mr.Wizard It adds a constant which I remove by Max[p] - p. There is a small errors because of the rasterization problems (especially in the last column). $\endgroup$
    – ybeltukov
    Commented Dec 29, 2013 at 19:35
4
$\begingroup$

Yet another way is using HistogramList and PixelValuePositions:

This will give an idea of why and that it works:

i = ImageCrop@Import["http://i.imgur.com/62buGws.png"];
Histogram3D[
 PixelValuePositions[i, Green], {12, 12}
 ]

histogram

Histogram3D uses HistogramList to gather the data. To generate the matrix the other answers have been able to generate we can do this:

N@(44 44)^-1 Reverse@Transpose@HistogramList[
      PixelValuePositions[i, Green], {12, 12}][[2]] // MatrixForm

44 44 is the size of one square.

$\endgroup$
1
  • $\begingroup$ Very nice! thanks $\endgroup$
    – dabd
    Commented Dec 31, 2013 at 0:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.