Here is one approach. See the section at the bottom about a few comments on how I chose the most important image processing parameters.
We start with your binarized image:
img = Import["https://i.sstatic.net/GAghg.png"]
The basic idea is to use the fact that the borders between particles seem to be nicely separated from the partciles themselves.
Next, we use MorphologicalComponents
and SelectComponents
to get the background:
bgImg = SelectComponents[MorphologicalComponents[ColorNegate[img], 0.99], Large] //
Unitize //
Colorize[#1, ColorRules -> {1 -> White}] &
Next, some cleaning:
procImg = bgImg //
Dilation[#, 2] & //
Closing[#, DiskMatrix@6] & //
ColorNegate
Now we can apply MorphologicalComponents
to get the individual particles, and then we use ArrayFilter
with Max
to grow them together (Update: I have updated the filter function to only apply Max
if the center cell is 0
- this ensures that the individual regions can only grow into the empty space. Additionally, I'm using Nest
to apply a filter with a smaller radius multiple times - this should help with growing all particles equally):
comps = procImg //
ImagePad[#, -2] & //
MorphologicalComponents[#, 0.5, CornerNeighbors -> False] & //
Nest[
ArrayFilter[
If[#[[3, 3]] == 0, Max@#, #[[3, 3]]] &,
#,
2
] &,
#,
2
] &;
Colorize@comps
The last step is to use ComponentMeasurements
with "Neighbours"
(to decide which edges to include) and "Centroid"
(to position the vertices) to build the graph:
ComponentMeasurements[comps, {"Neighbors", "Centroid"}, "PropertyComponentAssociation"] //
Graph[
DeleteDuplicates[Sort /@ Join @@ Thread /@ KeyValueMap[UndirectedEdge]@#Neighbors],
VertexCoordinates -> Normal@#Centroid,
VertexSize -> 0.7,
VertexStyle -> Yellow,
EdgeStyle -> Directive[Yellow, Thick],
PlotRange -> Transpose@{{0, 0}, ImageDimensions@img},
Prolog -> Inset[ImageMultiply[img, 0.7], Automatic, Automatic, Scaled@1]
] &
Choosing the parameters
A few notes on how I chose the parameters: The are three key parameters in the process above: The radius for Dilation
and Closing
, and the nesting parameter used for ArrayFilter
. In the following, I will briefly discuss each step. (You will notice that most parameters are not too critical, so making them a bit bigger might help to make the process more robust)
Dilation
:
The goal in this step is to make sure the individual particles are cleanly enclosed by the background. We do this by applying Dilation
with an appropriate radius. The following shows the effect of a few different values - essentially, as long as the tiny gaps are closed, the parameter is fine.
Row@Table[bgImg // Dilation[#, i] &, {i, 0, 3}]
Closing
:
This step is to remove small gaps in the background that are not real particles. The bigger the radius of the DiskMatrix
, the more holes are closed.
Row@Table[bgImg // Dilation[#, 2] & // Closing[#, DiskMatrix@i] &, {i, 2, 8, 2}]
ArrayFilter
:
This step is to grow the individual particles together, in order to decide which ones are adjacent. We do this by repeatedly (using Nest
) applying Max
based ArrayFilter
. The more often we apply the filter an the bigger the radius of the filter, the more the particles can be separated and still considered adjacent.
Row@Table[procImg //
ImagePad[#, -2] & //
MorphologicalComponents[#, 0.5, CornerNeighbors -> False] & //
With[{n = i},
ArrayFilter[
If[#[[n + 1, n + 1]] == 0, Max@#, #[[n + 1, n + 1]]] &,
#,
n
]
] & // Colorize, {i, 1, 13, 4}]
Note: I chose to use multiple applications of a smaller filter instead of one big one to make sure that all particles are grown more or less equally. Otherwise, the Max
part will always choose the particle with the biggest index to grow.
Estimating the z-coordinate of the particles
We can try to estimate the z-position of the particles by looking at the brightness of the particles in the individual image. To do this, we supply the raw image to ComponentMeasurements
together with the labeling mask (comps
), which allows us to use Mean
to get the average brightness of each particle.
rawImg = Import["https://i.sstatic.net/rUnvs.jpg"];
ComponentMeasurements[
{
ImagePad[
ColorConvert[
ImageResize[rawImg, ImageDimensions@img],(* make the image the same size *)
"GrayScale" (* convert to 1-channel image *)
],
-2
],
comps
},
{"Neighbors", "Centroid", "Mean", "Area"},
"PropertyComponentAssociation"
] //
Graph3D[
Table[Property[i, VertexSize -> Sqrt[#Area[i]/250]], {i,
Length@#Neighbors}] (* use the area for the size *),
DeleteDuplicates[Sort /@ Join @@ Thread /@ KeyValueMap[UndirectedEdge]@#Neighbors],
VertexCoordinates -> (* use the mean brightness as z-coordinate *)
Normal@Merge[Apply@Append]@{#Centroid, 500 #Mean},
EdgeStyle -> Directive[Blue, Thick],
PlotRange -> Append[All]@Transpose@{{0, 0}, ImageDimensions@img}
] &