diff options
29 files changed, 1203 insertions, 164 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index e9ffca0..4cd4313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 10/04/2022 - v3.3.0 + +- **Shortcuts:** + - (Add) **Delete:** While on layer preview and with roi or mask(s) selected, will remove the selected area from layer + - (Add) **Alt + Delete:** While on layer preview and with roi or mask(s) selected, will remove the selected area from all layers + - (Add) **Ctrl + Delete:** While on layer preview, will remove the current layer + - (Add) **Insert:** While on layer preview and with roi or mask(s) selected, will keep only the selected area in layer + - (Add) **Alt + Insert:** While on layer preview and with roi or mask(s) selected, will keep only the selected area in all layers + - (Add) **Ctrl + Insert:** While on layer preview, will clone the current layer + - (Add) **Home:** While on layer preview will go to first layer + - (Add) **End:** While on layer preview will go to last layer + - (Add) **Page up:** While on layer preview will skip +10 layers + - (Add) **Page down:** While on layer preview will skip -10 layers +- (Add) Tool - Lithophane: Generate lithophane from a picture +- (Fix) Pixel arithmetic: When run with masks it produce a incorrect outcome +- (Fix) CXDLP: Layer area table miscalculation, causing slow down prints + ## 06/04/2022 - v3.2.2 - **Settings:** diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1ebb05c..11aab5f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,23 +1,15 @@ -- **Settings:** - - (Add) Remove source file after automatic conversion (#444) - - (Add) Remove source file after manual conversion (#444) - - (Add) **Average resin bottle cost:** The average cost per one resin bottle of 1000ml. - Used to calculate the material cost when the file lacks that information. - Use 0 to disable this feature and only show the cost if file have that information. - If this value is changed, you need to reload the current file to update the cost. - - (Change) Move "Expand and show tool descriptions by default" to From `General` to `Tools` tab (Setting will reset to default) -- **File formats:** - - (Add) Property `StartingMaterialMilliliters`: Gets the starting material milliliters when the file was loaded - - (Add) Property `StartingMaterialCost`: Gets the starting material cost when the file was loaded - - (Add) Property `MaterialMilliliterCost`: Gets the material cost per one milliliter - - (Improvement) Update `MaterialCost` when `MaterialMilliliters` changes (#449) -- **Raft relief:** - - (Add) Linked lines: Remove the raft, keep supports and link them with lines - - (Improvement) Change the supports detection parameters to be more effective and precise on detect the starting layer - - (Fix) Brightness percentage not getting updated - - (Fix) Remove anti-aliased edges from Tabs -- (Improvement) Core: Minor clean-up -- (Fix) Issue repair error when 'Auto repair layers and issues on file load' is enabled (#446) -- (Fix) UI: Selecting a object with ROI when flip is verically, will cause 1px up-shift on the preview -- (Fix) macOS permission error due loss of attributes on the build script, WSL have bug with mv, using ln&rm instead +- **Shortcuts:** + - (Add) **Delete:** While on layer preview and with roi or mask(s) selected, will remove the selected area from layer + - (Add) **Alt + Delete:** While on layer preview and with roi or mask(s) selected, will remove the selected area from all layers + - (Add) **Ctrl + Delete:** While on layer preview, will remove the current layer + - (Add) **Insert:** While on layer preview and with roi or mask(s) selected, will keep only the selected area in layer + - (Add) **Alt + Insert:** While on layer preview and with roi or mask(s) selected, will keep only the selected area in all layers + - (Add) **Ctrl + Insert:** While on layer preview, will clone the current layer + - (Add) **Home:** While on layer preview will go to first layer + - (Add) **End:** While on layer preview will go to last layer + - (Add) **Page up:** While on layer preview will skip +10 layers + - (Add) **Page down:** While on layer preview will skip -10 layers +- (Add) Tool - Lithophane: Generate lithophane from a picture +- (Fix) Pixel arithmetic: When run with masks it produce a incorrect outcome +- (Fix) CXDLP: Layer area table miscalculation, causing slow down prints diff --git a/Scripts/010 Editor/cxdlp.bt b/Scripts/010 Editor/cxdlp.bt index 6d82510..b92fae4 100644 --- a/Scripts/010 Editor/cxdlp.bt +++ b/Scripts/010 Editor/cxdlp.bt @@ -24,9 +24,9 @@ typedef struct { typedef struct() { uint32 layerArea <fgcolor=cBlack, bgcolor=cWhite>; - uint32 layerPointNum <fgcolor=cBlack, bgcolor=cWhite>; + uint32 layerLineCount <fgcolor=cBlack, bgcolor=cWhite>; - layerPointsData pD()[layerPointNum]; + layerPointsData pD()[layerLineCount]; ubyte CR_LF2[2] <fgcolor=cBlack, bgcolor=cRed>; } layerData; diff --git a/UVtools.Core/EmguCV/EmguContour.cs b/UVtools.Core/EmguCV/EmguContour.cs index 4ecfecb..f7d3bd1 100644 --- a/UVtools.Core/EmguCV/EmguContour.cs +++ b/UVtools.Core/EmguCV/EmguContour.cs @@ -14,6 +14,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Drawing; +using System.Linq; using UVtools.Core.Extensions; namespace UVtools.Core.EmguCV; @@ -21,7 +22,7 @@ namespace UVtools.Core.EmguCV; /// <summary> /// A contour cache for OpenCV /// </summary> -public class EmguContour : IReadOnlyCollection<Point>, IDisposable +public class EmguContour : IReadOnlyCollection<Point>, IDisposable, IComparable<EmguContour>, IComparer<EmguContour> { #region Constants @@ -257,4 +258,43 @@ public class EmguContour : IReadOnlyCollection<Point>, IDisposable _moments?.Dispose(); } #endregion + + #region Equality + + protected bool Equals(EmguContour other) + { + if (Count != other.Count) return false; + return _points.ToArray().SequenceEqual(other.ToArray()); + } + + 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((EmguContour) obj); + } + + public override int GetHashCode() + { + return _points.GetHashCode(); + } + + public int CompareTo(EmguContour? other) + { + if (ReferenceEquals(this, other)) return 0; + if (ReferenceEquals(null, other)) return 1; + return _area.CompareTo(other._area); + } + + public int Compare(EmguContour? x, EmguContour? y) + { + if (ReferenceEquals(x, y)) return 0; + if (ReferenceEquals(null, y)) return 1; + if (ReferenceEquals(null, x)) return -1; + return x._area.CompareTo(y._area); + } + #endregion + + }
\ No newline at end of file diff --git a/UVtools.Core/EmguCV/EmguContours.cs b/UVtools.Core/EmguCV/EmguContours.cs index d8c0639..d0ac9a5 100644 --- a/UVtools.Core/EmguCV/EmguContours.cs +++ b/UVtools.Core/EmguCV/EmguContours.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Threading.Tasks; +using Emgu.CV.CvEnum; using UVtools.Core.Extensions; namespace UVtools.Core.EmguCV; @@ -38,6 +39,7 @@ public class EmguContours : IReadOnlyList<EmguContour>, IDisposable public int Count => _contours.Length; + public readonly int[,] Hierarchy = new int[0,0]; public EmguContour this[int index] => _contours[index]; @@ -51,6 +53,21 @@ public class EmguContours : IReadOnlyList<EmguContour>, IDisposable } } + public EmguContours(VectorOfVectorOfPoint vectorOfPointsOfPoints, int[,] hierarchy) : this(vectorOfPointsOfPoints) + { + Hierarchy = hierarchy; + } + + public EmguContours(IInputOutputArray mat, RetrType mode = RetrType.List, ChainApproxMethod method = ChainApproxMethod.ChainApproxSimple, Point offset = default) + { + using var contours = mat.FindContours(out Hierarchy, mode, method, offset); + _contours = new EmguContour[contours.Size]; + for (int i = 0; i < _contours.Length; i++) + { + _contours[i] = new EmguContour(contours[i]); + } + } + public (int Index, EmguContour Contour, double Distance)[][] CalculateCentroidDistances(bool includeOwn = false, bool sortByDistance = true) { var items = new (int Index, EmguContour Contour, double Distance)[Count][]; @@ -259,6 +276,24 @@ public class EmguContours : IReadOnlyList<EmguContour>, IDisposable } /// <summary> + /// Gets the largest contour area from a contour list + /// </summary> + /// <param name="contours">Contour list</param> + /// <returns></returns> + public static double GetLargestContourArea(VectorOfVectorOfPoint contours) + { + var vectorSize = contours.Size; + if (vectorSize == 0) return 0; + + double result = 0; + for (var i = 0; i < vectorSize; i++) + { + result = Math.Max(result, CvInvoke.ContourArea(contours[i])); + } + return result; + } + + /// <summary> /// Gets contours real area for a group of contours /// </summary> /// <param name="contours">Grouped contours</param> @@ -305,8 +340,8 @@ public class EmguContours : IReadOnlyList<EmguContour>, IDisposable using var contour2Mat = EmguExtensions.InitMat(totalRect.Size); var inverseOffset = new Point(-totalRect.X, -totalRect.Y); - CvInvoke.DrawContours(contour1Mat, contour1, -1, EmguExtensions.WhiteColor, -1, Emgu.CV.CvEnum.LineType.EightConnected, null, int.MaxValue, inverseOffset); - CvInvoke.DrawContours(contour2Mat, contour2, -1, EmguExtensions.WhiteColor, -1, Emgu.CV.CvEnum.LineType.EightConnected, null, int.MaxValue, inverseOffset); + CvInvoke.DrawContours(contour1Mat, contour1, -1, EmguExtensions.WhiteColor, -1, LineType.EightConnected, null, int.MaxValue, inverseOffset); + CvInvoke.DrawContours(contour2Mat, contour2, -1, EmguExtensions.WhiteColor, -1, LineType.EightConnected, null, int.MaxValue, inverseOffset); CvInvoke.BitwiseAnd(contour1Mat, contour2Mat, contour1Mat); diff --git a/UVtools.Core/Enumerations.cs b/UVtools.Core/Enumerations.cs index e756538..1e4e9b8 100644 --- a/UVtools.Core/Enumerations.cs +++ b/UVtools.Core/Enumerations.cs @@ -32,27 +32,33 @@ public enum LayerRangeSelection : byte Last } -public enum FlipDirection : byte +/// <summary> +/// Flip direction, same as <see cref="FlipType"/>, but add None which must be taken care/ignored on code +/// </summary> +public enum FlipDirection : sbyte { - None, - Horizontally, - Vertically, - Both, + None = sbyte.MinValue, + Horizontally = FlipType.Horizontal, + Vertically = FlipType.Vertical, + Both = FlipType.Both, } +/// <summary> +/// Rotate direction, same as <see cref="RotateFlags"/>, but add None which must be taken care/ignored on code +/// </summary> public enum RotateDirection : sbyte { [Description("None")] None = -1, /// <summary>Rotate 90 degrees clockwise (0)</summary> [Description("Rotate 90º CW")] - Rotate90Clockwise = 0, + Rotate90Clockwise = RotateFlags.Rotate90Clockwise, /// <summary>Rotate 180 degrees clockwise (1)</summary> [Description("Rotate 180º")] - Rotate180 = 1, + Rotate180 = RotateFlags.Rotate180, /// <summary>Rotate 270 degrees clockwise (2)</summary> [Description("Rotate 90º CCW")] - Rotate90CounterClockwise = 2, + Rotate90CounterClockwise = RotateFlags.Rotate90CounterClockwise, } public enum Anchor : byte @@ -118,6 +124,7 @@ public enum RemoveSourceFileAction : byte Prompt } +/* public static class Enumerations { public static FlipType ToOpenCVFlipType(FlipDirection flip) @@ -143,4 +150,4 @@ public static class Enumerations _ => throw new ArgumentOutOfRangeException(nameof(rotate), rotate, null) }; } -}
\ No newline at end of file +}*/
\ No newline at end of file diff --git a/UVtools.Core/Extensions/EmguExtensions.cs b/UVtools.Core/Extensions/EmguExtensions.cs index 243b669..07120bd 100644 --- a/UVtools.Core/Extensions/EmguExtensions.cs +++ b/UVtools.Core/Extensions/EmguExtensions.cs @@ -440,6 +440,9 @@ public static class EmguExtensions { return CvInvoke.Imencode(".png", mat); } + + public static Point GetCenterPoint(this Mat mat) => new(mat.Width / 2, mat.Height / 2); + #endregion #region Create methods @@ -457,20 +460,28 @@ public static class EmguExtensions return src.CreateMask(vec); } - public static Mat TrimByBounds(this Mat src) + public static Mat CropByBounds(this Mat src, bool cloneInsteadRoi = false) { var rect = CvInvoke.BoundingRectangle(src); if (rect.Size == Size.Empty) return src.New(); - if (src.Size == rect.Size) return src.Clone(); - using var roi = src.Roi(rect); - return roi.Clone(); + if (src.Size == rect.Size) return cloneInsteadRoi ? src.Roi(src.Size) : src.Clone(); + var roi = src.Roi(rect); + + if (cloneInsteadRoi) + { + var clone = roi.Clone(); + roi.Dispose(); + return clone; + } + + return roi; } - public static void TrimByBounds(this Mat src, Mat dst) + public static void CropByBounds(this Mat src, Mat dst) { - var mat = src.TrimByBounds(); - CvInvoke.Swap(mat, dst); - src.Dispose(); + using var mat = src.CropByBounds(); + dst.Create(mat.Rows, mat.Cols, mat.Depth, mat.NumberOfChannels); + src.CopyTo(dst); } @@ -918,6 +929,18 @@ public static class EmguExtensions xTrans + (src.Width - src.Width * xScale) / 2.0, yTrans + (src.Height - src.Height * yScale) / 2.0, dstSize, interpolation); } + + /// <summary> + /// Resize source mat proportional to a scale + /// </summary> + /// <param name="src"></param> + /// <param name="scale"></param> + /// <param name="interpolation"></param> + public static void Resize(this Mat src, double scale, Inter interpolation = Inter.Linear) + { + if (scale == 1) return; + CvInvoke.Resize(src, src, new Size((int) (src.Width * scale), (int) (src.Height * scale)), 0, 0, interpolation); + } #endregion #region Draw Methods diff --git a/UVtools.Core/FileFormats/CXDLPFile.cs b/UVtools.Core/FileFormats/CXDLPFile.cs index eea39a5..d303793 100644 --- a/UVtools.Core/FileFormats/CXDLPFile.cs +++ b/UVtools.Core/FileFormats/CXDLPFile.cs @@ -19,7 +19,9 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Emgu.CV.CvEnum; using UVtools.Core.Converters; +using UVtools.Core.EmguCV; using UVtools.Core.Extensions; using UVtools.Core.Layers; using UVtools.Core.Objects; @@ -720,16 +722,10 @@ public class CXDLPFile : FileFormat //var preLayers = new PreLayer[LayerCount]; //var layerDefs = new LayerDef[LayerCount]; //var layersStreams = new MemoryStream[LayerCount]; - - for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) - { - //var layer = this[layerIndex]; - outputFile.WriteBytes(BitExtensions.ToBytesBigEndian((uint)Math.Round(this[layerIndex].BoundingRectangleMillimeters.Area()*1000))); - //preLayers[layerIndex] = new(layer.NonZeroPixelCount); - } - //Helpers.SerializeWriteFileStream(outputFile, preLayers); - //Helpers.SerializeWriteFileStream(outputFile, pageBreak); + + var layerAreaPosition = outputFile.Position; + outputFile.Seek(4 * LayerCount, SeekOrigin.Current); outputFile.WriteBytes(pageBreak); if (HeaderSettings.Version >= 3) @@ -737,7 +733,9 @@ public class CXDLPFile : FileFormat Helpers.SerializeWriteFileStream(outputFile, SlicerInfoV3Settings); } + var layerLargestContourArea = new uint[LayerCount]; var layerBytes = new List<byte>[LayerCount]; + var pixelArea = PixelArea; foreach (var batch in BatchLayersIndexes()) { Parallel.ForEach(batch, CoreSettings.GetParallelOptions(progress), layerIndex => @@ -745,6 +743,10 @@ public class CXDLPFile : FileFormat var layer = this[layerIndex]; using (var mat = layer.LayerMat) { + using var contours = mat.FindContours(RetrType.External); + layerLargestContourArea[layerIndex] = (uint)(EmguContours.GetLargestContourArea(contours) * pixelArea * 1000); + //Debug.WriteLine($"Area: {contourArea} ({contourArea * PixelArea * 1000}) BR: {max.Bounds.Area()} ({max.Bounds.Area() * PixelArea * 1000})"); + var span = mat.GetDataByteSpan(); layerBytes[layerIndex] = new(); @@ -783,9 +785,7 @@ public class CXDLPFile : FileFormat } } - layerBytes[layerIndex].InsertRange(0, LayerDef.GetHeaderBytes( - (uint)Math.Round(layer.BoundingRectangleMillimeters.Area() * 1000), - lineCount)); + layerBytes[layerIndex].InsertRange(0, LayerDef.GetHeaderBytes(layerLargestContourArea[layerIndex], lineCount)); layerBytes[layerIndex].AddRange(pageBreak); } @@ -799,75 +799,14 @@ public class CXDLPFile : FileFormat } } - - /*Parallel.For(0, LayerCount, CoreSettings.ParallelOptions, - //new ParallelOptions{MaxDegreeOfParallelism = 1}, - layerIndex => - { - if (progress.Token.IsCancellationRequested) return; - //List<LayerLine> layerLines = new(); - var layer = this[layerIndex]; - using var mat = layer.LayerMat; - var span = mat.GetDataByteSpan(); - - layerBytes[layerIndex] = new(); - - for (int x = layer.BoundingRectangle.X; x < layer.BoundingRectangle.Right; x++) - { - int y = layer.BoundingRectangle.Y; - int startY = -1; - byte lastColor = 0; - for (; y < layer.BoundingRectangle.Bottom; y++) - { - int pos = mat.GetPixelPos(x, y); - byte color = span[pos]; - - if (lastColor == color && color != 0) continue; - - if (startY >= 0) - { - layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); - //layerLines.Add(new LayerLine((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); - //Debug.WriteLine(layerLines[^1]); - } - - startY = color == 0 ? -1 : y; - - lastColor = color; - } - - if (startY >= 0) - { - layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); - //layerLines.Add(new LayerLine((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); - //Debug.WriteLine(layerLines[^1]); - } - } - - //layerDefs[layerIndex] = new LayerDef(layer.NonZeroPixelCount, (uint)layerLines.Count, layerLines.ToArray()); - //var layerDef = new LayerDef(layer.NonZeroPixelCount, (uint)layerLines.Count, layerLines.ToArray()); - //layersStreams[layerIndex] = new MemoryStream(); - //Helpers.Serializer.Serialize(layersStreams[layerIndex], layerDef); - - //layerBytes[layerIndex].InsertRange(0, LayerDef.GetHeaderBytes(layer.NonZeroPixelCount, (uint) layerBytes[layerIndex].Count)); - //layerBytes[layerIndex].AddRange(PageBreak.Bytes); - - progress.LockAndIncrement(); - }); - - progress.Reset(OperationProgress.StatusWritingFile, LayerCount); - for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) + // Write layer largest contour area (mm^2 * 1000) + outputFile.Seek(layerAreaPosition, SeekOrigin.Begin); + foreach (var area in layerLargestContourArea) { - progress.Token.ThrowIfCancellationRequested(); - //Helpers.SerializeWriteFileStream(outputFile, layerDefs[layerIndex]); - //outputFile.WriteStream(layersStreams[layerIndex]); - //layersStreams[layerIndex].Dispose(); - outputFile.WriteBytes(LayerDef.GetHeaderBytes(this[layerIndex].NonZeroPixelCount, (uint)layerBytes[layerIndex].Count)); - outputFile.WriteBytes(layerBytes[layerIndex].ToArray()); - outputFile.WriteBytes(pageBreak); - progress++; - }*/ + outputFile.WriteUIntBigEndian(area); + } + outputFile.Seek(0, SeekOrigin.End); Helpers.SerializeWriteFileStream(outputFile, FooterSettings); progress.Reset("Calculating checksum"); @@ -986,7 +925,7 @@ public class CXDLPFile : FileFormat linesBytes[layerIndex] = null!; - _layers[layerIndex] = new Layer((uint)layerIndex, mat, this); + _layers[layerIndex] = new Layer((uint)layerIndex, mat, this); } progress.LockAndIncrement(); diff --git a/UVtools.Core/Layers/Layer.cs b/UVtools.Core/Layers/Layer.cs index 4db7a9e..535f93d 100644 --- a/UVtools.Core/Layers/Layer.cs +++ b/UVtools.Core/Layers/Layer.cs @@ -1488,6 +1488,17 @@ public class Layer : BindableBase, IEquatable<Layer>, IEquatable<uint> _materialMilliliters = _materialMilliliters, };*/ } + + public Layer[] Clone(uint times) + { + var layers = new Layer[times]; + for (int i = 0; i < times; i++) + { + layers[i] = Clone(); + } + + return layers; + } #endregion #region Static Methods diff --git a/UVtools.Core/Managers/MatCacheManager.cs b/UVtools.Core/Managers/MatCacheManager.cs index 2934c3f..9ffef33 100644 --- a/UVtools.Core/Managers/MatCacheManager.cs +++ b/UVtools.Core/Managers/MatCacheManager.cs @@ -170,12 +170,12 @@ public class MatCacheManager : IDisposable if (Flip != FlipDirection.None) { - CvInvoke.Flip(MatCache[currentCacheIndex][0], MatCache[currentCacheIndex][0], Enumerations.ToOpenCVFlipType(Flip)); + CvInvoke.Flip(MatCache[currentCacheIndex][0], MatCache[currentCacheIndex][0], (FlipType)Flip); } if (Rotate != RotateDirection.None) { - CvInvoke.Rotate(MatCache[currentCacheIndex][0], MatCache[currentCacheIndex][0], Enumerations.ToOpenCVRotateFlags(Rotate)); + CvInvoke.Rotate(MatCache[currentCacheIndex][0], MatCache[currentCacheIndex][0], (RotateFlags) Rotate); } if (StripAntiAliasing) diff --git a/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs b/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs index bce1855..58c73d6 100644 --- a/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs +++ b/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs @@ -671,7 +671,7 @@ public sealed class OperationCalibrateElephantFoot : Operation { var flip = SlicerFile.DisplayMirror; if (flip == FlipDirection.None) flip = FlipDirection.Horizontally; - Parallel.ForEach(layers, CoreSettings.ParallelOptions, mat => CvInvoke.Flip(mat, mat, Enumerations.ToOpenCVFlipType(flip))); + Parallel.ForEach(layers, CoreSettings.ParallelOptions, mat => CvInvoke.Flip(mat, mat, (FlipType)flip)); } // Preview diff --git a/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs b/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs index 5adc175..efd6b15 100644 --- a/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs +++ b/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs @@ -2238,7 +2238,7 @@ public sealed class OperationCalibrateExposureFinder : Operation { var flip = SlicerFile.DisplayMirror; if (flip == FlipDirection.None) flip = FlipDirection.Horizontally; - new OperationFlip(SlicerFile) { FlipDirection = Enumerations.ToOpenCVFlipType(flip) }.Execute(progress); + new OperationFlip(SlicerFile) { FlipDirection = (FlipType)flip }.Execute(progress); } } diff --git a/UVtools.Core/Operations/OperationCalibrateGrayscale.cs b/UVtools.Core/Operations/OperationCalibrateGrayscale.cs index 4b64dd4..010e3a4 100644 --- a/UVtools.Core/Operations/OperationCalibrateGrayscale.cs +++ b/UVtools.Core/Operations/OperationCalibrateGrayscale.cs @@ -467,7 +467,7 @@ public sealed class OperationCalibrateGrayscale : Operation { var flip = SlicerFile.DisplayMirror; if (flip == FlipDirection.None) flip = FlipDirection.Horizontally; - Parallel.ForEach(layers, CoreSettings.ParallelOptions, mat => CvInvoke.Flip(mat, mat, Enumerations.ToOpenCVFlipType(flip))); + Parallel.ForEach(layers, CoreSettings.ParallelOptions, mat => CvInvoke.Flip(mat, mat, (FlipType)flip)); } return layers; diff --git a/UVtools.Core/Operations/OperationCalibrateStressTower.cs b/UVtools.Core/Operations/OperationCalibrateStressTower.cs index c651fe4..ce33999 100644 --- a/UVtools.Core/Operations/OperationCalibrateStressTower.cs +++ b/UVtools.Core/Operations/OperationCalibrateStressTower.cs @@ -387,7 +387,7 @@ public sealed class OperationCalibrateStressTower : Operation { var flip = SlicerFile.DisplayMirror; if (flip == FlipDirection.None) flip = FlipDirection.Horizontally; - Parallel.ForEach(layers, CoreSettings.ParallelOptions, mat => CvInvoke.Flip(mat, mat, Enumerations.ToOpenCVFlipType(flip))); + Parallel.ForEach(layers, CoreSettings.ParallelOptions, mat => CvInvoke.Flip(mat, mat, (FlipType)flip)); } return layers; diff --git a/UVtools.Core/Operations/OperationCalibrateTolerance.cs b/UVtools.Core/Operations/OperationCalibrateTolerance.cs index d675c82..c6b9460 100644 --- a/UVtools.Core/Operations/OperationCalibrateTolerance.cs +++ b/UVtools.Core/Operations/OperationCalibrateTolerance.cs @@ -687,7 +687,7 @@ public sealed class OperationCalibrateTolerance : Operation { var flip = SlicerFile.DisplayMirror; if (flip == FlipDirection.None) flip = FlipDirection.Horizontally; - Parallel.ForEach(layers, CoreSettings.ParallelOptions, mat => CvInvoke.Flip(mat, mat, Enumerations.ToOpenCVFlipType(flip))); + Parallel.ForEach(layers, CoreSettings.ParallelOptions, mat => CvInvoke.Flip(mat, mat, (FlipType)flip)); } return layers; diff --git a/UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs b/UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs index 8e64255..dcee1aa 100644 --- a/UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs +++ b/UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs @@ -722,7 +722,7 @@ public sealed class OperationCalibrateXYZAccuracy : Operation { var flip = SlicerFile.DisplayMirror; if (flip == FlipDirection.None) flip = FlipDirection.Horizontally; - Parallel.ForEach(layers, CoreSettings.ParallelOptions, mat => CvInvoke.Flip(mat, mat, Enumerations.ToOpenCVFlipType(flip))); + Parallel.ForEach(layers, CoreSettings.ParallelOptions, mat => CvInvoke.Flip(mat, mat, (FlipType)flip)); } return layers; diff --git a/UVtools.Core/Operations/OperationLayerExportGif.cs b/UVtools.Core/Operations/OperationLayerExportGif.cs index cce026a..cd4a0ed 100644 --- a/UVtools.Core/Operations/OperationLayerExportGif.cs +++ b/UVtools.Core/Operations/OperationLayerExportGif.cs @@ -239,12 +239,12 @@ public sealed class OperationLayerExportGif : Operation if (_flipDirection != FlipDirection.None) { - CvInvoke.Flip(matRoi, matRoi, Enumerations.ToOpenCVFlipType(_flipDirection)); + CvInvoke.Flip(matRoi, matRoi, (FlipType)_flipDirection); } if (_rotateDirection != RotateDirection.None) { - CvInvoke.Rotate(matRoi, matRoi, Enumerations.ToOpenCVRotateFlags(_rotateDirection)); + CvInvoke.Rotate(matRoi, matRoi, (RotateFlags)_rotateDirection); } if (_renderLayerCount) diff --git a/UVtools.Core/Operations/OperationLayerExportHeatMap.cs b/UVtools.Core/Operations/OperationLayerExportHeatMap.cs index 47ca83c..2ed99d2 100644 --- a/UVtools.Core/Operations/OperationLayerExportHeatMap.cs +++ b/UVtools.Core/Operations/OperationLayerExportHeatMap.cs @@ -175,12 +175,12 @@ public sealed class OperationLayerExportHeatMap : Operation if (_flipDirection != FlipDirection.None) { - CvInvoke.Flip(sumMat, sumMat, Enumerations.ToOpenCVFlipType(_flipDirection)); + CvInvoke.Flip(sumMat, sumMat, (FlipType)_flipDirection); } if (_rotateDirection != RotateDirection.None) { - CvInvoke.Rotate(sumMat, sumMat, Enumerations.ToOpenCVRotateFlags(_rotateDirection)); + CvInvoke.Rotate(sumMat, sumMat, (RotateFlags)_rotateDirection); } if (_cropByRoi && HaveROI) diff --git a/UVtools.Core/Operations/OperationLayerExportImage.cs b/UVtools.Core/Operations/OperationLayerExportImage.cs index 5fbc7ca..b8787a8 100644 --- a/UVtools.Core/Operations/OperationLayerExportImage.cs +++ b/UVtools.Core/Operations/OperationLayerExportImage.cs @@ -191,12 +191,12 @@ public sealed class OperationLayerExportImage : Operation if (_flipDirection != FlipDirection.None) { - CvInvoke.Flip(matRoi, matRoi, Enumerations.ToOpenCVFlipType(_flipDirection)); + CvInvoke.Flip(matRoi, matRoi, (FlipType)_flipDirection); } if (_rotateDirection != RotateDirection.None) { - CvInvoke.Rotate(matRoi, matRoi, Enumerations.ToOpenCVRotateFlags(_rotateDirection)); + CvInvoke.Rotate(matRoi, matRoi, (RotateFlags)_rotateDirection); } var filename = SlicerFile[layerIndex].FormatFileName(_filename, _padLayerIndex ? SlicerFile.LayerDigits : byte.MinValue, IndexStartNumber.Zero, string.Empty); diff --git a/UVtools.Core/Operations/OperationLithophane.cs b/UVtools.Core/Operations/OperationLithophane.cs new file mode 100644 index 0000000..d497e36 --- /dev/null +++ b/UVtools.Core/Operations/OperationLithophane.cs @@ -0,0 +1,527 @@ +/* + * 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 System; +using System.Collections.Concurrent; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using UVtools.Core.Extensions; +using UVtools.Core.FileFormats; +using UVtools.Core.Layers; + +namespace UVtools.Core.Operations; + +[Serializable] +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 = 4; + private byte _removeNoiseIterations = 4; + 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 => "fas 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 selected file is empty"); + } + else if(!File.Exists(_filePath)) + { + sb.AppendLine("The selected file does not exists"); + } + + if (_startThresholdRange > _endThresholdRange) + { + sb.AppendLine("Start threshold can't be higher than end threshold"); + } + + 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}%"); + } + } + + 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, new Point(-1, -1), _removeNoiseIterations, BorderType.Reflect101, default); + if (_gapClosingIterations > 0) CvInvoke.MorphologyEx(mat, mat, MorphOp.Close, EmguExtensions.Kernel3x3Rectangle, new Point(-1, -1), _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 => + { + 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(); + }); + + 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.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)(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, new Point(-1, -1), _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 +}
\ No newline at end of file diff --git a/UVtools.Core/Operations/OperationPixelArithmetic.cs b/UVtools.Core/Operations/OperationPixelArithmetic.cs index 8ae65f2..1613018 100644 --- a/UVtools.Core/Operations/OperationPixelArithmetic.cs +++ b/UVtools.Core/Operations/OperationPixelArithmetic.cs @@ -866,7 +866,7 @@ public class OperationPixelArithmetic : Operation default: throw new ArgumentOutOfRangeException(nameof(_ignoreAreaOperator)); } - ApplyMask(originalRoi, target); + ApplyMask(original, mat); SlicerFile[layerIndex].LayerMat = mat; diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj index 69928f1..32b8612 100644 --- a/UVtools.Core/UVtools.Core.csproj +++ b/UVtools.Core/UVtools.Core.csproj @@ -10,7 +10,7 @@ <RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl> <PackageProjectUrl>https://github.com/sn4k3/UVtools</PackageProjectUrl> <Description>MSLA/DLP, file analysis, calibration, repair, conversion and manipulation</Description> - <Version>3.2.2</Version> + <Version>3.3.0</Version> <Copyright>Copyright © 2020 PTRTECH</Copyright> <PackageIcon>UVtools.png</PackageIcon> <Platforms>AnyCPU;x64</Platforms> diff --git a/UVtools.InstallerMM/UVtools.InstallerMM.wxs b/UVtools.InstallerMM/UVtools.InstallerMM.wxs index fb18481..3257bcb 100644 --- a/UVtools.InstallerMM/UVtools.InstallerMM.wxs +++ b/UVtools.InstallerMM/UVtools.InstallerMM.wxs @@ -2,7 +2,7 @@ <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"> <?define ComponentRules="OneToOne"?> <!-- SourceDir instructs IsWiX the location of the directory that contains files for this merge module --> - <?define SourceDir="..\publish\UVtools_win-x64_v3.2.2"?> + <?define SourceDir="..\publish\UVtools_win-x64_v3.3.0"?> <Module Id="UVtools" Language="1033" Version="1.0.0.0"> <Package Id="12aaa1cf-ff06-4a02-abd5-2ac01ac4f83b" Manufacturer="PTRTECH" InstallerVersion="200" Keywords="MSLA, DLP" Description="MSLA/DLP, file analysis, repair, conversion and manipulation" InstallScope="perMachine" Platform="x64" /> <Directory Id="TARGETDIR" Name="SourceDir"> diff --git a/UVtools.WPF/Controls/Tools/ToolLithophaneControl.axaml b/UVtools.WPF/Controls/Tools/ToolLithophaneControl.axaml new file mode 100644 index 0000000..39d0207 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolLithophaneControl.axaml @@ -0,0 +1,246 @@ +<UserControl xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:i="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:Class="UVtools.WPF.Controls.Tools.ToolLithophaneControl"> + <Grid ColumnDefinitions="Auto,10,350"> + <StackPanel Spacing="10"> + <Grid + RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto" + ColumnDefinitions="Auto,10,190,20,Auto,10,190"> + + <TextBlock Grid.Row="0" Grid.Column="0" + VerticalAlignment="Center" + Text="Layer height:"/> + <NumericUpDown Grid.Row="0" Grid.Column="2" + Classes="ValueLabel ValueLabel_mm" + Increment="0.01" + Minimum="0.01" + Maximum="0.30" + FormatString="F3" + Value="{Binding Operation.LayerHeight}"/> + + <TextBlock Grid.Row="0" Grid.Column="4" + VerticalAlignment="Center" + HorizontalAlignment="Right" + Text="Bottom layer count:"/> + <NumericUpDown Grid.Row="0" Grid.Column="6" + Classes="ValueLabel ValueLabel_layers" + Increment="1" + Minimum="1" + Maximum="1000" + Value="{Binding Operation.BottomLayerCount}"/> + + + <TextBlock Grid.Row="2" Grid.Column="0" + VerticalAlignment="Center" + Text="Bottom exposure:"/> + <NumericUpDown Grid.Row="2" Grid.Column="2" + Classes="ValueLabel ValueLabel_s" + Increment="0.5" + Minimum="0.1" + Maximum="200" + FormatString="F2" + Value="{Binding Operation.BottomExposure}"/> + <TextBlock Grid.Row="2" Grid.Column="4" + VerticalAlignment="Center" + HorizontalAlignment="Right" + Text="Normal exposure:"/> + <NumericUpDown Grid.Row="2" Grid.Column="6" + Classes="ValueLabel ValueLabel_s" + Increment="0.5" + Minimum="0.1" + Maximum="200" + FormatString="F2" + Value="{Binding Operation.NormalExposure}"/> + + <TextBlock Grid.Row="4" Grid.Column="0" + VerticalAlignment="Center" + Text="Image:"/> + + <Grid Grid.Row="4" Grid.Column="2" Grid.ColumnSpan="5" + ColumnDefinitions="*,Auto"> + <TextBox Grid.Column="0" + IsReadOnly="True" + VerticalAlignment="Center" + Text="{Binding Operation.FilePath}"/> + <Button Grid.Column="1" + VerticalAlignment="Stretch" + Command="{Binding SelectFile}" + i:Attached.Icon="fas fa-file-import"/> + </Grid> + + <TextBlock Grid.Row="6" Grid.Column="0" + VerticalAlignment="Center" + Text="Rotate:"/> + + <ComboBox Grid.Row="6" Grid.Column="2" + HorizontalAlignment="Stretch" + Items="{Binding Operation.Rotate, Converter={StaticResource EnumToCollectionConverter}, Mode=OneTime}" + SelectedItem="{Binding Operation.Rotate, Converter={StaticResource FromValueDescriptionToEnumConverter}}"/> + + <StackPanel Grid.Row="6" Grid.Column="4" Grid.ColumnSpan="3" + Orientation="Horizontal" Spacing="20"> + <CheckBox VerticalAlignment="Center" + IsChecked="{Binding Operation.Mirror}" + Content="Mirror"/> + + <CheckBox VerticalAlignment="Center" + IsChecked="{Binding Operation.InvertColor}" + Content="Invert color"/> + </StackPanel> + + <TextBlock Grid.Row="8" Grid.Column="0" + VerticalAlignment="Center" + Text="Resize:"/> + + <NumericUpDown Grid.Row="8" Grid.Column="2" + Classes="ValueLabel ValueLabel_percent" + Minimum="1" + Maximum="900" + Increment="1" + FormatString="F2" + Value="{Binding Operation.ResizeFactor}"/> + + <TextBlock Grid.Row="10" Grid.Column="0" + VerticalAlignment="Center" + Text="Brightness gain:"/> + + <NumericUpDown Grid.Row="10" Grid.Column="2" + Classes="ValueLabel ValueLabel_sun" + Minimum="-128" + Maximum="127" + Increment="1" + Value="{Binding Operation.BrightnessGain}"/> + + <CheckBox Grid.Row="10" Grid.Column="4" Grid.ColumnSpan="3" + VerticalAlignment="Center" + IsChecked="{Binding Operation.EnhanceContrast}" + Content="Enhance contrast"/> + + <TextBlock Grid.Row="12" Grid.Column="0" + VerticalAlignment="Center" + Text="Remove noise:"/> + + <NumericUpDown Grid.Row="12" Grid.Column="2" + Classes="ValueLabel ValueLabel_px" + Minimum="0" + Maximum="255" + Increment="1" + Value="{Binding Operation.RemoveNoiseIterations}"/> + + <TextBlock Grid.Row="12" Grid.Column="4" + VerticalAlignment="Center" + HorizontalAlignment="Right" + Text="Gap closing:"/> + + <NumericUpDown Grid.Row="12" Grid.Column="6" + Classes="ValueLabel ValueLabel_px" + Minimum="0" + Maximum="255" + Increment="1" + Value="{Binding Operation.GapClosingIterations}"/> + + <TextBlock Grid.Row="14" Grid.Column="0" + VerticalAlignment="Center" + Text="Gaussian blur:"/> + + <NumericUpDown Grid.Row="14" Grid.Column="2" + Classes="ValueLabel ValueLabel_px" + Minimum="0" + Maximum="255" + Increment="1" + Value="{Binding Operation.GaussianBlur}"/> + + <TextBlock Grid.Row="16" Grid.Column="0" + VerticalAlignment="Center" + Text="Start threshold:"/> + + <NumericUpDown Grid.Row="16" Grid.Column="2" + Classes="ValueLabel ValueLabel_sun" + Minimum="1" + Maximum="255" + Increment="1" + Value="{Binding Operation.StartThresholdRange}"/> + + <TextBlock Grid.Row="16" Grid.Column="4" + VerticalAlignment="Center" + HorizontalAlignment="Right" + Text="End threshold:"/> + + <NumericUpDown Grid.Row="16" Grid.Column="6" + Classes="ValueLabel ValueLabel_sun" + Minimum="1" + Maximum="255" + Increment="1" + Value="{Binding Operation.EndThresholdRange}"/> + + <TextBlock Grid.Row="18" Grid.Column="0" + VerticalAlignment="Center" + Text="Base type:"/> + + <ComboBox Grid.Row="18" Grid.Column="2" Grid.ColumnSpan="5" + HorizontalAlignment="Stretch" + Items="{Binding Operation.BaseType, Converter={StaticResource EnumToCollectionConverter}, Mode=OneTime}" + SelectedItem="{Binding Operation.BaseType, Converter={StaticResource FromValueDescriptionToEnumConverter}}"/> + + <TextBlock Grid.Row="20" Grid.Column="0" + VerticalAlignment="Center" + Text="Base thickness:"/> + + <NumericUpDown Grid.Row="20" Grid.Column="2" + Classes="ValueLabel ValueLabel_mm" + Minimum="0" + Maximum="255" + Increment="1" + FormatString="F2" + Value="{Binding Operation.BaseThickness}"/> + + <TextBlock Grid.Row="20" Grid.Column="4" + HorizontalAlignment="Right" + VerticalAlignment="Center" + Text="Base margin:"/> + + <NumericUpDown Grid.Row="20" Grid.Column="6" + Classes="ValueLabel ValueLabel_px" + Minimum="0" + Maximum="65535" + Increment="1" + Value="{Binding Operation.BaseMargin}"/> + + <TextBlock Grid.Row="22" Grid.Column="0" + VerticalAlignment="Center" + IsEnabled="{Binding !Operation.OneLayerPerThreshold}" + Text="Lithophane height:"/> + + <NumericUpDown Grid.Row="22" Grid.Column="2" + Classes="ValueLabel ValueLabel_px" + Minimum="0" + Maximum="10000" + Increment="1" + IsEnabled="{Binding !Operation.OneLayerPerThreshold}" + Value="{Binding Operation.LithophaneHeight}"/> + + <CheckBox Grid.Row="22" Grid.Column="4" Grid.ColumnSpan="3" + VerticalAlignment="Center" + IsChecked="{Binding Operation.OneLayerPerThreshold}" + Content="One layer per threshold level"/> + + <CheckBox Grid.Row="24" Grid.Column="2" + VerticalAlignment="Center" + IsChecked="{Binding Operation.EnableAntiAliasing}" + Content="Enable Anti-Aliasing"/> + </Grid> + + </StackPanel> + + <StackPanel Grid.Column="2" Orientation="Vertical" Spacing="10"> + <Image Stretch="Uniform" + Source="{Binding PreviewImage}"/> + + <TextBlock Text="{Binding PreviewImage.Size, StringFormat=Size: {0}}" HorizontalAlignment="Center"/> + </StackPanel> + </Grid> +</UserControl> diff --git a/UVtools.WPF/Controls/Tools/ToolLithophaneControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolLithophaneControl.axaml.cs new file mode 100644 index 0000000..f75f7e7 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolLithophaneControl.axaml.cs @@ -0,0 +1,82 @@ +using System.Timers; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using Avalonia.Threading; +using UVtools.Core.Operations; +using UVtools.WPF.Extensions; +using UVtools.WPF.Windows; + +namespace UVtools.WPF.Controls.Tools +{ + public partial class ToolLithophaneControl : ToolControl + { + public OperationLithophane Operation => BaseOperation as OperationLithophane; + + private readonly Timer _timer; + + private Bitmap _previewImage; + public Bitmap PreviewImage + { + get => _previewImage; + set => RaiseAndSetIfChanged(ref _previewImage, value); + } + + public ToolLithophaneControl() + { + BaseOperation = new OperationLithophane(SlicerFile); + if (!ValidateSpawn()) return; + InitializeComponent(); + + _timer = new Timer(20) + { + AutoReset = false + }; + _timer.Elapsed += (sender, e) => Dispatcher.UIThread.InvokeAsync(UpdatePreview); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public override void Callback(ToolWindow.Callbacks callback) + { + if (App.SlicerFile is null) return; + switch (callback) + { + case ToolWindow.Callbacks.Init: + case ToolWindow.Callbacks.Loaded: + Operation.PropertyChanged += (sender, e) => + { + _timer.Stop(); + _timer.Start(); + }; + _timer.Stop(); + _timer.Start(); + break; + } + } + + public void UpdatePreview() + { + using var mat = Operation.GetTargetMat(); + _previewImage?.Dispose(); + PreviewImage = mat?.ToBitmap(); + } + + public async void SelectFile() + { + var dialog = new OpenFileDialog + { + AllowMultiple = false, + Filters = Helpers.ImagesFileFilter, + }; + + var files = await dialog.ShowAsync(ParentWindow); + if (files is null || files.Length == 0) return; + + Operation.FilePath = files[0]; + } + } +} diff --git a/UVtools.WPF/MainWindow.LayerPreview.cs b/UVtools.WPF/MainWindow.LayerPreview.cs index b8bb051..ff0db6f 100644 --- a/UVtools.WPF/MainWindow.LayerPreview.cs +++ b/UVtools.WPF/MainWindow.LayerPreview.cs @@ -24,11 +24,13 @@ using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; using Emgu.CV.Util; +using MessageBox.Avalonia.Enums; using UVtools.AvaloniaControls; using UVtools.Core; using UVtools.Core.EmguCV; using UVtools.Core.Extensions; using UVtools.Core.Layers; +using UVtools.Core.Operations; using UVtools.Core.PixelEditor; using UVtools.WPF.Extensions; using UVtools.WPF.Structures; @@ -798,6 +800,18 @@ public partial class MainWindow ActualLayer = SliderMaximumValue; } + public void GoUpLayers(uint layers) + { + if (!IsFileLoaded) return; + ActualLayer = Math.Min(SlicerFile.LastLayerIndex, ActualLayer + layers); + } + + public void GoDownLayers(uint layers) + { + if (!IsFileLoaded) return; + ActualLayer = (uint)Math.Max(0, (int)ActualLayer - layers); + } + public void GoMassLayer(string which) { if (!IsFileLoaded) return; @@ -1884,41 +1898,141 @@ public partial class MainWindow private void LayerImageBox_KeyDown(object? sender, KeyEventArgs e) { - if (e.Key == Key.Up) + switch (e.Key) { - GoNextLayer(); - e.Handled = true; - return; - } - - if (e.Key == Key.Down) - { - GoPreviousLayer(); - e.Handled = true; - return; + case Key.Up: + GoNextLayer(); + e.Handled = true; + return; + case Key.Down: + GoPreviousLayer(); + e.Handled = true; + return; + case Key.PageUp: + GoUpLayers(10); + e.Handled = true; + return; + case Key.PageDown: + GoDownLayers(10); + e.Handled = true; + return; } } - private void LayerImageBox_KeyUp(object? sender, KeyEventArgs e) + private async void LayerImageBox_KeyUp(object? sender, KeyEventArgs e) { - if (e.Key == Key.Escape) + switch (e.Key) { - if (e.KeyModifiers == KeyModifiers.Shift) + case Key.Escape: { - ClearROI(); + if (e.KeyModifiers == KeyModifiers.Shift) + { + ClearROI(); + } + /*else if(e.KeyModifiers == KeyModifiers.Alt) + { + ClearMask(); + }*/ + else + { + ClearROIAndMask(); + } + e.Handled = true; + return; } - /*else if(e.KeyModifiers == KeyModifiers.Alt) + case Key.Insert: { - ClearMask(); - }*/ - else + if (e.KeyModifiers == KeyModifiers.Control) + { + if (await this.MessageBoxQuestion($"Are you sure you want to clone the current layer {_actualLayer}?", + "Clone the current layer?") != ButtonResult.Yes) return; + + var operationLayerClone = new OperationLayerClone(SlicerFile); + operationLayerClone.SelectCurrentLayer(_actualLayer); + await RunOperation(operationLayerClone); + + e.Handled = true; + return; + } + + if (ROI == Rectangle.Empty && _maskPoints.Count == 0) return; + var operation = new OperationPixelArithmetic(SlicerFile) + { + Operator = OperationPixelArithmetic.PixelArithmeticOperators.KeepRegion, + ROI = ROI, + MaskPoints = _maskPoints.ToArray() + }; + + string layerRange; + if (e.KeyModifiers == KeyModifiers.Alt) + { + operation.SelectAllLayers(); + layerRange = $"within all {SlicerFile.LayerCount} layers"; + } + else + { + operation.SelectCurrentLayer(ActualLayer); + layerRange = $"in the current {ActualLayer} layer"; + } + + if (await this.MessageBoxQuestion($"Are you sure you want to keep only the selected region/mask(s) {layerRange}?", + "Keep only selected region/mask(s)?") != ButtonResult.Yes) return; + await RunOperation(operation); + e.Handled = true; + return; + } + case Key.Delete: { - ClearROIAndMask(); + if (e.KeyModifiers == KeyModifiers.Control) + { + if (await this.MessageBoxQuestion($"Are you sure you want to remove the current layer {_actualLayer}?", + "Remove the current layer?") != ButtonResult.Yes) return; + + var operationLayerRemove = new OperationLayerRemove(SlicerFile); + operationLayerRemove.SelectCurrentLayer(_actualLayer); + await RunOperation(operationLayerRemove); + + e.Handled = true; + return; + } + + if (ROI == Rectangle.Empty && _maskPoints.Count == 0) return; + var operation = new OperationPixelArithmetic(SlicerFile) + { + Operator = OperationPixelArithmetic.PixelArithmeticOperators.DiscardRegion, + ROI = ROI, + MaskPoints = _maskPoints.ToArray() + }; + + string layerRange; + if (e.KeyModifiers == KeyModifiers.Alt) + { + operation.SelectAllLayers(); + layerRange = $"within all {SlicerFile.LayerCount} layers"; + } + else + { + operation.SelectCurrentLayer(ActualLayer); + layerRange = $"in the current {ActualLayer} layer"; + } + + if (await this.MessageBoxQuestion($"Are you sure you want to discard the selected region/mask(s) {layerRange}?", + "Discard selected region/mask(s)?") != ButtonResult.Yes) return; + await RunOperation(operation); + e.Handled = true; + return; } - e.Handled = true; - return; + case Key.Home: + GoFirstLayer(); + e.Handled = true; + return; + case Key.End: + GoLastLayer(); + e.Handled = true; + return; } + if ((e.KeyModifiers & KeyModifiers.Control) != 0) { if (e.Key is Key.LeftShift or Key.RightShift || (e.KeyModifiers & KeyModifiers.Shift) != 0) // Ctrl + Shift @@ -1968,6 +2082,7 @@ public partial class MainWindow if (e.Key == Key.B) { SelectModelVolumeRoi(); + e.Handled = true; return; } diff --git a/UVtools.WPF/MainWindow.axaml.cs b/UVtools.WPF/MainWindow.axaml.cs index 7165d44..efdda1d 100644 --- a/UVtools.WPF/MainWindow.axaml.cs +++ b/UVtools.WPF/MainWindow.axaml.cs @@ -168,6 +168,10 @@ public partial class MainWindow : WindowEx }, new() { + Tag = new OperationLithophane(), + }, + new() + { Tag = new OperationScripting(), }, new() diff --git a/UVtools.WPF/Structures/OperationProfiles.cs b/UVtools.WPF/Structures/OperationProfiles.cs index 6691e49..774abcb 100644 --- a/UVtools.WPF/Structures/OperationProfiles.cs +++ b/UVtools.WPF/Structures/OperationProfiles.cs @@ -47,6 +47,7 @@ public class OperationProfiles //: IList<Operation> [XmlElement(typeof(OperationRaiseOnPrintFinish))] [XmlElement(typeof(OperationChangeResolution))] [XmlElement(typeof(OperationTimelapse))] + [XmlElement(typeof(OperationLithophane))] [XmlElement(typeof(OperationScripting))] [XmlElement(typeof(OperationLayerExportGif))] diff --git a/UVtools.WPF/UVtools.WPF.csproj b/UVtools.WPF/UVtools.WPF.csproj index b688f5d..77dd831 100644 --- a/UVtools.WPF/UVtools.WPF.csproj +++ b/UVtools.WPF/UVtools.WPF.csproj @@ -12,7 +12,7 @@ <PackageLicenseFile>LICENSE</PackageLicenseFile> <RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl> <RepositoryType>Git</RepositoryType> - <Version>3.2.2</Version> + <Version>3.3.0</Version> <Platforms>AnyCPU;x64</Platforms> <PackageIcon>UVtools.png</PackageIcon> <PackageReadmeFile>README.md</PackageReadmeFile> |