Skip to content

Instantly share code, notes, and snippets.

@tslater2006
Created September 9, 2021 21:57
Show Gist options
  • Select an option

  • Save tslater2006/ba29de5e02be058e52f30a0e34eaef05 to your computer and use it in GitHub Desktop.

Select an option

Save tslater2006/ba29de5e02be058e52f30a0e34eaef05 to your computer and use it in GitHub Desktop.
using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Structure;
using Emgu.CV.Util;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace ResinTrapPlayground
{
public partial class FindResinTraps : Form
{
/* The following is a novel implementation of resin/suction trap detection for MSLA sliced images *
* It largely consists of 2 passes through the layers.
*
* The 1st pass processes the layers from the bottom to the top, keeping track of what areas in
* the previous layer are air (or connected to air in some way). The initial air map is derived from
* the starting layer, by inverting the layer and then filling all external contours black (not air)
* which results in an inital air map of everything that is not solid, or inside a solid, being considered
* air. While traversing the layers, the air map is updated to include any new air regions, or hollow areas
* that intersect with the air map (since while they are not air, they are connected to air). Any regions
* that do not overlap the air map are declared resin traps.
*
* The 2nd pass processes the layers from the top to the bottom, the initial air map differs from
* the 1st pass. For the 2nd pass, anything that is not solid is considered to be air (a bitwise
* not of the top layer is used). The airmap is updated per layer just like it was in the 1st pass,
* however we do not test all contours of the layer, only those that were marked as resin traps by
* the 1st pass. This is acceptable because in the event we did test all hollow areas, and one of those
* was *not* detected on 1st pass, it must be air-accessible from the bottom. In the even that a contour
* we are testing does overlap the air map during the 2nd pass, it is cleared from the "resin trap" list
* and instead added to the "suction trap" list. This is to indicate that while there is access to air from
* the top, there are regions that could be affected by vacuum/suction forces during the print
*/
public FindResinTraps()
{
//string[] files = Directory.GetFiles(@"C:\Users\tslat\Downloads\RERF\R_E_R_F", "layer*.png");
/** Just a bunch of code to load layer images from disk (extracted by UVTools) and store as PNG data **/
/** This is done this way so the PNG data in memory is of a grayscale image, not full color in case that matters **/
string[] files = Directory.GetFiles(@"C:\Users\tslat\Downloads\ResinTrapTests\hollow_yoda_no_print", "layer*.png");
int layerCount = files.Length;
List<byte[]> layerData = new List<byte[]>();
var loadedMats = new Mat[files.Length];
string windowName = "Debug"; //The name of the window
CvInvoke.NamedWindow(windowName);
for (var x = 0; x < loadedMats.Length; x++)
{
loadedMats[x] = new Mat(files[x], ImreadModes.Grayscale);
CvInvoke.Threshold(loadedMats[x], loadedMats[x], 1, byte.MaxValue, ThresholdType.Binary);
layerData.Add(CvInvoke.Imencode(".png", loadedMats[x]));
loadedMats[x].Dispose();
}
Stopwatch sw = new Stopwatch();
sw.Start();
var (traps, suctions) = FindResinAndSuctionTraps(layerData, layerCount);
sw.Stop();
int layersWithTraps = traps.Where(t => t != null && t.Count > 0).Count();
int layersWithSuction = suctions.Where(t => t != null && t.Count > 0).Count();
InitializeComponent();
}
public (List<VectorOfVectorOfPoint>[], List<VectorOfVectorOfPoint>[]) FindResinAndSuctionTraps(List<byte[]> layerData, int layerCount)
{
/* these hold an array (one element for each layer), that has a List of contours, or null */
List<VectorOfVectorOfPoint>[] resinTraps = new List<VectorOfVectorOfPoint>[layerCount];
List<VectorOfVectorOfPoint>[] suctionTraps = new List<VectorOfVectorOfPoint>[layerCount];
/* this maintains a Mat that reflects the current air-accessible region for the layer that is being processed, black is no air, white is air (or air accessible) */
Mat currentAirMap = null;
/* the first pass does bottom to top, and tracks anything it thinks is a resin trap */
for (var layerIndex = 0; layerIndex < layerData.Count; layerIndex++)
{
using Mat curLayer = new();
CvInvoke.Imdecode(layerData[layerIndex], ImreadModes.Grayscale, curLayer);
var layerAirMap = GenerateAirMap(curLayer);
if (layerIndex == 0)
{
currentAirMap = layerAirMap.Clone();
}
/* remove solid areas of current layer from the air map */
CvInvoke.Subtract(currentAirMap, curLayer, currentAirMap);
/* add in areas of air in current layer to air map */
CvInvoke.BitwiseOr(layerAirMap, currentAirMap, currentAirMap);
/* all done with this mat, clean it up */
layerAirMap.Dispose();
/* find hollows of current layer */
using VectorOfVectorOfPoint contours = new();
using Mat hierarchy = new();
CvInvoke.FindContours(curLayer, contours, hierarchy, RetrType.Tree, ChainApproxMethod.ChainApproxSimple);
var arr = hierarchy.GetData();
/* hollow contour grouping */
var hollowGroups = new List<VectorOfVectorOfPoint>();
var processedContours = new bool[contours.Size];
for (int i = 0; i < contours.Size; i++)
{
if (processedContours[i]) continue;
processedContours[i] = true;
if ((int)arr.GetValue(0, i, 3) == -1) continue;
hollowGroups.Add(new VectorOfVectorOfPoint(contours[i]));
for (int n = i + 1; n < contours.Size; n++)
{
if (processedContours[n] || (int)arr.GetValue(0, n, 3) != i) continue;
processedContours[n] = true;
hollowGroups[^1].Push(contours[n]);
}
}
for (var i = 0; i < hollowGroups.Count; i++)
{
/* intersect current contour, with the current airmap. */
Mat currentContour = Mat.Zeros(curLayer.Height, curLayer.Width, curLayer.Depth, curLayer.NumberOfChannels);
CvInvoke.DrawContours(currentContour, hollowGroups[i], -1, new MCvScalar(255, 255, 255, 255), -1);
Mat airOverlap = currentAirMap.Clone();
CvInvoke.BitwiseAnd(currentAirMap, currentContour, airOverlap);
int overlapCount = CvInvoke.CountNonZero(airOverlap);
if (overlapCount == 0)
{
/* this countour does *not* overlap known air */
if (resinTraps[layerIndex] == null)
{
resinTraps[layerIndex] = new List<VectorOfVectorOfPoint>();
}
/* add a resin trap (for now... will be revisited in part 2) */
resinTraps[layerIndex].Add(hollowGroups[i]);
}
else
{
/* this contour does overlap air, add it to the current air map */
CvInvoke.BitwiseOr(currentContour, currentAirMap, currentAirMap);
}
/* cleanup */
currentContour.Dispose();
airOverlap.Dispose();
};
}
/* starting over again but this time from the top to the bottom */
currentAirMap = null;
for (var layerIndex = resinTraps.Length - 1; layerIndex >= 0; layerIndex--)
{
using Mat curLayer = new();
CvInvoke.Imdecode(layerData[layerIndex], ImreadModes.Grayscale, curLayer);
if (layerIndex == resinTraps.Length - 1)
{
/* this is subtly different that for the first pass, we don't use GenerateAirMap for the initial airmap */
/* instead we use a bitwise not, this way anything that is open/hollow on the top layer is treated as air */
currentAirMap = curLayer.Clone();
CvInvoke.BitwiseNot(curLayer, currentAirMap);
}
/* we still modify the airmap like normal, where we account for the air areas of the layer, and any contours that might overlap...*/
var layerAirMap = GenerateAirMap(curLayer);
/* remove solid areas of current layer from the air map */
CvInvoke.Subtract(currentAirMap, curLayer, currentAirMap);
/* add in areas of air in current layer to air map */
CvInvoke.BitwiseOr(layerAirMap, currentAirMap, currentAirMap);
layerAirMap.Dispose();
if (resinTraps[layerIndex] != null)
{
/* here we don't worry about finding contours on the layer, the bottom to top pass did that already */
/* all we care about is contours the first pass thought were resin traps, since there was no access to air from the bottom */
for (var x = 0; x < resinTraps[layerIndex].Count; x++)
{
/* check if each contour overlaps known air */
Mat currentContour = Mat.Zeros(curLayer.Height, curLayer.Width, curLayer.Depth, curLayer.NumberOfChannels);
CvInvoke.DrawContours(currentContour, resinTraps[layerIndex][x], -1, new MCvScalar(255, 255, 255, 255), -1);
Mat airOverlap = currentAirMap.Clone();
CvInvoke.BitwiseAnd(currentAirMap, currentContour, airOverlap);
int overlapCount = CvInvoke.CountNonZero(airOverlap);
/* if this trap still doesn't overlap with air from the top, its an actual resin trap - leave it as is*/
if (overlapCount > 0) {
/* this contour does overlap air, add this it our air map */
CvInvoke.BitwiseOr(currentContour, currentAirMap, currentAirMap);
/* if we haven't defined a suctionTrap list for this layer, do so */
if (suctionTraps[layerIndex] == null)
{
suctionTraps[layerIndex] = new List<VectorOfVectorOfPoint>();
}
/* since we know it isn't a resin trap, it becomes a suction trap */
suctionTraps[layerIndex].Add(resinTraps[layerIndex][x]);
/* to keep things tidy while we iterate resin traps, it will be left in the list for now, and removed later */
}
currentContour.Dispose();
airOverlap.Dispose();
};
/* anything that converted to a suction trap needs to removed from resinTraps. Loop backwards so indexes don't shift */
if (suctionTraps[layerIndex] != null)
{
for (var x = suctionTraps[layerIndex].Count - 1; x >= 0; x--)
{
resinTraps[layerIndex].Remove(suctionTraps[layerIndex][x]);
if (resinTraps[layerIndex].Count == 0)
{
resinTraps[layerIndex] = null;
}
}
}
}
}
/* some rendering code that draws resin traps blue, and suction traps green */
for (var layerIndex = 0; layerIndex < resinTraps.Length; layerIndex++)
{
using Mat curLayer = new();
CvInvoke.Imdecode(layerData[layerIndex], ImreadModes.Color, curLayer);
if (resinTraps[layerIndex] != null && resinTraps[layerIndex].Count > 0)
{
foreach (var r in resinTraps[layerIndex])
{
CvInvoke.DrawContours(curLayer, r, -1, new MCvScalar(255, 0, 0, 255), -1);
}
}
if (suctionTraps[layerIndex] != null)
{
foreach (var s in suctionTraps[layerIndex])
{
CvInvoke.DrawContours(curLayer, s, -1, new MCvScalar(0, 255, 0, 255), -1);
}
}
ShowImage(curLayer);
}
CvInvoke.DestroyAllWindows();
/* return the arrays we found */
return (resinTraps,suctionTraps);
}
public void ShowImage(Mat img)
{
Mat resized = Mat.Zeros(img.Height, img.Width, img.Depth, img.NumberOfChannels);
//CvInvoke.Resize(img, resized, new Size(1024, 576));
CvInvoke.Resize(img, resized, new Size((int)(1024 * ((double)img.Width/img.Height)), 1024));
CvInvoke.Imshow("Debug", resized);
resized.Dispose();
CvInvoke.WaitKey(40);
}
public Mat GenerateAirMap(Mat input)
{
var clone = input.Clone();
CvInvoke.BitwiseNot(input, clone);
using VectorOfVectorOfPoint contours = new();
CvInvoke.FindContours(input, contours, null, RetrType.External, ChainApproxMethod.ChainApproxSimple);
CvInvoke.DrawContours(clone, contours, -1, new MCvScalar(0,0,0, 255), -1);
return clone;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment