<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Image Processing | Stephen Timothy Gordon II</title><link>https://stephentgordonii.com/tags/image-processing/</link><atom:link href="https://stephentgordonii.com/tags/image-processing/index.xml" rel="self" type="application/rss+xml"/><description>Image Processing</description><generator>HugoBlox Kit (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Sat, 01 Nov 2025 00:00:00 +0000</lastBuildDate><image><url>https://stephentgordonii.com/media/icon.svg</url><title>Image Processing</title><link>https://stephentgordonii.com/tags/image-processing/</link></image><item><title>Machine-Vision Surface Porosity Analysis</title><link>https://stephentgordonii.com/projects/machine-vision-porosity/</link><pubDate>Sat, 01 Nov 2025 00:00:00 +0000</pubDate><guid>https://stephentgordonii.com/projects/machine-vision-porosity/</guid><description>&lt;p&gt;Porosity affects the strength and durability of cementitious materials, and measuring it
normally means cutting the specimen. I wanted to estimate it from images instead, so the same
sample could still be used for other testing.&lt;/p&gt;
&lt;p&gt;This started as a continuation of another student&amp;rsquo;s project. Trinh Vo had used MATLAB&amp;rsquo;s Image
Processing Toolbox to estimate the porosity of thin plastic samples imaged by SEM. I extended
that approach to cementitious materials, working with fly ash geopolymer concrete, and to
images taken outside the SEM.&lt;/p&gt;
&lt;h2 id="specimens-and-imaging"&gt;Specimens and imaging&lt;/h2&gt;
&lt;p&gt;The samples were fly ash geopolymer concrete, labeled by the compressive strength each
cylinder reached at break. The images here were taken on a Hitachi S-4800 FESEM at 3 kV in
secondary electron mode, at 700x with a 50 micrometer scale bar. I also ran the pipeline on
ordinary macro photographs to see how it behaved outside the SEM.&lt;/p&gt;
&lt;h2 id="method"&gt;Method&lt;/h2&gt;
&lt;p&gt;The approach is simple. Load the image, convert it to grayscale, threshold it into a binary
image, and take the fraction of pore pixels.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-matlab" data-lang="matlab"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;imread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FILE_NAME&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;gray_img&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;binary_img&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gray_img&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;porosity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;binary_img&lt;/span&gt;&lt;span class="p"&gt;(:));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;figure&gt;&lt;img src="https://stephentgordonii.com/projects/machine-vision-porosity/pipeline-output.jpg"
alt="Pipeline output for one specimen. Original micrograph, grayscale conversion, and the binary image the porosity value is calculated from."&gt;&lt;figcaption&gt;
&lt;p&gt;Pipeline output for one specimen. Original micrograph, grayscale conversion, and the binary image the porosity value is calculated from.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="threshold-sensitivity"&gt;Threshold sensitivity&lt;/h2&gt;
&lt;p&gt;The original code used a fixed threshold of 70. That value does not carry from one image set
to the next.&lt;/p&gt;
&lt;p&gt;On one SEM image, a threshold of 70 gave a porosity of 0.0832, and raising it to 75 gave
0.0937. On a macro photograph, a threshold of 70 returned 0.0000, and the value did not begin
to move until roughly 72.&lt;/p&gt;
&lt;p&gt;Initial work assumed that the choice of threshold does not significantly affect the result when
the goal is ranking samples against each other. That holds only within a single imaging mode.
The absolute values move with the threshold, and a macro image can return zero porosity purely
as an artifact of it. Any use of this method needs a stated protocol for how the threshold was
set.&lt;/p&gt;
&lt;h2 id="what-i-changed"&gt;What I changed&lt;/h2&gt;
&lt;p&gt;To reduce the dependence on a hand-picked number, I replaced the fixed threshold with
&lt;code&gt;imbinarize&lt;/code&gt;, which selects a threshold from the image histogram rather than from a constant
in the script.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-matlab" data-lang="matlab"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;gray_img&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;im2gray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;BI&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;imbinarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gray_img&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;porosity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BI&lt;/span&gt;&lt;span class="p"&gt;(:));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I also tested adaptive thresholding for images with uneven lighting, and set up a script to
try classifying pore regions with the Neural Network Toolbox, though I did not take that past
initial setup.&lt;/p&gt;
&lt;h2 id="extension-to-object-counting"&gt;Extension to object counting&lt;/h2&gt;
&lt;p&gt;The same front end was later reused for counting instead of area measurement. I developed this
for oyster habitat rehabilitation research I worked on at Louisiana Tech, where the task was
counting oysters in photographs of shell substrate.&lt;/p&gt;
&lt;p&gt;Porosity only needs the fraction of dark pixels, so the pixels do not have to be grouped into
anything. Counting does. Two steps were added after binarization. &lt;code&gt;bwareaopen&lt;/code&gt; discards regions
below a set pixel area, which clears the speckle that would otherwise be counted as objects.
&lt;code&gt;bwconncomp&lt;/code&gt; then labels the connected regions and returns how many there are. A hole-filled
version is generated alongside them to check how the binary image is holding together.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-matlab" data-lang="matlab"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;BI&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;imbinarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;im2gray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;BW&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;BI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;% objects dark, background white&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;denoise&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bwareaopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c"&gt;% drop regions under 50 pixels&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;cc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bwconncomp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BW&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;cc2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bwconncomp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;denoise&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;num&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NumObjects&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;num_denoise&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cc2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NumObjects&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Reporting the raw count alongside the denoised count shows how much of the result came from
noise.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://stephentgordonii.com/projects/machine-vision-porosity/oyster-count-output.jpg"
alt="Counting output for a substrate photograph, showing the original image and the binary, denoised, and hole-filled stages. This run returned a count of 337."&gt;&lt;figcaption&gt;
&lt;p&gt;Counting output for a substrate photograph, showing the original image and the binary, denoised, and hole-filled stages. This run returned a count of 337.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Dense substrate is the hard case. Shells that touch merge into a single connected region and
undercount, which is visible in the binary panels above as large joined masses rather than
separate objects. The count is most reliable when the objects are well separated.&lt;/p&gt;
&lt;h2 id="limitations"&gt;Limitations&lt;/h2&gt;
&lt;p&gt;This measures surface porosity in the imaged field, not bulk porosity. It cannot see closed
pores below the surface, and it is sensitive to surface preparation, lighting, and
magnification. I treat it as a fast comparative measure between specimens imaged the same way,
not as a replacement for a standard porosity test.&lt;/p&gt;</description></item></channel></rss>