Files
UVtools/UVtools.Core/Managers/IssueManager.cs
T
Tiago Conceição c968eaedf9 v2.25.0
- **File formats:**
   - (Add) Allow to partial open the files for read and/or change properties, the layer images won't be read nor cached (Fast)
   - (Add) More abstraction on partial save
- **Scripting:**
   - (Add) ScriptOpenFolderDialogInput - Selects a folder path
   - (Add) ScriptOpenFileDialogInput - Selectes a file to open
   - (Add) ScriptSaveFileDialogInput - Selects a file to save
- (Add) [UNSAVED] tag to the title bar when there are unsaved changes on the current session
- (Improvement) Better handling of empty images on the UI
2021-11-18 03:29:08 +00:00

1132 lines
59 KiB
C#

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 Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Structure;
using Emgu.CV.Util;
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 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(
IslandDetectionConfiguration islandConfig = null,
OverhangDetectionConfiguration overhangConfig = null,
ResinTrapDetectionConfiguration resinTrapConfig = null,
TouchingBoundDetectionConfiguration touchBoundConfig = null,
PrintHeightDetectionConfiguration printHeightConfig = null,
bool emptyLayersConfig = true,
OperationProgress progress = null)
{
if (SlicerFile.DecodeType == FileFormat.FileDecodeType.Partial) return null;
islandConfig ??= new IslandDetectionConfiguration();
overhangConfig ??= new OverhangDetectionConfiguration();
resinTrapConfig ??= new ResinTrapDetectionConfiguration();
touchBoundConfig ??= new TouchingBoundDetectionConfiguration();
printHeightConfig ??= new PrintHeightDetectionConfiguration();
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)
{
foreach (var layer in SlicerFile)
{
if (layer.PositionZ > printHeightWithOffset)
{
AddIssue(new MainIssue(MainIssue.IssueType.PrintHeight, new Issue(layer)));
}
}
}
}
if (emptyLayersConfig)
{
foreach (var layer in SlicerFile)
{
if (layer.IsEmpty)
{
AddIssue(new MainIssue(MainIssue.IssueType.EmptyLayer, new Issue(layer)));
}
}
}
if (islandConfig.Enabled || overhangConfig.Enabled || resinTrapConfig.Enabled || touchBoundConfig.Enabled)
{
progress.Reset(OperationProgress.StatusIslands, SlicerFile.LayerCount);
// Detect contours
Parallel.For(0, SlicerFile.LayerCount, CoreSettings.ParallelOptions, layerIndexInt =>
{
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 || overhangConfig.WhiteListLayers is not null && !overhangConfig.WhiteListLayers.Contains(layerIndex))) &&
(!islandConfig.Enabled || islandConfig.Enabled && (layerIndex == 0 || islandConfig.WhiteListLayers is not null && !islandConfig.WhiteListLayers.Contains(layerIndex)))
)
{
progress.LockAndIncrement();
return;
}
using (var image = layer.LayerMat)
{
var step = image.GetRealStep();
var span = image.GetDataByteSpan();
if (touchBoundConfig.Enabled)
{
// TouchingBounds Checker
List<Point> pixels = new();
bool touchTop = layer.BoundingRectangle.Top <= touchBoundConfig.MarginTop;
bool touchBottom = layer.BoundingRectangle.Bottom >= image.Height - touchBoundConfig.MarginBottom;
bool touchLeft = layer.BoundingRectangle.Left <= touchBoundConfig.MarginLeft;
bool touchRight = layer.BoundingRectangle.Right >=
image.Width - touchBoundConfig.MarginRight;
int minx = int.MaxValue;
int miny = int.MaxValue;
int maxx = 0;
int maxy = 0;
if (touchTop || touchBottom)
{
for (int x = 0; x < image.Width; x++) // Check Top and Bottom bounds
{
if (touchTop)
{
for (int y = 0; y < touchBoundConfig.MarginTop; y++) // Top
{
if (span[image.GetPixelPos(x, y)] >=
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.Height - touchBoundConfig.MarginBottom;
y < image.Height;
y++) // Bottom
{
if (span[image.GetPixelPos(x, y)] >=
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 = touchBoundConfig.MarginTop;
y < image.Height - touchBoundConfig.MarginBottom;
y++) // Check Left and Right bounds
{
if (touchLeft)
{
for (int x = 0; x < touchBoundConfig.MarginLeft; x++) // Left
{
if (span[image.GetPixelPos(x, y)] >=
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 = image.Width - touchBoundConfig.MarginRight;
x < image.Width;
x++) // Right
{
if (span[image.GetPixelPos(x, y)] >=
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) // No islands nor overhangs for layer 0
{
Mat previousImage = null;
Span<byte> previousSpan = null;
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, islandImage, islandConfig.BinaryThreshold, byte.MaxValue,
ThresholdType.Binary);
}
else
{
islandImage = image;
}
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 labelSpan = labels.GetDataSpan<int>();
for (int i = 1; i < numLabels; i++)
{
Rectangle rect = new(
(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 (rect.Area() < islandConfig.RequiredAreaToProcessCheck)
continue;
if (previousImage is null)
{
previousImage = SlicerFile[layerIndex - 1].LayerMat;
previousSpan = previousImage.GetDataByteSpan();
}
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++)
{
int pixel = step * y + x;
if (
labelSpan[pixel] !=
i || // Background pixel or a pixel from another component within the bounding rectangle
span[pixel] <
islandConfig
.RequiredPixelBrightnessToProcessCheck // Low brightness, ignore
) continue;
points.Add(new Point(x, y));
if (previousSpan[pixel] >=
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...*/
IssueOfPoints island = null;
if (pixelsSupportingIsland < requiredSupportingPixels)
{
island = new IssueOfPoints(layer, points.ToArray(), rect);
}
// Check for overhangs
if (overhangConfig.Enabled && !overhangConfig.IndependentFromIslands &&
island is null
|| island is not null && islandConfig.EnhancedDetection &&
pixelsSupportingIsland >= 10
)
{
points.Clear();
using var imageRoi = image.Roi(rect);
using var previousImageRoi = previousImage.Roi(rect);
using var subtractedImage = new Mat();
var anchor = new Point(-1, -1);
CvInvoke.Subtract(imageRoi, previousImageRoi, subtractedImage);
CvInvoke.Threshold(subtractedImage, subtractedImage, 127, 255,
ThresholdType.Binary);
CvInvoke.Erode(subtractedImage, subtractedImage,
CvInvoke.GetStructuringElement(ElementShape.Rectangle,
new Size(3, 3), anchor),
anchor, overhangConfig.ErodeIterations, BorderType.Default,
new MCvScalar());
var subtractedSpan = subtractedImage.GetDataByteSpan();
var subtractedStep = subtractedImage.GetRealStep();
for (int y = 0; y < subtractedImage.Height; y++)
for (int x = 0; x < subtractedStep; x++)
{
int labelX = rect.X + x;
int labelY = rect.Y + y;
int pixel = subtractedImage.GetPixelPos(x, y);
int pixelLabel = labelY * step + labelX;
if (labelSpan[pixelLabel] != i || subtractedSpan[pixel] == 0)
continue;
points.Add(new Point(labelX, labelY));
}
if (points.Count >= overhangConfig.RequiredPixelsToConsider
) // Overhang
{
AddIssue(new MainIssue(MainIssue.IssueType.Overhang, new IssueOfPoints(layer, points, rect)));
}
else if (islandConfig.EnhancedDetection) // No overhang
{
island = null;
}
}
if (island is not null)
AddIssue(new MainIssue(MainIssue.IssueType.Island, island));
}
}
}
// Overhangs
if (!islandConfig.Enabled && overhangConfig.Enabled ||
(islandConfig.Enabled && overhangConfig.Enabled &&
overhangConfig.IndependentFromIslands))
{
bool canProcessCheck = true;
if (overhangConfig.WhiteListLayers is not null) // Check white list
{
if (!overhangConfig.WhiteListLayers.Contains(layerIndex))
{
canProcessCheck = false;
}
}
if (canProcessCheck)
{
previousImage ??= SlicerFile[layerIndex - 1].LayerMat;
using var subtractedImage = new Mat();
using var vecPoints = new VectorOfPoint();
var anchor = new Point(-1, -1);
CvInvoke.Subtract(image, previousImage, subtractedImage);
CvInvoke.Threshold(subtractedImage, subtractedImage, 127, 255,
ThresholdType.Binary);
CvInvoke.Erode(subtractedImage, subtractedImage,
CvInvoke.GetStructuringElement(ElementShape.Rectangle, new Size(3, 3),
anchor),
anchor, overhangConfig.ErodeIterations, BorderType.Default,
new MCvScalar());
CvInvoke.FindNonZero(subtractedImage, vecPoints);
if (vecPoints.Size >= overhangConfig.RequiredPixelsToConsider)
{
AddIssue(new MainIssue(MainIssue.IssueType.Overhang, new IssueOfPoints(layer, vecPoints.ToArray(), layer.BoundingRectangle)
));
}
}
}
previousImage?.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, resinTrapImage, resinTrapConfig.BinaryThreshold, byte.MaxValue, ThresholdType.Binary);
}
else
{
needDispose = true;
resinTrapImage = image;
}
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 =>
{
//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 =>
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 =>
{
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 =>
{
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])
{
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])
{
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)
{
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 */
}
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.LayerManager.DrawModifications(drillOps, progress);
return drilledIssues.ToArray();
}
}
}