mirror of
https://github.com/riegera2412/UVtools.git
synced 2026-07-09 01:52:32 +02:00
8e4995a6a5
- **UVtoolsCmd:**
- **print-properties:**
- (Change) `-b`, `--base` option to `-a`, `-all` to indicate all properties and sub-properties, now defaults to only show base properties
- (Add) `-r`, `--range` option to prints only the matching layer(s) index(es) in a range
- (Add) `-i`, `--indexes` option to prints only the matching layer(s) index(es)
- (Add) Command: `set-properties <input-file> <property=value> Set properties in a file or to it layers with new values`
- (Add) Command: `print-issues <input-file> Detect and print issues in a file`
- (Add) New option to the `run` command: `-p, --property <property=value> Set a property with a new value (Compatible with operations only)`
- (Remove) Command: `print-layers` as it has been moved to `print-properties`, use `-r :` to obtain same result as default on `print-layers`
- **Issues:**
- (Fix) Issues groups with only one issue was displaying the wrong area value
- (Fix) Volume incorrectly calculated, resulting in a high value for group of issues
- (Fix) Incorrect calculation of the bounding rectangle for a group of issues
- **Repair layers:**
- (Add) Switch to opt between "Re-detect the selected issues before repair" and "Use and repair the previous detected issues" (Default)
- (Improvement) Do not allow to run the tool if there are no detected issues when the option "Use and repair the previous detected issues" is selected
- (Improvement) Linux: Recompile libcvextern.so on a older system to be able to run on both older and newest system (#603)
- (Upgrade) .NET from 6.0.10 to 6.0.11
516 lines
19 KiB
C#
516 lines
19 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 =>
|
|
{
|
|
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 =>
|
|
{
|
|
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 =>
|
|
{
|
|
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
|
|
} |