- **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
This commit is contained in:
Tiago Conceição
2022-04-11 00:08:15 +01:00
parent 84aa3d47e3
commit 99e3e33ac6
29 changed files with 1204 additions and 165 deletions
+17
View File
@@ -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:**
+14 -22
View File
@@ -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
+2 -2
View File
@@ -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;
+41 -1
View File
@@ -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
}
+37 -2
View File
@@ -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][];
@@ -258,6 +275,24 @@ public class EmguContours : IReadOnlyList<EmguContour>, IDisposable
return result;
}
/// <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>
@@ -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);
+16 -9
View File
@@ -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)
};
}
}
}*/
+31 -8
View File
@@ -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
+19 -80
View File
@@ -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 =>
// Write layer largest contour area (mm^2 * 1000)
outputFile.Seek(layerAreaPosition, SeekOrigin.Begin);
foreach (var area in layerLargestContourArea)
{
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++)
{
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();
+11
View File
@@ -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
+2 -2
View File
@@ -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)
@@ -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
@@ -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);
}
}
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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)
@@ -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)
@@ -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);
@@ -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
}
@@ -866,7 +866,7 @@ public class OperationPixelArithmetic : Operation
default:
throw new ArgumentOutOfRangeException(nameof(_ignoreAreaOperator));
}
ApplyMask(originalRoi, target);
ApplyMask(original, mat);
SlicerFile[layerIndex].LayerMat = mat;
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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">
@@ -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>
@@ -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];
}
}
}
+138 -23
View File
@@ -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
{
ClearROIAndMask();
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;
}
e.Handled = true;
return;
case Key.Delete:
{
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;
}
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;
}
+4
View File
@@ -167,6 +167,10 @@ public partial class MainWindow : WindowEx
Tag = new OperationTimelapse(),
},
new()
{
Tag = new OperationLithophane(),
},
new()
{
Tag = new OperationScripting(),
},
@@ -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))]
+1 -1
View File
@@ -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>