mirror of
https://github.com/riegera2412/UVtools.git
synced 2026-07-08 17:42:31 +02:00
a626cfdc72
- (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
519 lines
20 KiB
C#
519 lines
20 KiB
C#
/*
|
|
* GNU AFFERO GENERAL PUBLIC LICENSE
|
|
* Version 3, 19 November 2007
|
|
* Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
* Everyone is permitted to copy and distribute verbatim copies
|
|
* of this license document, but changing it is not allowed.
|
|
*/
|
|
|
|
using Emgu.CV;
|
|
using Emgu.CV.CvEnum;
|
|
using Emgu.CV.Util;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using UVtools.Core.Extensions;
|
|
using UVtools.Core.FileFormats;
|
|
using UVtools.Core.Layers;
|
|
using UVtools.Core.Managers;
|
|
|
|
namespace UVtools.Core.Operations;
|
|
|
|
|
|
public class OperationRepairLayers : Operation
|
|
{
|
|
#region Members
|
|
private bool _detectIssues;
|
|
private bool _repairIslands = true;
|
|
private bool _repairResinTraps = true;
|
|
private bool _repairSuctionCups;
|
|
private bool _removeEmptyLayers = true;
|
|
private ushort _removeIslandsBelowEqualPixelCount = 5;
|
|
private ushort _removeIslandsRecursiveIterations = 4;
|
|
private ushort _attachIslandsBelowLayers = 2;
|
|
private byte _resinTrapsOverlapBy = 5;
|
|
private byte _suctionCupsVentHole = 16;
|
|
private uint _gapClosingIterations = 1;
|
|
private uint _noiseRemovalIterations;
|
|
|
|
#endregion
|
|
|
|
#region Overrides
|
|
public override bool CanROI => false;
|
|
public override string IconClass => "fa-solid fa-toolbox";
|
|
public override string Title => "Repair layers and issues";
|
|
public override string Description => string.Empty;
|
|
|
|
public override string ConfirmationText => "attempt this repair?";
|
|
|
|
public override string ProgressTitle =>
|
|
$"Repairing layers {LayerIndexStart} through {LayerIndexEnd}";
|
|
|
|
public override string ProgressAction => "Repaired layers";
|
|
|
|
public override string? ValidateInternally()
|
|
{
|
|
var sb = new StringBuilder();
|
|
|
|
if (!_repairIslands && !_repairResinTraps && !_repairSuctionCups && !_removeEmptyLayers)
|
|
{
|
|
sb.AppendLine("You must select at least one repair operation.");
|
|
}
|
|
|
|
if (!_detectIssues && SlicerFile.IssueManager.Count == 0)
|
|
{
|
|
sb.AppendLine("There are no present issues on the current session to repair.");
|
|
sb.AppendLine("Please detect issues before run this tool or check the option: \"Re-detect the selected issues before repair\" to force a detect and repair.");
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
var repair = new List<string>();
|
|
if(_repairIslands) repair.Add("Islands");
|
|
if(_repairResinTraps) repair.Add("Resin traps");
|
|
if(_repairSuctionCups) repair.Add("Suction cups");
|
|
if(_removeEmptyLayers) repair.Add("Empty layers");
|
|
var result = $"[Repair: {string.Join('/', repair)}] [Detect: {_detectIssues}]" +
|
|
$"[Gap closing: {_gapClosingIterations}px] " +
|
|
$"[Noise removal: {_noiseRemovalIterations}px]" + LayerRangeString;
|
|
if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}";
|
|
return result;
|
|
}
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
public OperationRepairLayers() { }
|
|
|
|
public OperationRepairLayers(FileFormat slicerFile) : base(slicerFile) { }
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
/// <summary>
|
|
/// IF true it will re-detect the selected issues before repair, otherwise uses and repair the previous detected issues
|
|
/// </summary>
|
|
public bool DetectIssues
|
|
{
|
|
get => _detectIssues;
|
|
set => RaiseAndSetIfChanged(ref _detectIssues, value);
|
|
}
|
|
|
|
public bool RepairIslands
|
|
{
|
|
get => _repairIslands;
|
|
set => RaiseAndSetIfChanged(ref _repairIslands, value);
|
|
}
|
|
|
|
public bool RepairResinTraps
|
|
{
|
|
get => _repairResinTraps;
|
|
set => RaiseAndSetIfChanged(ref _repairResinTraps, value);
|
|
}
|
|
|
|
public bool RepairSuctionCups
|
|
{
|
|
get => _repairSuctionCups;
|
|
set => RaiseAndSetIfChanged(ref _repairSuctionCups, value);
|
|
}
|
|
|
|
public bool RemoveEmptyLayers
|
|
{
|
|
get => _removeEmptyLayers;
|
|
set => RaiseAndSetIfChanged(ref _removeEmptyLayers, value);
|
|
}
|
|
|
|
public ushort RemoveIslandsBelowEqualPixelCount
|
|
{
|
|
get => _removeIslandsBelowEqualPixelCount;
|
|
set => RaiseAndSetIfChanged(ref _removeIslandsBelowEqualPixelCount, value);
|
|
}
|
|
|
|
public ushort RemoveIslandsRecursiveIterations
|
|
{
|
|
get => _removeIslandsRecursiveIterations;
|
|
set => RaiseAndSetIfChanged(ref _removeIslandsRecursiveIterations, value);
|
|
}
|
|
|
|
public ushort AttachIslandsBelowLayers
|
|
{
|
|
get => _attachIslandsBelowLayers;
|
|
set => RaiseAndSetIfChanged(ref _attachIslandsBelowLayers, value);
|
|
}
|
|
|
|
public byte ResinTrapsOverlapBy
|
|
{
|
|
get => _resinTrapsOverlapBy;
|
|
set => RaiseAndSetIfChanged(ref _resinTrapsOverlapBy, value);
|
|
}
|
|
|
|
public byte SuctionCupsVentHole
|
|
{
|
|
get => _suctionCupsVentHole;
|
|
set => RaiseAndSetIfChanged(ref _suctionCupsVentHole, value);
|
|
}
|
|
|
|
public uint GapClosingIterations
|
|
{
|
|
get => _gapClosingIterations;
|
|
set => RaiseAndSetIfChanged(ref _gapClosingIterations, value);
|
|
}
|
|
|
|
public uint NoiseRemovalIterations
|
|
{
|
|
get => _noiseRemovalIterations;
|
|
set => RaiseAndSetIfChanged(ref _noiseRemovalIterations, value);
|
|
}
|
|
|
|
public IssuesDetectionConfiguration IssuesDetectionConfig { get; set; } = new();
|
|
|
|
#endregion
|
|
|
|
#region Equality
|
|
|
|
protected bool Equals(OperationRepairLayers other)
|
|
{
|
|
return _detectIssues == other._detectIssues && _repairIslands == other._repairIslands && _repairResinTraps == other._repairResinTraps && _repairSuctionCups == other._repairSuctionCups && _removeEmptyLayers == other._removeEmptyLayers && _removeIslandsBelowEqualPixelCount == other._removeIslandsBelowEqualPixelCount && _removeIslandsRecursiveIterations == other._removeIslandsRecursiveIterations && _attachIslandsBelowLayers == other._attachIslandsBelowLayers && _resinTrapsOverlapBy == other._resinTrapsOverlapBy && _suctionCupsVentHole == other._suctionCupsVentHole && _gapClosingIterations == other._gapClosingIterations && _noiseRemovalIterations == other._noiseRemovalIterations;
|
|
}
|
|
|
|
public override bool Equals(object? obj)
|
|
{
|
|
if (ReferenceEquals(null, obj)) return false;
|
|
if (ReferenceEquals(this, obj)) return true;
|
|
if (obj.GetType() != this.GetType()) return false;
|
|
return Equals((OperationRepairLayers) obj);
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
var hashCode = new HashCode();
|
|
hashCode.Add(_detectIssues);
|
|
hashCode.Add(_repairIslands);
|
|
hashCode.Add(_repairResinTraps);
|
|
hashCode.Add(_repairSuctionCups);
|
|
hashCode.Add(_removeEmptyLayers);
|
|
hashCode.Add(_removeIslandsBelowEqualPixelCount);
|
|
hashCode.Add(_removeIslandsRecursiveIterations);
|
|
hashCode.Add(_attachIslandsBelowLayers);
|
|
hashCode.Add(_resinTrapsOverlapBy);
|
|
hashCode.Add(_suctionCupsVentHole);
|
|
hashCode.Add(_gapClosingIterations);
|
|
hashCode.Add(_noiseRemovalIterations);
|
|
return hashCode.ToHashCode();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
protected override bool ExecuteInternally(OperationProgress progress)
|
|
{
|
|
List<MainIssue> issues;
|
|
|
|
if (_detectIssues)
|
|
{
|
|
var config = IssuesDetectionConfig.Clone();
|
|
config.DisableAll();
|
|
config.IslandConfig.Enabled =
|
|
(_repairIslands && _removeIslandsBelowEqualPixelCount > 0 && _removeIslandsRecursiveIterations != 1) ||
|
|
_repairIslands && _attachIslandsBelowLayers > 0;
|
|
config.ResinTrapConfig.Enabled = _repairResinTraps;
|
|
config.ResinTrapConfig.DetectSuctionCups = _repairSuctionCups;
|
|
config.EmptyLayerConfig.Enabled = _removeEmptyLayers;
|
|
|
|
issues = SlicerFile.IssueManager.DetectIssues(config, progress).ToList();
|
|
issues.RemoveAll(mainIssue => SlicerFile.IssueManager.IgnoredIssues.Contains(mainIssue));
|
|
}
|
|
else
|
|
{
|
|
issues = SlicerFile.IssueManager.GetVisible().ToList();
|
|
}
|
|
|
|
if (issues.Count == 0) return true;
|
|
|
|
// Remove islands
|
|
if (//Issues is not null
|
|
//IslandDetectionConfig is not null
|
|
_repairIslands
|
|
&& _removeIslandsBelowEqualPixelCount > 0
|
|
&& _removeIslandsRecursiveIterations != 1)
|
|
{
|
|
progress.Reset("Removed recursive islands");
|
|
ushort limit = _removeIslandsRecursiveIterations == 0
|
|
? ushort.MaxValue
|
|
: _removeIslandsRecursiveIterations;
|
|
|
|
var recursiveIssues = issues;
|
|
var islandsToRecompute = new ConcurrentBag<uint>();
|
|
var config = IssuesDetectionConfig.Clone();
|
|
config.DisableAll();
|
|
config.IslandConfig.Enable();
|
|
//islandConfig.RequiredAreaToProcessCheck = (ushort)(_removeIslandsBelowEqualPixelCount / 2);
|
|
|
|
for (uint i = 0; i < limit; i++)
|
|
{
|
|
if (i > 0)
|
|
{
|
|
/*var whiteList = islandsToRecompute.GroupBy(u => u)
|
|
.Select(grp => grp.First())
|
|
.ToList();*/
|
|
config.IslandConfig.WhiteListLayers = islandsToRecompute.ToList();
|
|
recursiveIssues = SlicerFile.IssueManager.DetectIssues(config);
|
|
//Debug.WriteLine(i);
|
|
}
|
|
|
|
var issuesGroup = IssueManager.GetIssuesBy(recursiveIssues, MainIssue.IssueType.Island)
|
|
.Where(issue => issue.PixelsCount <= RemoveIslandsBelowEqualPixelCount)
|
|
.GroupBy(issue => issue.LayerIndex);
|
|
|
|
|
|
if (!issuesGroup.Any()) break; // Nothing to process
|
|
|
|
islandsToRecompute.Clear();
|
|
Parallel.ForEach(issuesGroup, CoreSettings.GetParallelOptions(progress), group =>
|
|
{
|
|
progress.PauseIfRequested();
|
|
var layer = SlicerFile[group.Key];
|
|
var image = layer.LayerMat;
|
|
var span = image.GetDataByteSpan();
|
|
foreach (IssueOfPoints issue in group)
|
|
{
|
|
foreach (var issuePixel in issue.Points)
|
|
{
|
|
span[image.GetPixelPos(issuePixel)] = 0;
|
|
}
|
|
|
|
progress.LockAndIncrement();
|
|
}
|
|
|
|
var nextLayerIndex = group.Key + 1;
|
|
if (nextLayerIndex < SlicerFile.LayerCount)
|
|
islandsToRecompute.Add(nextLayerIndex);
|
|
|
|
layer.LayerMat = image;
|
|
});
|
|
|
|
// Remove from main list due the replicate below repair
|
|
issues.RemoveAll(mainIssue => mainIssue.Type == MainIssue.IssueType.Island && mainIssue.Area <= RemoveIslandsBelowEqualPixelCount);
|
|
|
|
if (islandsToRecompute.IsEmpty) break; // No more leftovers
|
|
}
|
|
}
|
|
|
|
if (_repairIslands && _attachIslandsBelowLayers > 0)
|
|
{
|
|
var islandsToProcess = issues;
|
|
|
|
/*if (islandsToProcess.Count == 0)
|
|
{
|
|
var config = IssuesDetectionConfig.Clone();
|
|
config.DisableAll();
|
|
config.IslandConfig.Enable();
|
|
|
|
islandsToProcess = SlicerFile.IssueManager.DetectIssues(config, progress);
|
|
islandsToProcess.RemoveAll(mainIssue => SlicerFile.IssueManager.IgnoredIssues.Contains(mainIssue));
|
|
}*/
|
|
|
|
var issuesGroup = IssueManager.GetIssuesBy(islandsToProcess, MainIssue.IssueType.Island).GroupBy(issue => issue.LayerIndex);
|
|
|
|
progress.Reset("Attempt to attach islands below", (uint) islandsToProcess.Count);
|
|
var sync = new object();
|
|
Parallel.ForEach(issuesGroup, CoreSettings.GetParallelOptions(progress), group =>
|
|
{
|
|
progress.PauseIfRequested();
|
|
using var mat = SlicerFile[group.Key].LayerMat;
|
|
var matSpan = mat.GetDataByteSpan();
|
|
var matCache = new Dictionary<uint, Mat>();
|
|
var matCacheModified = new Dictionary<uint, bool>();
|
|
var startLayer = Math.Max(0, (int)group.Key - 2);
|
|
var lowestPossibleLayer = (uint)Math.Max(0, (int)group.Key - 1 - _attachIslandsBelowLayers);
|
|
|
|
for (var layerIndex = startLayer+1; layerIndex >= lowestPossibleLayer; layerIndex--)
|
|
{
|
|
//Debug.WriteLine(layerIndex);
|
|
Monitor.Enter(SlicerFile[layerIndex].Mutex);
|
|
matCache.Add((uint) layerIndex, SlicerFile[layerIndex].LayerMat);
|
|
matCacheModified.Add((uint) layerIndex, false);
|
|
}
|
|
|
|
foreach (IssueOfPoints issue in group)
|
|
{
|
|
int foundAt = startLayer == 0 ? 0 : - 1;
|
|
var requiredSupportingPixels = Math.Max(1, issue.PixelsCount * IssuesDetectionConfig.IslandConfig.RequiredPixelsToSupportMultiplier);
|
|
|
|
for (var layerIndex = startLayer; layerIndex >= lowestPossibleLayer && foundAt < 0; layerIndex--)
|
|
{
|
|
uint pixelsSupportingIsland = 0;
|
|
|
|
unsafe
|
|
{
|
|
var span = matCache[(uint) layerIndex].GetBytePointer();
|
|
|
|
foreach (var point in issue.Points)
|
|
{
|
|
if (span[mat.GetPixelPos(point)] < IssuesDetectionConfig.IslandConfig.RequiredPixelBrightnessToSupport)
|
|
continue;
|
|
|
|
pixelsSupportingIsland++;
|
|
|
|
if (pixelsSupportingIsland >= requiredSupportingPixels)
|
|
{
|
|
foundAt = layerIndex + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// Copy pixels
|
|
if (foundAt >= 0)
|
|
{
|
|
for (var layerIndex = startLayer + 1; layerIndex >= foundAt; layerIndex--)
|
|
{
|
|
matCacheModified[(uint) layerIndex] = true;
|
|
unsafe
|
|
{
|
|
var span = matCache[(uint) layerIndex].GetBytePointer();
|
|
|
|
foreach (var point in issue.Points)
|
|
{
|
|
var pos = mat.GetPixelPos(point);
|
|
span[pos] = (byte)Math.Min(span[pos] + matSpan[pos], byte.MaxValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
lock (sync)
|
|
{
|
|
// Remove from processed issues
|
|
issues.Remove(issue.Parent!);
|
|
}
|
|
}
|
|
|
|
progress.LockAndIncrement();
|
|
}
|
|
|
|
foreach (var dict in matCache)
|
|
{
|
|
if (matCacheModified[dict.Key])
|
|
{
|
|
SlicerFile[dict.Key].LayerMat = dict.Value;
|
|
}
|
|
dict.Value.Dispose();
|
|
Monitor.Exit(SlicerFile[dict.Key].Mutex);
|
|
}
|
|
});
|
|
}
|
|
|
|
progress.Reset(ProgressAction, LayerRangeCount);
|
|
if (_repairIslands || _repairResinTraps)
|
|
{
|
|
Parallel.For(LayerIndexStart, LayerIndexEnd, CoreSettings.GetParallelOptions(progress), layerIndex =>
|
|
{
|
|
progress.PauseIfRequested();
|
|
var layer = SlicerFile[layerIndex];
|
|
Mat? image = null;
|
|
|
|
void InitImage()
|
|
{
|
|
image ??= layer.LayerMat;
|
|
}
|
|
|
|
if (issues.Count > 0)
|
|
{
|
|
if (_repairIslands && _removeIslandsBelowEqualPixelCount > 0 && _removeIslandsRecursiveIterations == 1)
|
|
{
|
|
Span<byte> bytes = null;
|
|
foreach (IssueOfPoints issue in IssueManager.GetIssuesBy(issues, MainIssue.IssueType.Island, (uint)layerIndex))
|
|
{
|
|
if (issue.PixelsCount > _removeIslandsBelowEqualPixelCount) continue;
|
|
|
|
InitImage();
|
|
if (bytes == null) bytes = image!.GetDataByteSpan();
|
|
|
|
foreach (var issuePixel in issue.Points)
|
|
{
|
|
bytes[image!.GetPixelPos(issuePixel)] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_repairResinTraps)
|
|
{
|
|
foreach (IssueOfContours issue in IssueManager.GetIssuesBy(issues, MainIssue.IssueType.ResinTrap, (uint)layerIndex))
|
|
{
|
|
InitImage();
|
|
using var vec = new VectorOfVectorOfPoint(issue.Contours);
|
|
CvInvoke.DrawContours(image, vec, -1, EmguExtensions.WhiteColor, -1);
|
|
if (_resinTrapsOverlapBy > 0)
|
|
{
|
|
CvInvoke.DrawContours(image, vec, -1, EmguExtensions.WhiteColor, _resinTrapsOverlapBy * 2 + 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_repairIslands && (_gapClosingIterations > 0 || _noiseRemovalIterations > 0))
|
|
{
|
|
InitImage();
|
|
|
|
if (_gapClosingIterations > 0)
|
|
{
|
|
CvInvoke.MorphologyEx(image, image, MorphOp.Close, EmguExtensions.Kernel3x3Rectangle, EmguExtensions.AnchorCenter,
|
|
(int)_gapClosingIterations, BorderType.Default, default);
|
|
}
|
|
|
|
if (_noiseRemovalIterations > 0)
|
|
{
|
|
CvInvoke.MorphologyEx(image, image, MorphOp.Open, EmguExtensions.Kernel3x3Rectangle, EmguExtensions.AnchorCenter,
|
|
(int)_noiseRemovalIterations, BorderType.Default, default);
|
|
}
|
|
}
|
|
|
|
if (image is not null)
|
|
{
|
|
layer.LayerMat = image;
|
|
image.Dispose();
|
|
}
|
|
|
|
progress.LockAndIncrement();
|
|
});
|
|
}
|
|
|
|
|
|
if (_repairSuctionCups && issues.Count > 0)
|
|
{
|
|
SlicerFile.IssueManager.DrillSuctionCupsForIssues(issues.Where(mainIssue => mainIssue.Type == MainIssue.IssueType.SuctionCup), _suctionCupsVentHole, progress);
|
|
}
|
|
|
|
if (_removeEmptyLayers)
|
|
{
|
|
var removeLayers = new List<uint>();
|
|
for (var layerIndex = LayerIndexStart; layerIndex <= LayerIndexEnd; layerIndex++)
|
|
{
|
|
if (SlicerFile[layerIndex].NonZeroPixelCount == 0)
|
|
{
|
|
removeLayers.Add(layerIndex);
|
|
}
|
|
}
|
|
|
|
if (removeLayers.Count > 0)
|
|
{
|
|
OperationLayerRemove.RemoveLayers(SlicerFile, removeLayers, progress);
|
|
}
|
|
}
|
|
|
|
return !progress.Token.IsCancellationRequested;
|
|
}
|
|
|
|
#endregion
|
|
} |