Files
Tiago Conceição a626cfdc72 v3.12.0
- (Add) Allow to pause and resume operations (#654)
- (Add) `Layer.FirstTransitionLayer`
- (Add) `Layer.LastTransitionLayer`
- (Add) File format: Elegoo GOO
- (Add) PrusaSlicer Printer: Elegoo Mars 4
- (Improvement) Allocate maximum GPU memory for Skia up to 256 MB
- (Improvement) Set and sanitize transition layers exposure time from last bottom layer and first normal layer instead of global times (#659)
- (Change) CXDLP: Default version from 2 to 3
- (Fix) UI was not rendering with GPU (ANGLE)
- (Fix) `Layer.IsTransitionLayer` was returning the wrong value
- (Upgrade) .NET from 6.0.13 to 6.0.14
2023-02-27 03:22:40 +00:00

1182 lines
60 KiB
C#

using CommunityToolkit.HighPerformance;
using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Util;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using UVtools.Core.EmguCV;
using UVtools.Core.Extensions;
using UVtools.Core.FileFormats;
using UVtools.Core.Layers;
using UVtools.Core.Operations;
using UVtools.Core.PixelEditor;
namespace UVtools.Core.Managers;
public sealed class IssueManager : RangeObservableCollection<MainIssue>
{
public FileFormat SlicerFile { get; }
public List<MainIssue> IgnoredIssues { get; } = new();
public bool HaveIssues => Count > 0;
public IssueManager(FileFormat slicerFile)
{
SlicerFile = slicerFile;
}
/// <summary>
/// Gets the visible <see cref="MainIssue"/> aka not ignored
/// </summary>
/// <returns></returns>
public MainIssue[] GetVisible()
{
return this.Where(mainIssue => !IgnoredIssues.Contains(mainIssue)).ToArray();
}
public static Issue[] GetIssues(IEnumerable<MainIssue> issues)
{
var result = new List<Issue>();
foreach (var mainIssue in issues)
{
result.AddRange(mainIssue);
}
return result.ToArray();
}
public static Issue[] GetIssuesBy(IEnumerable<MainIssue> issues, MainIssue.IssueType type, uint layerIndex)
{
var result = new List<Issue>();
foreach (var mainIssue in issues)
{
if (mainIssue.Type != type) continue;
if (!mainIssue.IsIssueInBetween(layerIndex)) continue;
foreach (var issue in mainIssue)
{
if (issue.LayerIndex != layerIndex) continue;
result.Add(issue);
}
}
return result.ToArray();
}
public static Issue[] GetIssuesBy(IEnumerable<MainIssue> issues, MainIssue.IssueType type)
{
var result = new List<Issue>();
foreach (var mainIssue in issues)
{
if (mainIssue.Type != type) continue;
result.AddRange(mainIssue);
}
return result.ToArray();
}
public static Issue[] GetIssuesBy(IEnumerable<MainIssue> issues, uint layerIndex)
{
var result = new List<Issue>();
foreach (var mainIssue in issues)
{
if (!mainIssue.IsIssueInBetween(layerIndex)) continue;
foreach (var issue in mainIssue)
{
if (issue.LayerIndex != layerIndex) continue;
result.Add(issue);
}
}
return result.ToArray();
}
public Issue[] GetIssues()
{
return GetIssues(this);
}
public Issue[] GetIssuesBy(MainIssue.IssueType type)
{
return GetIssuesBy(this, type);
}
public Issue[] GetIssuesBy(MainIssue.IssueType type, uint layerIndex)
{
return GetIssuesBy(this, type, layerIndex);
}
public Issue[] GetIssuesBy(uint layerIndex)
{
return GetIssuesBy(this, layerIndex);
}
public List<MainIssue> DetectIssues(IssuesDetectionConfiguration? config = null, OperationProgress? progress = null)
{
if (SlicerFile.DecodeType == FileFormat.FileDecodeType.Partial) return new List<MainIssue>();
config ??= new IssuesDetectionConfiguration();
var (
islandConfig,
overhangConfig,
resinTrapConfig,
touchBoundConfig,
printHeightConfig,
emptyLayerConfig
) = config;
progress ??= new OperationProgress();
var result = new ConcurrentBag<MainIssue>();
//var layerHollowAreas = new ConcurrentDictionary<uint, List<LayerHollowArea>>();
var resinTraps = new List<VectorOfVectorOfPoint>[SlicerFile.LayerCount];
var suctionCups = new List<VectorOfVectorOfPoint>[SlicerFile.LayerCount];
var externalContours = new VectorOfVectorOfPoint[SlicerFile.LayerCount];
var hollows = new List<VectorOfVectorOfPoint>[SlicerFile.LayerCount];
var airContours = new List<VectorOfVectorOfPoint>[SlicerFile.LayerCount];
var resinTrapsContoursArea = new double[SlicerFile.LayerCount][];
bool IsIgnored(MainIssue issue) => IgnoredIssues.Count > 0 && IgnoredIssues.Contains(issue);
bool AddIssue(MainIssue issue)
{
if (IsIgnored(issue)) return false;
result.Add(issue);
return true;
}
List<MainIssue> GetResult()
{
return result.OrderBy(mainIssue => mainIssue.Type).ThenBy(issue => issue.StartLayerIndex).ThenByDescending(issue => issue.Area).ToList();
}
void GenerateAirMap(IInputArray input, IInputOutputArray output, VectorOfVectorOfPoint? externals)
{
CvInvoke.BitwiseNot(input, output);
if (externals is null || externals.Size == 0) return;
CvInvoke.DrawContours(output, externals, -1, EmguExtensions.BlackColor, -1);
}
if (printHeightConfig.Enabled && SlicerFile.MachineZ > 0)
{
float printHeightWithOffset = Layer.RoundHeight(SlicerFile.MachineZ + printHeightConfig.Offset);
if (SlicerFile.PrintHeight > printHeightWithOffset)
{
var issues = (from layer in SlicerFile where layer.PositionZ > printHeightWithOffset select new Issue(layer)).ToList();
if(issues.Count > 0) AddIssue(new MainIssue(MainIssue.IssueType.PrintHeight, issues));
}
}
if (emptyLayerConfig.Enabled)
{
for (var layerIndex = 0; layerIndex < SlicerFile.Count; layerIndex++)
{
var layer = SlicerFile[layerIndex];
if (!layer.IsEmpty) continue;
if (!emptyLayerConfig.IgnoreStartingEmptyLayers
&& emptyLayerConfig is {IgnoreLooseEmptyLayers: false, IgnoreEndingEmptyLayers: false})
{
AddIssue(new MainIssue(MainIssue.IssueType.EmptyLayer, new Issue(layer)));
continue;
}
// 1 = Starting
// 2 = Loose
// 3 = Ending
byte emptyLayerPosType = 0;
int i;
for (i = 0; i < layerIndex && SlicerFile[i].IsEmpty; layerIndex++) { }
if (i == layerIndex)
{
emptyLayerPosType = 1;
}
else
{
for (i = (int) SlicerFile.LastLayerIndex; i > layerIndex && SlicerFile[i].IsEmpty; layerIndex--) { }
emptyLayerPosType = i == layerIndex ? (byte) 3 : (byte) 2;
}
if (emptyLayerPosType == 1)
{
if(!emptyLayerConfig.IgnoreStartingEmptyLayers) AddIssue(new MainIssue(MainIssue.IssueType.EmptyLayer, new Issue(layer)));
}
else if (emptyLayerPosType == 2)
{
if (!emptyLayerConfig.IgnoreLooseEmptyLayers) AddIssue(new MainIssue(MainIssue.IssueType.EmptyLayer, new Issue(layer)));
}
else if (emptyLayerPosType == 3)
{
if (!emptyLayerConfig.IgnoreEndingEmptyLayers) AddIssue(new MainIssue(MainIssue.IssueType.EmptyLayer, new Issue(layer)));
}
else
{
AddIssue(new MainIssue(MainIssue.IssueType.EmptyLayer, new Issue(layer)));
}
}
}
if (islandConfig.Enabled || overhangConfig.Enabled || resinTrapConfig.Enabled || touchBoundConfig.Enabled)
{
progress.Reset(OperationProgress.StatusIslands, SlicerFile.LayerCount);
var firstLayer = SlicerFile.FirstLayer;
int overhangsIterations = overhangConfig.ErodeIterations;
using var overhangsKernel = EmguExtensions.GetDynamicKernel(ref overhangsIterations, ElementShape.Cross);
// Detect contours
Parallel.For(0, SlicerFile.LayerCount, CoreSettings.ParallelOptions, layerIndexInt =>
{
progress.PauseIfRequested();
if (progress.Token.IsCancellationRequested) return;
uint layerIndex = (uint)layerIndexInt;
var layer = SlicerFile[layerIndex];
if (layer.IsEmpty)
{
progress.LockAndIncrement();
return;
}
// Spare a decoding cycle
if (!touchBoundConfig.Enabled &&
!resinTrapConfig.Enabled &&
(!overhangConfig.Enabled || overhangConfig.Enabled && (layerIndex == 0 || layer.PositionZ <= firstLayer!.PositionZ || overhangConfig.WhiteListLayers is not null && !overhangConfig.WhiteListLayers.Contains(layerIndex))) &&
(!islandConfig.Enabled || islandConfig.Enabled && (layerIndex == 0 || layer.PositionZ <= firstLayer!.PositionZ || islandConfig.WhiteListLayers is not null && !islandConfig.WhiteListLayers.Contains(layerIndex)))
)
{
progress.LockAndIncrement();
return;
}
using (var image = layer.GetLayerMat(layerIndex == 0 ? SlicerFile.BoundingRectangle : Layer.GetBoundingRectangleUnion(SlicerFile[layerIndex - 1], layer)))
{
var sourceSpan = image.SourceMat.GetDataByteSpan2D();
var roiSpan = image.RoiMat.GetDataByteSpan2D();
if (touchBoundConfig.Enabled)
{
// TouchingBounds Checker
List<Point> pixels = new();
bool touchTop = layer.BoundingRectangle.Top <= touchBoundConfig.MarginTop;
bool touchBottom = layer.BoundingRectangle.Bottom >= image.SourceMat.Height - touchBoundConfig.MarginBottom;
bool touchLeft = layer.BoundingRectangle.Left <= touchBoundConfig.MarginLeft;
bool touchRight = layer.BoundingRectangle.Right >= image.SourceMat.Width - touchBoundConfig.MarginRight;
int minx = int.MaxValue;
int miny = int.MaxValue;
int maxx = 0;
int maxy = 0;
if (touchTop || touchBottom)
{
for (int x = layer.BoundingRectangle.X; x < layer.BoundingRectangle.Right; x++) // Check Top and Bottom bounds
{
if (touchTop)
{
for (int y = layer.BoundingRectangle.Y; y < touchBoundConfig.MarginTop; y++) // Top
{
if (sourceSpan.DangerousGetReferenceAt(y, x) >= touchBoundConfig.MinimumPixelBrightness)
{
pixels.Add(new Point(x, y));
minx = Math.Min(minx, x);
miny = Math.Min(miny, y);
maxx = Math.Max(maxx, x);
maxy = Math.Max(maxy, y);
}
}
}
if (touchBottom)
{
for (int y = image.SourceMat.Height - touchBoundConfig.MarginBottom;
y < layer.BoundingRectangle.Bottom;
y++) // Bottom
{
if (sourceSpan.DangerousGetReferenceAt(y, x) >= touchBoundConfig.MinimumPixelBrightness)
{
pixels.Add(new Point(x, y));
minx = Math.Min(minx, x);
miny = Math.Min(miny, y);
maxx = Math.Max(maxx, x);
maxy = Math.Max(maxy, y);
}
}
}
}
}
if (touchLeft || touchRight)
{
for (int y = layer.BoundingRectangle.Y + touchBoundConfig.MarginTop;
y < layer.BoundingRectangle.Bottom - touchBoundConfig.MarginBottom;
y++) // Check Left and Right bounds
{
if (touchLeft)
{
for (int x = layer.BoundingRectangle.X; x < touchBoundConfig.MarginLeft; x++) // Left
{
if (sourceSpan.DangerousGetReferenceAt(y, x) >= touchBoundConfig.MinimumPixelBrightness)
{
pixels.Add(new Point(x, y));
minx = Math.Min(minx, x);
miny = Math.Min(miny, y);
maxx = Math.Max(maxx, x);
maxy = Math.Max(maxy, y);
}
}
}
if (touchRight)
{
for (int x = layer.BoundingRectangle.Right - touchBoundConfig.MarginRight;
x < layer.BoundingRectangle.Right;
x++) // Right
{
if (sourceSpan.DangerousGetReferenceAt(y, x) >= touchBoundConfig.MinimumPixelBrightness)
{
pixels.Add(new Point(x, y));
minx = Math.Min(minx, x);
miny = Math.Min(miny, y);
maxx = Math.Max(maxx, x);
maxy = Math.Max(maxy, y);
}
}
}
}
}
if (pixels.Count > 0)
{
AddIssue(new MainIssue(MainIssue.IssueType.TouchingBound, new IssueOfPoints(layer, pixels,
new Rectangle(minx, miny, maxx - minx + 1, maxy - miny + 1))));
}
}
if (layerIndex > 0 && layer.PositionZ > firstLayer!.PositionZ) // No islands nor overhangs for layer 0 or on plate
{
MatRoi? previousImage = null;
Span2D<byte> previousSpan = null;
Mat? overhangImage = null;
var previousLayer = SlicerFile[layerIndex - 1];
// Overhangs
//var overhangCount = 0;
var overhangs = new List<MainIssue>();
//if (!islandConfig.Enabled && overhangConfig.Enabled ||
// (islandConfig.Enabled && overhangConfig.Enabled && overhangConfig.IndependentFromIslands))
if (overhangConfig.Enabled)
{
bool canProcessCheck = true;
if (overhangConfig.WhiteListLayers is not null) // Check white list
{
if (!overhangConfig.WhiteListLayers.Contains(layerIndex))
{
canProcessCheck = false;
}
}
if (canProcessCheck)
{
previousImage ??= previousLayer.GetLayerMat(Layer.GetBoundingRectangleUnion(previousLayer, layer));
overhangImage = new Mat();
using var vecPoints = new VectorOfPoint();
CvInvoke.Subtract(image.RoiMat, previousImage.RoiMat, overhangImage);
CvInvoke.Threshold(overhangImage, overhangImage, 127, 255, ThresholdType.Binary);
CvInvoke.Erode(overhangImage, overhangImage, overhangsKernel,
EmguExtensions.AnchorCenter, overhangsIterations, BorderType.Default, default);
//CvInvoke.MorphologyEx(subtractedImage, subtractedImage, MorphOp.Open, EmguExtensions.Kernel3x3Rectangle,
// EmguExtensions.AnchorCenter, 2, BorderType.Reflect101, default);
using var contours = overhangImage.FindContours(out var hierarchy, RetrType.Tree, ChainApproxMethod.ChainApproxSimple, image.RoiLocation);
var contoursInGroups = EmguContours.GetPositiveContoursInGroups(contours, hierarchy);
foreach (var contourGroup in contoursInGroups)
{
if (contourGroup[0].Size < 3) continue; // Single contour, single line, ignore
var area = EmguContours.GetContourArea(contourGroup);
if (area >= overhangConfig.RequiredPixelsToConsider)
{
var rect = CvInvoke.BoundingRectangle(contourGroup[0]);
var overhangIssue = new MainIssue(MainIssue.IssueType.Overhang, new IssueOfContours(layer, contourGroup.ToArrayOfArray(), rect, area));
overhangs.Add(overhangIssue);
AddIssue(overhangIssue);
}
}
}
}
if (islandConfig.Enabled)
{
bool canProcessCheck = true;
if (islandConfig.WhiteListLayers is not null) // Check white list
{
if (!islandConfig.WhiteListLayers.Contains(layerIndex))
{
canProcessCheck = false;
}
}
if (canProcessCheck)
{
bool needDispose = false;
Mat islandImage;
if (islandConfig.BinaryThreshold > 0)
{
needDispose = true;
islandImage = new();
CvInvoke.Threshold(image.RoiMat, islandImage, islandConfig.BinaryThreshold, byte.MaxValue, ThresholdType.Binary);
}
else
{
islandImage = image.RoiMat;
}
using Mat labels = new();
using Mat stats = new();
using Mat centroids = new();
var numLabels = CvInvoke.ConnectedComponentsWithStats(islandImage, labels, stats,
centroids,
islandConfig.AllowDiagonalBonds
? LineType.EightConnected
: LineType.FourConnected);
if (needDispose)
{
islandImage.Dispose();
}
// Get array that contains details of each connected component
//var ccStats = stats.GetData();
//stats[i][0]: Left Edge of Connected Component
//stats[i][1]: Top Edge of Connected Component
//stats[i][2]: Width of Connected Component
//stats[i][3]: Height of Connected Component
//stats[i][4]: Total Area (in pixels) in Connected Component
var ccStats = stats.GetDataSpan<int>();
var labelSpan = labels.GetDataSpan2D<int>();
for (int i = 1; i < numLabels; i++)
{
int pos = i * stats.Cols;
Rectangle rect = new(
ccStats[pos + (int)ConnectedComponentsTypes.Left],
ccStats[pos + (int)ConnectedComponentsTypes.Top],
ccStats[pos + (int)ConnectedComponentsTypes.Width],
ccStats[pos + (int)ConnectedComponentsTypes.Height]
/*(int)ccStats.GetValue(i, (int)ConnectedComponentsTypes.Left)!,
(int)ccStats.GetValue(i, (int)ConnectedComponentsTypes.Top)!,
(int)ccStats.GetValue(i, (int)ConnectedComponentsTypes.Width)!,
(int)ccStats.GetValue(i, (int)ConnectedComponentsTypes.Height)!*/
);
if (ccStats[pos + (int)ConnectedComponentsTypes.Area] < islandConfig.RequiredAreaToProcessCheck)
continue;
previousImage ??= previousLayer.GetLayerMat(Layer.GetBoundingRectangleUnion(previousLayer, layer));
if (previousSpan == null)
{
previousSpan = previousImage.RoiMat.GetDataByteSpan2D();
}
List<Point> points = new();
uint pixelsSupportingIsland = 0;
for (int y = rect.Y; y < rect.Bottom; y++)
for (int x = rect.X; x < rect.Right; x++)
{
if (labelSpan.DangerousGetReferenceAt(y, x) != i || // Background pixel or a pixel from another component within the bounding rectangle
roiSpan.DangerousGetReferenceAt(y, x) < islandConfig.RequiredPixelBrightnessToProcessCheck // Low brightness, ignore
) continue;
points.Add(new Point(image.Roi.X + x, image.Roi.Y + y));
//int pixel = roiStep * y + x;
if (previousSpan.DangerousGetReferenceAt(y, x) >= islandConfig.RequiredPixelBrightnessToSupport)
{
pixelsSupportingIsland++;
}
}
if (points.Count == 0) continue; // Should never happen
var requiredSupportingPixels = Math.Max(1, points.Count * islandConfig.RequiredPixelsToSupportMultiplier);
/*if (pixelsSupportingIsland >= islandConfig.RequiredPixelsToSupport)
isIsland = false; // Not a island, bounding is strong, i think...
else if (pixelsSupportingIsland > 0 &&
points.Count < islandConfig.RequiredPixelsToSupport &&
pixelsSupportingIsland >= Math.Max(1, points.Count / 2))
isIsland = false; // Not a island, but maybe weak bounding...*/
if (pixelsSupportingIsland >= requiredSupportingPixels) continue;
var islandBoundingRectangle = rect.OffsetBy(image.RoiLocation);
// Check for overhangs in islands
if (islandConfig.EnhancedDetection && pixelsSupportingIsland >= 10 && pixelsSupportingIsland >= requiredSupportingPixels / 4)
// && (!overhangConfig.Enabled || (overhangConfig.Enabled && overhangCount > 0))
{
if (overhangConfig.Enabled && // No overhangs nor intersecting = discard island
overhangs.TrueForAll(overhang => !overhang.BoundingRectangle.IntersectsWith(islandBoundingRectangle)))
{
continue;
}
using var islandRoi = image.RoiMat.Roi(rect);
using var previousIslandRoi = previousImage.RoiMat.Roi(rect);
var islandOverhangMat = overhangImage;
if (islandOverhangMat is null)
{
islandOverhangMat = new Mat();
CvInvoke.Subtract(islandRoi, previousIslandRoi, islandOverhangMat);
CvInvoke.Threshold(islandOverhangMat, islandOverhangMat, 127, 255, ThresholdType.Binary);
CvInvoke.Erode(islandOverhangMat, islandOverhangMat, overhangsKernel,
EmguExtensions.AnchorCenter, overhangsIterations, BorderType.Default, default);
}
using var subtractedImage = islandOverhangMat.Roi(rect);
var subtractedSpan = subtractedImage.GetDataByteSpan2D();
var subtractedStep = subtractedImage.GetRealStep();
int overhangPixels = 0;
for (int y = 0; y < subtractedImage.Height && overhangPixels < overhangConfig.RequiredPixelsToConsider; y++)
for (int x = 0; x < subtractedStep && overhangPixels < overhangConfig.RequiredPixelsToConsider; x++)
{
int labelX = rect.X + x;
int labelY = rect.Y + y;
if (labelSpan[labelY, labelX] != i || subtractedSpan.DangerousGetReferenceAt(y, x) == 0) continue;
overhangPixels++;
}
if (!ReferenceEquals(overhangImage, islandOverhangMat)) islandOverhangMat.Dispose();
if (overhangPixels < overhangConfig.RequiredPixelsToConsider) // No overhang = no island
{
continue;
}
}
AddIssue(new MainIssue(MainIssue.IssueType.Island, new IssueOfPoints(layer, points, islandBoundingRectangle)));
}
}
}
previousImage?.Dispose();
overhangImage?.Dispose();
}
if (resinTrapConfig.Enabled)
{
/* this used to calculate all contours for the layers, however new algorithm crops the layers to the overall bounding box
* so the contours produced here are not translated properly. We will generate contours during the algorithm itself later */
bool needDispose = false;
Mat resinTrapImage;
if (resinTrapConfig.BinaryThreshold > 0)
{
resinTrapImage = new Mat();
CvInvoke.Threshold(image.SourceMat, resinTrapImage, resinTrapConfig.BinaryThreshold, byte.MaxValue, ThresholdType.Binary);
}
else
{
needDispose = true;
resinTrapImage = image.SourceMat;
}
var contourLayer = resinTrapImage.Roi(SlicerFile.BoundingRectangle);
using var contours = contourLayer.FindContours(out var heirarchy, RetrType.Tree);
externalContours[layerIndex] = EmguContours.GetExternalContours(contours, heirarchy);
hollows[layerIndex] = EmguContours.GetNegativeContoursInGroups(contours, heirarchy);
resinTrapsContoursArea[layerIndex] = EmguContours.GetContoursArea(hollows[layerIndex]);
if (needDispose)
{
resinTrapImage.Dispose();
}
/*//
//hierarchy[i][0]: the index of the next contour of the same level
//hierarchy[i][1]: the index of the previous contour of the same level
//hierarchy[i][2]: the index of the first child
//hierarchy[i][3]: the index of the parent
//
var listHollowArea = new List<LayerHollowArea>();
var hollowGroups = EmguContours.GetNegativeContoursInGroups(contours, hierarchy);
var areas = EmguContours.GetContoursArea(hollowGroups);
for (var i = 0; i < hollowGroups.Count; i++)
{
if (areas[i] < resinTrapConfig.RequiredAreaToProcessCheck) continue;
var rect = CvInvoke.BoundingRectangle(hollowGroups[i][0]);
listHollowArea.Add(new LayerHollowArea(hollowGroups[i].ToArrayOfArray(),
rect,
areas[i],
layer.Index <= resinTrapConfig.StartLayerIndex ||
layer.Index == LayerCount - 1 // First and Last layers, always drains
? LayerHollowArea.AreaType.Drain
: LayerHollowArea.AreaType.Unknown));
}
if (listHollowArea.Count > 0) layerHollowAreas.TryAdd(layer.Index, listHollowArea);*/
}
}
progress.LockAndIncrement();
}); // Parallel end
}
if (resinTrapConfig.Enabled)
{
//progress.Reset("Detecting Air Boundaries (Resin traps)", LayerCount);
//if (progress.Token.IsCancellationRequested) return result.OrderBy(issue => issue.Type).ThenBy(issue => issue.LayerIndex).ThenBy(issue => issue.Area).ToList();
progress.Reset("Detection pass 1 of 2 (Resin traps)", SlicerFile.LayerCount, resinTrapConfig.StartLayerIndex);
using var matCache = new MatCacheManager(SlicerFile, 0, 2)
{
AfterCacheAction = mats =>
{
mats[1] = mats[0].Roi(SlicerFile.BoundingRectangle);
if (resinTrapConfig.MaximumPixelBrightnessToDrain > 0)
{
CvInvoke.Threshold(mats[1], mats[1], resinTrapConfig.MaximumPixelBrightnessToDrain, byte.MaxValue, ThresholdType.Binary);
}
}
};
/* define all mats up front, reducing allocations */
var currentAirMap = EmguExtensions.InitMat(SlicerFile.BoundingRectangle.Size);
var layerAirMap = currentAirMap.NewBlank();
/* the first pass does bottom to top, and tracks anything it thinks is a resin trap */
for (var layerIndex = resinTrapConfig.StartLayerIndex; layerIndex < SlicerFile.LayerCount; layerIndex++)
{
if (progress.Token.IsCancellationRequested) return GetResult();
var curLayer = matCache.Get(layerIndex, 1);
//CacheLayers(layerIndex, true);
//var curLayer = matTargetCache[layerIndex];
//curLayer.Save($"D:\\dump\\{layerIndex}_a.png");
/* find hollows of current layer */
GenerateAirMap(curLayer, layerAirMap, externalContours[layerIndex]);
//layerAirMap.Save($"D:\\dump\\{layerIndex}_b.png");
if (layerIndex == resinTrapConfig.StartLayerIndex)
{
currentAirMap = layerAirMap.Clone();
}
//currentAirMap.Save($"D:\\dump\\{layerIndex}_c.png");
/* remove solid areas of current layer from the air map */
CvInvoke.Subtract(currentAirMap, curLayer, currentAirMap);
//currentAirMap.Save($"D:\\dump\\{layerIndex}_d.png");
/* add in areas of air in current layer to air map */
CvInvoke.BitwiseOr(layerAirMap, currentAirMap, currentAirMap);
//currentAirMap.Save($"D:\\dump\\{layerIndex}_e.png");
if (hollows[layerIndex] is not null)
{
resinTraps[layerIndex] = new();
airContours[layerIndex] = new();
Parallel.For(0, hollows[layerIndex].Count, CoreSettings.ParallelOptions, i =>
{
progress.PauseIfRequested();
//for (var i = 0; i < hollows[layerIndex].Count; i++)
//{
if (progress.Token.IsCancellationRequested) return;
if (resinTrapsContoursArea[layerIndex][i] < resinTrapConfig.RequiredAreaToProcessCheck) return;
/* intersect current contour, with the current airmap. */
using var currentContour = curLayer.NewBlank();
using var airOverlap = curLayer.NewBlank();
CvInvoke.DrawContours(currentContour, hollows[layerIndex][i], -1, EmguExtensions.WhiteColor, -1);
CvInvoke.BitwiseAnd(currentAirMap, currentContour, airOverlap);
var overlapCount = CvInvoke.CountNonZero(airOverlap);
lock (SlicerFile[layerIndex].Mutex)
{
if (overlapCount == 0)
{
/* this countour does *not* overlap known air */
/* add a resin trap (for now... will be revisited in part 2) */
resinTraps[layerIndex].Add(hollows[layerIndex][i]);
}
else
{
if (overlapCount >= resinTrapConfig.RequiredBlackPixelsToDrain)
{
/* this contour does overlap air, add it to the current air map and remember this contour was air-connected for 2nd pass */
airContours[layerIndex].Add(hollows[layerIndex][i]);
CvInvoke.BitwiseOr(currentContour, currentAirMap, currentAirMap);
}
else
{
/* it overlapped ,but not by enough, treat as solid */
CvInvoke.Subtract(currentAirMap, currentContour, currentAirMap);
}
}
}
});
if (progress.Token.IsCancellationRequested)
return GetResult();
}
//matCache[layerIndex].Dispose();
//matCache[layerIndex] = null;
//matTargetCache[layerIndex] = null;
matCache.Consume(layerIndex);
progress++;
}
if (progress.Token.IsCancellationRequested) return GetResult();
progress.Reset("Detection pass 2 of 2 (Resin traps)", SlicerFile.LayerCount, resinTrapConfig.StartLayerIndex);
/* starting over again but this time from the top to the bottom */
if (currentAirMap is not null)
{
currentAirMap.Dispose();
currentAirMap = null;
}
var resinTrapGroups = new List<List<(VectorOfVectorOfPoint contour, uint layerIndex)>>();
matCache.Direction = false;
matCache.Clear();
for (int layerIndex = resinTraps.Length - 1; layerIndex >= resinTrapConfig.StartLayerIndex; layerIndex--)
{
if (progress.Token.IsCancellationRequested) return GetResult();
var curLayer = matCache.Get((uint)layerIndex, 1);
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...*/
GenerateAirMap(curLayer, layerAirMap, externalContours[layerIndex]);
/* Update air map with any hollows that were found to be air-connected during first pass */
if (airContours[layerIndex] is not null)
{
Parallel.ForEach(airContours[layerIndex], CoreSettings.ParallelOptions, vec =>
{
progress.PauseIfRequested();
CvInvoke.DrawContours(layerAirMap, vec, -1, EmguExtensions.WhiteColor, -1);
}
);
}
/* 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);
if (resinTraps[layerIndex] is not null)
{
suctionCups[layerIndex] = new();
/* 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 */
Parallel.For(0, resinTraps[layerIndex].Count, CoreSettings.ParallelOptions, x =>
{
progress.PauseIfRequested();
if (progress.Token.IsCancellationRequested) return;
/* check if each contour overlaps known air */
using var currentContour = curLayer.NewBlank();
using var airOverlap = curLayer.NewBlank();
CvInvoke.DrawContours(currentContour, resinTraps[layerIndex][x], -1, EmguExtensions.WhiteColor, -1);
CvInvoke.BitwiseAnd(currentAirMap, currentContour, airOverlap);
var overlapCount = CvInvoke.CountNonZero(airOverlap);
//lock (SlicerFile[layerIndex].Mutex)
//{
if (overlapCount >= resinTrapConfig.RequiredBlackPixelsToDrain)
{
/* this contour does overlap air, add this it our air map */
CvInvoke.BitwiseOr(currentContour, currentAirMap, currentAirMap, currentContour);
/* Always add the removed contour to suctionTraps (even if we aren't reporting suction traps)
* This is because contours that are placed on here get removed from resin traps in the next stage
* if you don't put them here, they never get removed even if they should :) */
/* if we haven't defined a suctionTrap list for this layer, do so */
lock (SlicerFile[layerIndex].Mutex)
{
/* since we know it isn't a resin trap, it becomes a suction trap */
suctionCups[layerIndex].Add(resinTraps[layerIndex][x]);
for (var groupIndex = resinTrapGroups.Count - 1; groupIndex >= 0; groupIndex--)
{
var group = resinTrapGroups[groupIndex];
if (group[^1].layerIndex > layerIndex + 1)
{
// this group is disconnected from current layer by at least 1 layer, no need to process anything from here anymore
//group.Clear();
//resinTrapGroups.Remove(group);
continue;
}
for (var contourIndex = group.Count - 1; contourIndex >= 0; contourIndex--)
{
if (group[contourIndex].layerIndex > layerIndex + 1) break;
var testContour = group[contourIndex].contour;
if (!EmguContours.ContoursIntersect(testContour, resinTraps[layerIndex][x])) continue;
// if any contours in this group, that are on the previous layer, overlap the new suction area, they are all suction areas
foreach (var item in group)
{
suctionCups[item.layerIndex].Add(item.contour);
if (item.layerIndex != layerIndex)
{
resinTraps[item.layerIndex].Remove(item.contour);
}
}
group.Clear();
resinTrapGroups.Remove(group);
break;
}
}
}
/* to keep things tidy while we iterate resin traps, it will be left in the list for now, and removed later */
}
else
{
/* doesn't overlap by enough, remove from air map */
CvInvoke.Subtract(currentAirMap, currentContour, currentAirMap, currentContour);
lock (SlicerFile[layerIndex].Mutex)
{
/* put it in a group of resin traps, used when a subsequent layer becomes a suction cup, it can convert any overlapping groups to suction cup */
/* select new LayerIssue(this[layerIndex], LayerIssue.IssueType.ResinTrap, area.Contour, area.BoundingRectangle)) */
var overlappingGroupIndexes = new List<int>();
for (var groupIndex = 0; groupIndex < resinTrapGroups.Count; groupIndex++)
{
if (resinTrapGroups[groupIndex][^1].layerIndex != layerIndex && resinTrapGroups[groupIndex][^1].layerIndex != layerIndex + 1) continue;
if (EmguContours.ContoursIntersect(resinTrapGroups[groupIndex][^1].contour, resinTraps[layerIndex][x]))
{
overlappingGroupIndexes.Add(groupIndex);
}
}
if (overlappingGroupIndexes.Count == 0)
{
// no overlaps, make a single issue
resinTrapGroups.Add(new List<(VectorOfVectorOfPoint contour, uint layerIndex)> { (resinTraps[layerIndex][x], (uint)layerIndex) });
}
else if (overlappingGroupIndexes.Count == 1)
{
resinTrapGroups[overlappingGroupIndexes[0]].Add((resinTraps[layerIndex][x], (uint)layerIndex));
}
else
{
var combinedGroup = new List<(VectorOfVectorOfPoint contour, uint layerIndex)>();
foreach (var index in overlappingGroupIndexes)
{
combinedGroup.AddRange(resinTrapGroups[index]);
}
for (var index = overlappingGroupIndexes.Count - 1; index >= 0; index--)
{
resinTrapGroups[overlappingGroupIndexes[index]].Clear();
resinTrapGroups.RemoveAt(overlappingGroupIndexes[index]);
}
combinedGroup.Add((resinTraps[layerIndex][x], (uint)layerIndex));
resinTrapGroups.Add(combinedGroup);
}
}
}
//}
});
/* anything that converted to a suction trap needs to removed from resinTraps. Loop backwards so indexes don't shift */
if (suctionCups[layerIndex] is not null)
{
for (var i = suctionCups[layerIndex].Count - 1; i >= 0; i--)
{
resinTraps[layerIndex].Remove(suctionCups[layerIndex][i]);
if (resinTraps[layerIndex].Count > 0) continue;
resinTraps[layerIndex] = null!;
break;
}
}
}
matCache.Consume((uint)layerIndex);
progress++;
}
if (progress.Token.IsCancellationRequested) return GetResult();
/* translate all contour points by ROI x and y */
var offsetBy = new Point(SlicerFile.BoundingRectangle.X, SlicerFile.BoundingRectangle.Y);
foreach (var listOfLayers in new[] { resinTraps, suctionCups })
{
Parallel.ForEach(listOfLayers.Where(list => list is not null), contoursGroups =>
{
progress.PauseIfRequested();
for (var groupIndex = 0; groupIndex < contoursGroups.Count; groupIndex++)
{
var contours = contoursGroups[groupIndex];
var arrayOfArrayOfPoints = contours.ToArrayOfArray();
foreach (var pointArray in arrayOfArrayOfPoints)
for (var i = 0; i < pointArray.Length; i++)
pointArray[i].Offset(offsetBy);
contoursGroups[groupIndex].Dispose();
contoursGroups[groupIndex] = new VectorOfVectorOfPoint(arrayOfArrayOfPoints);
}
//progress.LockAndIncrement();
});
}
if (progress.Token.IsCancellationRequested) return GetResult();
if (resinTrapConfig.DetectSuctionCups)
progress.Reset("Interpolating areas (Resin traps & suction cups)", (uint)(resinTraps.Count(list => list is not null) + suctionCups.Count(list => list is not null)));
else
progress.Reset("Interpolating areas (Resin traps)", (uint)(resinTraps.Count(list => list is not null)));
Parallel.Invoke(() =>
{
var resinTrapGroups = new List<List<IssueOfContours>>();
for (var layerIndex = resinTraps.Length - 1; layerIndex >= 0; layerIndex--)
{
if (resinTraps[layerIndex] is null) continue;
/* select new LayerIssue(this[layerIndex], LayerIssue.IssueType.ResinTrap, area.Contour, area.BoundingRectangle)) */
foreach (var trap in resinTraps[layerIndex])
{
progress.PauseIfRequested();
if (progress.Token.IsCancellationRequested) return;
var area = EmguContours.GetContourArea(trap);
var rect = CvInvoke.BoundingRectangle(trap[0]);
var trapIssue = new IssueOfContours(SlicerFile[layerIndex], trap.ToArrayOfArray(), rect, area);
var overlappingGroupIndexes = new List<int>();
for (var x = 0; x < resinTrapGroups.Count; x++)
{
if (resinTrapGroups[x][^1].LayerIndex != layerIndex && resinTrapGroups[x][^1].LayerIndex != layerIndex + 1) continue;
using var vec = new VectorOfVectorOfPoint(resinTrapGroups[x][^1].Contours);
if (EmguContours.ContoursIntersect(trap, vec))
{
overlappingGroupIndexes.Add(x);
}
}
if (overlappingGroupIndexes.Count == 0)
{
/* no overlaps, make a single issue */
resinTrapGroups.Add(new List<IssueOfContours> { trapIssue });
}
else if (overlappingGroupIndexes.Count == 1)
{
resinTrapGroups[overlappingGroupIndexes[0]].Add(trapIssue);
}
else
{
var combinedGroup = new List<IssueOfContours>();
foreach (var index in overlappingGroupIndexes)
{
combinedGroup.AddRange(resinTrapGroups[index]);
}
for (var index = overlappingGroupIndexes.Count - 1; index >= 0; index--)
{
resinTrapGroups[overlappingGroupIndexes[index]].Clear();
resinTrapGroups.RemoveAt(overlappingGroupIndexes[index]);
}
combinedGroup.Add(trapIssue);
resinTrapGroups.Add(combinedGroup);
}
}
progress.LockAndIncrement();
}
foreach (var group in resinTrapGroups)
{
if(group.Any(issue => issue.LayerIndex == 0)) continue; // Not a trap if on plate
AddIssue(new MainIssue(MainIssue.IssueType.ResinTrap, group));
}
},
() =>
{
/* only report suction cup issues if enabled */
if (resinTrapConfig.DetectSuctionCups)
{
var minimumSuctionArea = resinTrapConfig.RequiredAreaToConsiderSuctionCup;
var suctionGroups = new List<List<IssueOfContours>>();
for (var layerIndex = suctionCups.Length - 1; layerIndex >= 0; layerIndex--)
{
if (suctionCups[layerIndex] is null) continue;
foreach (var trap in suctionCups[layerIndex])
{
progress.PauseIfRequested();
if (progress.Token.IsCancellationRequested) return;
var area = EmguContours.GetContourArea(trap);
if (area < minimumSuctionArea) continue;
var rect = CvInvoke.BoundingRectangle(trap[0]);
var trapIssue = new IssueOfContours(SlicerFile[layerIndex], trap.ToArrayOfArray(), rect, area);
var overlappingGroupIndexes = new List<int>();
for (var x = 0; x < suctionGroups.Count; x++)
{
if (suctionGroups[x][^1].LayerIndex != layerIndex && suctionGroups[x][^1].LayerIndex != layerIndex + 1) continue;
using var vec = new VectorOfVectorOfPoint(suctionGroups[x][^1].Contours);
if (EmguContours.ContoursIntersect(trap, vec))
{
overlappingGroupIndexes.Add(x);
}
}
if (overlappingGroupIndexes.Count == 0)
{
/* no overlaps, make a new group */
suctionGroups.Add(new List<IssueOfContours> { trapIssue });
}
else if (overlappingGroupIndexes.Count == 1)
{
suctionGroups[overlappingGroupIndexes[0]].Add(trapIssue);
}
else
{
var combinedGroup = new List<IssueOfContours>();
/* iterate backwards to not screw up indexes */
for (var i = overlappingGroupIndexes.Count - 1; i >= 0; i--)
{
var index = overlappingGroupIndexes[i];
combinedGroup.AddRange(suctionGroups[index]);
suctionGroups[index].Clear();
suctionGroups.RemoveAt(index);
}
combinedGroup.Add(trapIssue);
suctionGroups.Add(combinedGroup);
}
}
progress.LockAndIncrement();
}
foreach (var group in suctionGroups)
{
var mainIssue = new MainIssue(MainIssue.IssueType.SuctionCup, group);
if ((decimal)mainIssue.TotalHeight >= resinTrapConfig.RequiredHeightToConsiderSuctionCup)
{
AddIssue(mainIssue);
}
}
}
});
// Dispose
foreach (var listOfVectors in new[] { resinTraps, suctionCups, hollows, airContours })
{
foreach (var vectorArray in listOfVectors)
{
if (vectorArray is null) continue;
foreach (var vector in vectorArray)
{
vector?.Dispose();
}
}
}
foreach (var vector in externalContours)
{
vector?.Dispose();
}
}
return GetResult();
}
public MainIssue[] DrillSuctionCupsForIssues(IEnumerable<MainIssue> issues, int ventHoleDiameter, OperationProgress progress)
{
var drillOps = new List<PixelOperation>();
var drilledIssues = new List<MainIssue>();
//var suctionReliefSize = (ushort)Math.Max(SlicerFile.PpmmMax * 0.8, 17);
/* for each suction cup issue that is an initial layer */
foreach (var mainIssue in issues)
{
var drillPoint = GetDrillLocation((IssueOfContours)mainIssue[0], ventHoleDiameter);
if (drillPoint.IsAnyNegative()) continue;
drillOps.Add(new PixelDrainHole(mainIssue.StartLayerIndex, drillPoint, (ushort)ventHoleDiameter));
drilledIssues.Add(mainIssue);
}
SlicerFile.DrawModifications(drillOps, progress);
return drilledIssues.ToArray();
}
public static Point GetDrillLocation(IssueOfContours issue, int diameter)
{
using var vecCentroid = new VectorOfPoint(issue.Contours[0]);
var centroid = EmguContour.GetCentroid(vecCentroid);
if (centroid.IsAnyNegative()) return centroid;
using var circleCheck = EmguExtensions.InitMat(issue.BoundingRectangle.Size);
using var contourMat = EmguExtensions.InitMat(issue.BoundingRectangle.Size);
var inverseOffset = new Point(issue.BoundingRectangle.X * -1, issue.BoundingRectangle.Y * -1);
using var vec = new VectorOfVectorOfPoint(issue.Contours);
CvInvoke.DrawContours(contourMat, vec, -1, EmguExtensions.WhiteColor, -1, LineType.EightConnected, null, int.MaxValue, inverseOffset);
CvInvoke.Circle(circleCheck, new(centroid.X + inverseOffset.X, centroid.Y + inverseOffset.Y), diameter, EmguExtensions.WhiteColor, -1);
CvInvoke.BitwiseAnd(circleCheck, contourMat, circleCheck);
return CvInvoke.CountNonZero(circleCheck) > 0
? centroid /* 5px centroid is inside layer! drill baby drill */
: new Point(-1,-1); /* centroid is not inside the actual contour, no drill */
}
}