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

540 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.Structure;
using System;
using System.Collections.Concurrent;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UVtools.Core.Extensions;
using UVtools.Core.FileFormats;
using UVtools.Core.Layers;
namespace UVtools.Core.Operations;
public class OperationLithophane : Operation
{
#region Enum
public enum LithophaneBaseType : byte
{
None,
Square,
Model
}
#endregion
#region Members
private decimal _layerHeight;
private ushort _bottomLayerCount;
private decimal _bottomExposure;
private decimal _normalExposure;
private string? _filePath;
private RotateDirection _rotate = RotateDirection.None;
private bool _mirror;
private bool _invertColor;
private decimal _resizeFactor = 100;
private bool _enhanceContrast;
private sbyte _brightnessGain;
private byte _gapClosingIterations;
private byte _removeNoiseIterations;
private byte _gaussianBlur;
private byte _startThresholdRange = 1;
private byte _endThresholdRange = byte.MaxValue;
private decimal _baseThickness = 2;
private LithophaneBaseType _baseType = LithophaneBaseType.Square;
private ushort _baseMargin = 80;
private decimal _lithophaneHeight = 3;
private bool _oneLayerPerThreshold;
private bool _enableAntiAliasing = true;
#endregion
#region Overrides
public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None;
public override string IconClass => "fa-solid fa-portrait";
public override string Title => "Lithophane";
public override string Description =>
"Generate lithophane from a picture.\n" +
"Note: The current opened file will be overwritten with this lithophane, use a dummy or a not needed file.";
public override string ConfirmationText =>
"generate the lithophane?";
public override string ProgressTitle =>
"Generating lithophane";
public override string ProgressAction => "Threshold levels";
public override string? ValidateInternally()
{
var sb = new StringBuilder();
if (string.IsNullOrWhiteSpace(_filePath))
{
sb.AppendLine("The input file is empty");
}
else if(!File.Exists(_filePath))
{
sb.AppendLine("The input file does not exists");
}
else
{
using var mat = GetSourceMat();
if (mat is null)
{
sb.AppendLine("Unable to generate the mat from source file, is it a valid image file?");
}
else
{
if (SlicerFile.ResolutionX < mat.Width * _resizeFactor / 100 || SlicerFile.ResolutionY < mat.Height * _resizeFactor / 100)
{
//int differenceX = (int)SlicerFile.ResolutionX - mat.Width;
//int differenceY = (int)SlicerFile.ResolutionY - mat.Height;
var scaleX = SlicerFile.ResolutionX * 100f / mat.Width;
var scaleY = SlicerFile.ResolutionY * 100f / mat.Height;
var maxScale = Math.Min(scaleX, scaleY);
sb.AppendLine($"The printer resolution is not enough to accomodate the lithophane image, please scale down to a maximum of {maxScale:F0}%");
}
}
}
if (_startThresholdRange == _endThresholdRange)
{
sb.AppendLine($"Start threshold can't be equal than end threshold ({_endThresholdRange})");
}
else if (_startThresholdRange > _endThresholdRange)
{
sb.AppendLine("Start threshold can't be higher than end threshold");
}
return sb.ToString();
}
public override string ToString()
{
var result = $"{(FileExists ? $"{Path.GetFileName(_filePath)} ({Math.Abs(GetHashCode())})" : $"Lithophane {Math.Abs(GetHashCode())}")}";
if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}";
return result;
}
#endregion
#region Constructor
public OperationLithophane() { }
public OperationLithophane(FileFormat slicerFile) : base(slicerFile)
{
if (_layerHeight <= 0) _layerHeight = (decimal)SlicerFile.LayerHeight;
if (_bottomExposure <= 0) _bottomExposure = (decimal)SlicerFile.BottomExposureTime;
if (_normalExposure <= 0) _normalExposure = (decimal)SlicerFile.ExposureTime;
if (_bottomLayerCount <= 0) _bottomLayerCount = SlicerFile.BottomLayerCount;
_mirror = SlicerFile.DisplayMirror != FlipDirection.None;
}
#endregion
#region Properties
public decimal LayerHeight
{
get => _layerHeight;
set => RaiseAndSetIfChanged(ref _layerHeight, Layer.RoundHeight(value));
}
public ushort BottomLayerCount
{
get => _bottomLayerCount;
set => RaiseAndSetIfChanged(ref _bottomLayerCount, value);
}
public decimal BottomExposure
{
get => _bottomExposure;
set => RaiseAndSetIfChanged(ref _bottomExposure, Math.Round(value, 2));
}
public decimal NormalExposure
{
get => _normalExposure;
set => RaiseAndSetIfChanged(ref _normalExposure, Math.Round(value, 2));
}
public string? FilePath
{
get => _filePath;
set => RaiseAndSetIfChanged(ref _filePath, value);
}
public bool FileExists => !string.IsNullOrWhiteSpace(_filePath) && File.Exists(_filePath);
public RotateDirection Rotate
{
get => _rotate;
set => RaiseAndSetIfChanged(ref _rotate, value);
}
public bool Mirror
{
get => _mirror;
set => RaiseAndSetIfChanged(ref _mirror, value);
}
public bool InvertColor
{
get => _invertColor;
set => RaiseAndSetIfChanged(ref _invertColor, value);
}
public decimal ResizeFactor
{
get => _resizeFactor;
set => RaiseAndSetIfChanged(ref _resizeFactor, Math.Max(1, value));
}
public bool EnhanceContrast
{
get => _enhanceContrast;
set => RaiseAndSetIfChanged(ref _enhanceContrast, value);
}
public sbyte BrightnessGain
{
get => _brightnessGain;
set => RaiseAndSetIfChanged(ref _brightnessGain, value);
}
public byte GapClosingIterations
{
get => _gapClosingIterations;
set => RaiseAndSetIfChanged(ref _gapClosingIterations, value);
}
public byte RemoveNoiseIterations
{
get => _removeNoiseIterations;
set => RaiseAndSetIfChanged(ref _removeNoiseIterations, value);
}
public byte GaussianBlur
{
get => _gaussianBlur;
set => RaiseAndSetIfChanged(ref _gaussianBlur, value);
}
public byte StartThresholdRange
{
get => _startThresholdRange;
set => RaiseAndSetIfChanged(ref _startThresholdRange, Math.Max((byte)1, value));
}
public byte EndThresholdRange
{
get => _endThresholdRange;
set => RaiseAndSetIfChanged(ref _endThresholdRange, Math.Max((byte)1, value));
}
public decimal BaseThickness
{
get => _baseThickness;
set => RaiseAndSetIfChanged(ref _baseThickness, Math.Max(0, value));
}
public LithophaneBaseType BaseType
{
get => _baseType;
set => RaiseAndSetIfChanged(ref _baseType, value);
}
public ushort BaseMargin
{
get => _baseMargin;
set => RaiseAndSetIfChanged(ref _baseMargin, value);
}
public decimal LithophaneHeight
{
get => _lithophaneHeight;
set => RaiseAndSetIfChanged(ref _lithophaneHeight, Math.Max(0.01m, value));
}
public bool OneLayerPerThreshold
{
get => _oneLayerPerThreshold;
set => RaiseAndSetIfChanged(ref _oneLayerPerThreshold, value);
}
public bool EnableAntiAliasing
{
get => _enableAntiAliasing;
set => RaiseAndSetIfChanged(ref _enableAntiAliasing, value);
}
#endregion
#region Equality
protected bool Equals(OperationLithophane other)
{
return _layerHeight == other._layerHeight && _bottomLayerCount == other._bottomLayerCount && _bottomExposure == other._bottomExposure && _normalExposure == other._normalExposure && _filePath == other._filePath && _rotate == other._rotate && _mirror == other._mirror && _invertColor == other._invertColor && _enhanceContrast == other._enhanceContrast && _resizeFactor == other._resizeFactor && _brightnessGain == other._brightnessGain && _gapClosingIterations == other._gapClosingIterations && _removeNoiseIterations == other._removeNoiseIterations && _gaussianBlur == other._gaussianBlur && _startThresholdRange == other._startThresholdRange && _endThresholdRange == other._endThresholdRange && _baseThickness == other._baseThickness && _baseType == other._baseType && _baseMargin == other._baseMargin && _lithophaneHeight == other._lithophaneHeight && _oneLayerPerThreshold == other._oneLayerPerThreshold && _enableAntiAliasing == other._enableAntiAliasing;
}
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((OperationLithophane) obj);
}
public override int GetHashCode()
{
var hashCode = new HashCode();
hashCode.Add(_layerHeight);
hashCode.Add(_bottomLayerCount);
hashCode.Add(_bottomExposure);
hashCode.Add(_normalExposure);
hashCode.Add(_filePath);
hashCode.Add((int) _rotate);
hashCode.Add(_mirror);
hashCode.Add(_invertColor);
hashCode.Add(_enhanceContrast);
hashCode.Add(_resizeFactor);
hashCode.Add(_brightnessGain);
hashCode.Add(_gapClosingIterations);
hashCode.Add(_removeNoiseIterations);
hashCode.Add(_gaussianBlur);
hashCode.Add(_startThresholdRange);
hashCode.Add(_endThresholdRange);
hashCode.Add(_baseThickness);
hashCode.Add((int) _baseType);
hashCode.Add(_baseMargin);
hashCode.Add(_lithophaneHeight);
hashCode.Add(_oneLayerPerThreshold);
hashCode.Add(_enableAntiAliasing);
return hashCode.ToHashCode();
}
#endregion
#region Methods
public Mat? GetSourceMat()
{
if (!FileExists) return null;
try
{
var mat = CvInvoke.Imread(_filePath, ImreadModes.Grayscale);
if (_invertColor) CvInvoke.BitwiseNot(mat, mat);
mat = mat.CropByBounds();
return mat.Size == Size.Empty ? null : mat;
}
catch
{
// ignored
}
return null;
}
public Mat? GetTargetMat()
{
var mat = GetSourceMat();
if (mat is null) return null;
if (_resizeFactor != 100) mat.Resize((double)_resizeFactor / 100.0);
if (_enhanceContrast) CvInvoke.EqualizeHist(mat, mat);
if (_brightnessGain != 0)
{
using var mask = mat.NewSetTo(new MCvScalar(Math.Abs(_brightnessGain)));
if(_brightnessGain > 0) CvInvoke.Add(mat, mask, mat, mat);
else CvInvoke.Subtract(mat, mask, mat, mat);
}
if (_gaussianBlur > 0)
{
var ksize = 1 + _gaussianBlur * 2;
CvInvoke.GaussianBlur(mat, mat, new Size(ksize, ksize), 0);
}
if (_removeNoiseIterations > 0) CvInvoke.MorphologyEx(mat, mat, MorphOp.Open, EmguExtensions.Kernel3x3Rectangle, EmguExtensions.AnchorCenter, _removeNoiseIterations, BorderType.Reflect101, default);
if (_gapClosingIterations > 0) CvInvoke.MorphologyEx(mat, mat, MorphOp.Close, EmguExtensions.Kernel3x3Rectangle, EmguExtensions.AnchorCenter, _gapClosingIterations, BorderType.Reflect101, default);
if (_rotate != RotateDirection.None) CvInvoke.Rotate(mat, mat, (RotateFlags) _rotate);
if (_mirror)
{
var flip = SlicerFile.DisplayMirror;
if (flip == FlipDirection.None) flip = FlipDirection.Horizontally;
CvInvoke.Flip(mat, mat, (FlipType)flip);
}
return mat;
}
protected override bool ExecuteInternally(OperationProgress progress)
{
using var mat = GetTargetMat();
if (mat is null) return false;
var layersBag = new ConcurrentDictionary<byte, Layer>();
progress.Reset("Threshold levels", byte.MaxValue);
Parallel.For(_startThresholdRange, _endThresholdRange, CoreSettings.GetParallelOptions(progress), threshold =>
{
progress.PauseIfRequested();
using var thresholdMat = new Mat();
CvInvoke.Threshold(mat, thresholdMat, threshold, byte.MaxValue, ThresholdType.Binary);
if (CvInvoke.CountNonZero(thresholdMat) == 0) return;
if (_enableAntiAliasing)
{
CvInvoke.GaussianBlur(thresholdMat, thresholdMat, new Size(3, 3), 0);
}
using var layerMat = EmguExtensions.InitMat(SlicerFile.Resolution);
thresholdMat.CopyToCenter(layerMat);
layersBag.TryAdd((byte)threshold, new Layer(layerMat, SlicerFile));
progress.LockAndIncrement();
});
if (layersBag.Count == 0)
{
throw new InvalidOperationException("Unable to continue due to no threshold layers was generated, either by lack of pixels or by using a short range.");
}
var thresholdLayers = layersBag.OrderBy(pair => pair.Key).Select(pair => pair.Value).ToArray();
if (!_oneLayerPerThreshold)
{
var layerIncrementF = thresholdLayers.Length * _layerHeight / Math.Max(_layerHeight, _lithophaneHeight);
if (layerIncrementF >= 2)
{
var layerIncrement = (uint) layerIncrementF;
var indexes = new int[(int)Math.Ceiling(thresholdLayers.Length / (float)layerIncrement)];
var newLayers = new Layer[indexes.Length];
var count = 0;
for (int index = 0; index < thresholdLayers.Length; index++)
{
if (index % layerIncrement != 0) continue;
newLayers[count] = thresholdLayers[index];
indexes[count++] = index;
}
progress.ResetNameAndProcessed("Packed layers");
Parallel.ForEach(indexes, CoreSettings.GetParallelOptions(progress), i =>
{
progress.PauseIfRequested();
progress.LockAndIncrement();
using var mat = thresholdLayers[i].LayerMat;
for (int index = i+1; index < i + layerIncrement && index < thresholdLayers.Length; index++)
{
using var nextMat = thresholdLayers[index].LayerMat;
CvInvoke.Max(mat, nextMat, mat);
progress.LockAndIncrement();
}
thresholdLayers[i].LayerMat = mat;
});
thresholdLayers = newLayers;
}
else if (layerIncrementF < 1)
{
var layerIncrement = (uint)Math.Ceiling(1/layerIncrementF);
if (layerIncrement > 1)
{
progress.Reset("Packed layers");
var newLayers = new Layer[thresholdLayers.Length * layerIncrement];
for (int i = 0; i < thresholdLayers.Length; i++)
{
var layer = thresholdLayers[i];
var newIndex = i * layerIncrement;
newLayers[newIndex] = layer;
for (int x = 1; x < layerIncrement; x++)
{
newLayers[++newIndex] = layer.Clone();
}
}
thresholdLayers = newLayers;
}
}
}
if (_baseType != LithophaneBaseType.None && _baseThickness > 0)
{
int baseLayerCount = (int)(_baseThickness / _layerHeight);
var newLayers = new Layer[thresholdLayers.Length + baseLayerCount];
using var baseMat = SlicerFile.CreateMat();
switch (_baseType)
{
case LithophaneBaseType.Square:
{
var rectangle = new Rectangle(
baseMat.Width / 2 - mat.Width / 2 - _baseMargin / 2,
baseMat.Height / 2 - mat.Height / 2 - _baseMargin / 2,
mat.Width + _baseMargin,
mat.Height + _baseMargin);
CvInvoke.Rectangle(baseMat, rectangle, EmguExtensions.WhiteColor, -1, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected);
break;
}
case LithophaneBaseType.Model:
{
using var dilatedMat = new Mat();
CvInvoke.Threshold(mat, dilatedMat, 1, byte.MaxValue, ThresholdType.Binary);
CvInvoke.Dilate(dilatedMat, dilatedMat, EmguExtensions.Kernel3x3Rectangle, EmguExtensions.AnchorCenter, _baseMargin, BorderType.Reflect101, default);
dilatedMat.CopyToCenter(baseMat);
break;
}
}
var baseLayer = new Layer(baseMat, SlicerFile);
newLayers[0] = baseLayer;
for (int i = 1; i < baseLayerCount; i++)
{
newLayers[i] = baseLayer.Clone();
}
Array.Copy(thresholdLayers, 0, newLayers, baseLayerCount, thresholdLayers.Length);
thresholdLayers = newLayers;
}
SlicerFile.SuppressRebuildPropertiesWork(() =>
{
SlicerFile.LayerHeight = (float) _layerHeight;
SlicerFile.BottomLayerCount = _bottomLayerCount;
SlicerFile.BottomExposureTime = (float) _bottomExposure;
SlicerFile.ExposureTime = (float) _normalExposure;
SlicerFile.Layers = thresholdLayers;
}, true);
using var bgrMat = new Mat();
CvInvoke.CvtColor(mat, bgrMat, ColorConversion.Gray2Bgr);
int baseLine = 0;
var textSize = CvInvoke.GetTextSize("UVtools Lithophane", FontFace.HersheyDuplex, 2, 3, ref baseLine);
CvInvoke.PutText(bgrMat, "UVtools Lithophane", new Point(bgrMat.Width / 2 - textSize.Width / 2, 60), FontFace.HersheyDuplex, 2, new MCvScalar(255, 27, 245), 3);
SlicerFile.SetThumbnails(bgrMat);
return !progress.Token.IsCancellationRequested;
}
#endregion
}