Files
UVtools/UVtools.Core/Managers/IssueManager.cs
T
Tiago Conceição 4600e81b08 v3.0.0
- **(Add) Suggestions:**
   - A new module that detect bad or parameters out of a defined range and suggest a change on the file, those can be auto applied if configured to do so
   - **Avaliable suggestions:**
      - **Bottom layer count:** Bottom layers should be kept to a minimum, usually from 2 to 3, it function is to provide a good adhesion to the first layer on the build plate, using a high count have disadvantages.
      - **Wait time before cure:** Rest some time before cure the layer is crucial to let the resin settle after the lift sequence and allow some time for the arm settle at the correct Z position as the resin will offer some resistance and push the structure.
                                   This lead to better quality with more successful prints, less lamination problems, better first layers with more success of stick to the build plate and less elephant foot effect.
      - **Wait time after cure:** Rest some time after cure the layer and before the lift sequence can be important to allow the layer to cooldown a bit and detach better from the FEP.
      - **Layer height:** Using the right layer height is important to get successful prints:
                          Thin layers may cause problems on adhesion, lamination, will print much slower and have no real visual benefits.
                          Thick layers may not fully cure no matter the exposure time you use, causing lamination and other hazards. Read your resin dtasheet to know the limits.
                          Using layer height with too many decimal digits may produce a wrong positioning due stepper step loss and/or Z axis quality.
- **Core:**
   - Convert the project to Nullable aware and "null-safe"
- **File Formats:**
   - (Add) `Volume` property to get the total model volume
   - (Add) `SanitizeLayers` method to reassign indexes and force attribute parent file
   - (Improvement) Merge `LayerManager` into `FileFormat` and cleanup: This affects the whole project and external scripts.
   If using scripts please update them, search for `.LayerManager.` and replace by `.`
   - (Change) Chitubox encrypted format can now be saved as normal
   - (Fix) Converted files layers was pointing to the source file and related to it
- **Layers:**
   - (Add) Methods: `ResetParameters`, `CopyParametersTo`, `CopyExposureTo`, `CopyWaitTimesTo`
   - (Improvement) `IsBottomLayer` property will also return true when the index is inside bottom layer count
- **Scripting:**
   - (Add) Configuration variable: `MinimumVersionToRun` - Sets the minimum version able to run the script
   - (Improvement) Allow run scripts written in C# 10 with the new namespace; style as well as nullables methods
   - (Improvement) Convert scripts to use Nullable code
- **UI:**
   - (Add) Fluent Dark theme
   - (Add) Default Light theme
   - (Add) Default Dark theme
   - (Change) Use fontawesome and material design to render the icons instead of static png images
   - (Change) Some icons
   - (Change) Move log tab to clipboard tab
   - (Change) Tooltip overlay default color
   - (Improvement) Windows position for tool windows, sometimes framework can return negative values affecting positions, now limits to 0 (#387)
   - (Fix) Center image icon for layer action button
   - (Fix) Center image icon for save layer image button
- **Tools:**
   - (Add) Layer re-height: Offset mode, change layers position by a defined offset (#423)
   - (Improvement) Rotate: Unable to use an angle of 0
   - (Improvement) Remove layers: Will not recalcualte and reset properties of layers anymore, allowing removing layers on dynamic layer height models and others
   - (Improvement) Clone layers: Will not recalcualte and reset properties of layers anymore, allowing cloning layers on dynamic layer height models and others
   - (Fix) Exposure time finder: Very small printers may not print the stock object as it is configured, lead to a unknown error while generating the test. It will now show a better error message and advice a solution (#426)
- **Terminal:**
   - (Add) More default namespaces
   - (Improvement) Set a MinHeight for the rows to prevent spliter from eat the elements
   - (Change) Set working space to the MainWindow instead of TerminalWindow
- **(Upgrade) .NET from 5.0.14 to 6.0.3**
   - This brings big performance improvements, better JIT, faster I/O operations and others
   - Read more: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6
   - Due this macOS requirement starts at 10.15 (Catalina)
   - Read more: https://github.com/dotnet/core/blob/main/release-notes/6.0/supported-os.md
- (Add) Native support for MacOS ARM64 architecture (Mac M1 and upcomming Mac's) (#187)
- (Exchange) Dependency Newtonsoft Json by System.Text.Json to parse the json documents
- (Remove) "Automations - Light-off delay" in favor of new suggestion "wait time before cure" module
- (Fix) File - Send to: Winrar or 7zip have a wrong extension on the list (uvt) when should be (uvj)
- (Upgrade) AvaloniaUI from 0.10.12 to 0.10.13
2022-03-12 21:04:47 +00:00

1114 lines
55 KiB
C#

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 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 new List<MainIssue>();
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)
{
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 (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, EmguExtensions.Kernel3x3Rectangle,
anchor, overhangConfig.ErodeIterations, BorderType.Default, default);
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, EmguExtensions.Kernel3x3Rectangle,
anchor, overhangConfig.ErodeIterations, BorderType.Default, default);
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)
{
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 */
}
}