/// <summary> /// Loads and processes an image from the filesystem. /// </summary> /// <param name="ImagePath">The path on the filesystem of the image to load</param> private void LoadImage(string ImagePath) { // Clear the previous image if there was one Image preview = this.FindControl <Image>("Preview"); preview.Source = null; preview.InvalidateVisual(); // Process and draw the new image ImageToEtch = new EtchableImage(ImagePath); Slider luminanceSlider = this.FindControl <Slider>("LuminanceSlider"); ImageToEtch.ProcessImage(1.0 - luminanceSlider.Value); preview.Source = ImageToEtch.Bitmap; preview.InvalidateVisual(); // Set the image dimension labels TextBlock imageWidthLabel = this.FindControl <TextBlock>("ImageWidthLabel"); TextBlock imageHeightLabel = this.FindControl <TextBlock>("ImageHeightLabel"); imageWidthLabel.Text = $"{ImageToEtch.Width} px"; imageHeightLabel.Text = $"{ImageToEtch.Height} px"; // Route it RecalculateRoute(); }
/// <summary> /// Loads a picture from a file when it's dropped onto the preview image control. /// </summary> /// <param name="sender">Not used</param> /// <param name="e">The drop event arguments containing the contents that were dropped</param> private async void Drop(object sender, DragEventArgs e) { IEnumerable <string> files = e.Data.GetFileNames(); if (files.Count() > 0) { string imagePath = files.ElementAt(0); try { Button recalculateButton = this.FindControl <Button>("RecalculateButton"); Button exportButton = this.FindControl <Button>("ExportButton"); recalculateButton.IsEnabled = false; exportButton.IsEnabled = false; ImageToEtch = null; Route = null; Title = $"OpenEtch v{VersionInfo.Version}"; LoadImage(imagePath); recalculateButton.IsEnabled = true; exportButton.IsEnabled = true; OriginalFilename = System.IO.Path.GetFileName(imagePath); Title = $"OpenEtch v{VersionInfo.Version} - {OriginalFilename}"; } catch (Exception ex) { ImageToEtch = null; Route = null; await MessageBox.Show(this, $"Error loading image: {ex.Message}", "Error loading image"); return; } } }
/// <summary> /// Loads an image from the filesystem. /// </summary> /// <param name="sender">Not used</param> /// <param name="e">Not used</param> public async void LoadImageButton_Click(object sender, RoutedEventArgs e) { Button recalculateButton = this.FindControl <Button>("RecalculateButton"); Button exportButton = this.FindControl <Button>("ExportButton"); recalculateButton.IsEnabled = false; exportButton.IsEnabled = false; ImageToEtch = null; Route = null; Title = $"OpenEtch v{VersionInfo.Version}"; // Give the user a new dialog for choosing the image file OpenFileDialog dialog = new OpenFileDialog() { AllowMultiple = false, Title = "Select Image", Filters = { new FileDialogFilter { Name = "Images", Extensions ={ "bmp", "png", "jpg", "jpeg" } }, new FileDialogFilter { Name = "All Files", Extensions ={ "*" } } } }; // Get the selection, if they selected something string[] selection = await dialog.ShowAsync(this); if (selection.Length != 1) { return; } // Try to load and process the image string imagePath = selection[0]; try { LoadImage(imagePath); RecalculateRoute(); recalculateButton.IsEnabled = true; exportButton.IsEnabled = true; OriginalFilename = System.IO.Path.GetFileName(imagePath); Title = $"OpenEtch v{VersionInfo.Version} - {OriginalFilename}"; } catch (Exception ex) { ImageToEtch = null; Route = null; await MessageBox.Show(this, $"Error loading image: {ex.Message}", "Error loading image"); return; } }
/// <summary> /// Creates an etching route to etch the outlines of all of the bodies in the image, /// thus providing a stencil. /// </summary> /// <param name="Image">The image to etch</param> /// <returns>A route for etching a stencil of the provided image</returns> public Route Route(EtchableImage Image) { Point origin = new Point(0, 0); // Handle the pre-etch trace first Point topRight = new Point(Image.Width - 1, 0); Point bottomRight = new Point(Image.Width - 1, Image.Height - 1); Point bottomLeft = new Point(0, Image.Height - 1); Path preEtchTrace = new Path( new List <Point>() { origin, topRight, bottomRight, bottomLeft, origin }); // Handle the outline etching List <Body> bodies = FindBodies(Image); List <Path> outlines = new List <Path>(); foreach (Body body in bodies) { outlines.Add(body.Outline); } // Done! Route route = new Route(Config, preEtchTrace, outlines); return(route); }
/// <summary> /// Finds all of the bodies in the provided image (areas of connected black pixels). /// </summary> /// <param name="Image">The image to find the bodies in</param> /// <returns>A collection of bodies in the image</returns> public List <Body> FindBodies(EtchableImage Image) { List <Body> bodies = new List <Body>(); bool[,] visitedPixels = new bool[Image.Width, Image.Height]; using (ILockedFramebuffer buffer = Image.Bitmap.Lock()) { IntPtr address = buffer.Address; for (int y = 0; y < Image.Height; y++) { IntPtr rowOffsetAddress = address + (y * Image.Width * 4); for (int x = 0; x < Image.Width; x++) { // Ignore pixels we've already looked at if (visitedPixels[x, y]) { continue; } // Set the flag for this pixel because now we've looked at it visitedPixels[x, y] = true; // Ignore white pixels IntPtr pixelAddress = rowOffsetAddress + (x * 4); // Get the blue channel since that isn't used for path drawing, // so it will always have the correct pixel value of 0 or 255 byte pixelValue = Marshal.ReadByte(pixelAddress); if (pixelValue == 0xFF) { continue; } // If we get here, we have a new body! Body body = ProcessBody(buffer, x, y, visitedPixels); bodies.Add(body); } } } return(bodies); }
/// <summary> /// Creates a route for raster-etching the provided image. /// </summary> /// <param name="Image">The image to etch</param> /// <returns>A route for etching the provided image</returns> public Route Route(EtchableImage Image) { Point origin = new Point(0, 0); // Handle the pre-etch trace first Point topRight = new Point(Image.Width - 1, 0); Point bottomRight = new Point(Image.Width - 1, Image.Height - 1); Point bottomLeft = new Point(0, Image.Height - 1); Path preEtchTrace = new Path( new List <Point>() { origin, topRight, bottomRight, bottomLeft, origin }); // Handle the image etching List <Path> etchMoves = new List <Path>(); Point lastPoint = origin; List <Line> etchLines = GetRasterEtchLines(Image); foreach (Line line in etchLines) { // Check if the start of the line or the end is closest to the last point Point lineStart = new Point(line.Start, line.YIndex); Point lineEnd = new Point(line.End, line.YIndex); double startDistance = lastPoint.GetDistance(lineStart); double endDistance = lastPoint.GetDistance(lineEnd); // Go left-to-right if (startDistance <= endDistance) { for (int i = 0; i < line.Segments.Count; i++) { EtchSegment segment = line.Segments[i]; Point etchStart = new Point(segment.Start, line.YIndex); Point etchEnd = new Point(segment.End, line.YIndex); // Run the etch Path etch = new Path(new List <Point> { etchStart, etchEnd }); etchMoves.Add(etch); // Set the last known point to the end of this etch lastPoint = etchEnd; } } // Go right-to-left else { for (int i = line.Segments.Count - 1; i >= 0; i--) { EtchSegment segment = line.Segments[i]; Point etchStart = new Point(segment.End, line.YIndex); Point etchEnd = new Point(segment.Start, line.YIndex); // Run the etch Path etch = new Path(new List <Point> { etchStart, etchEnd }); etchMoves.Add(etch); // Set the last known point to the end of this etch lastPoint = etchEnd; } } } // Done! Route route = new Route(Config, preEtchTrace, etchMoves); return(route); }
/// <summary> /// Breaks the provided image into horizontal raster etch segments. /// </summary> /// <param name="Image">The image to get the raster etch lines for</param> /// <returns>A collection of horizontal lines (each of which can have multiple segments) /// necessary to etch the image in raster mode.</returns> private List <Line> GetRasterEtchLines(EtchableImage Image) { List <Line> lines = new List <Line>(); using (ILockedFramebuffer bitmapBuffer = Image.Bitmap.Lock()) { IntPtr bitmapAddress = bitmapBuffer.Address; for (int y = 0; y < Image.Height; y++) { Line line = new Line(y); bool isInEtchSegment = false; int etchStart = 0; for (int x = 0; x < Image.Width; x++) { // Get the blue channel - since this is a black and white image, the channel doesn't really matter byte pixelValue = Marshal.ReadByte(bitmapAddress); // This is a white pixel if (pixelValue > 127) { if (isInEtchSegment) { // Create a new etch segment from the recorded start to the previous pixel EtchSegment segment = new EtchSegment(etchStart, x - 1); line.Segments.Add(segment); isInEtchSegment = false; } } // This is a black pixel else { if (!isInEtchSegment) { // Start a new etch segment isInEtchSegment = true; etchStart = x; } // Check if we're at the last pixel in the row but still in an etch segment else if (x == Image.Width - 1) { EtchSegment segment = new EtchSegment(etchStart, x); line.Segments.Add(segment); isInEtchSegment = false; } } // Move to the next pixel in the bitmap buffer bitmapAddress += 4; } // Ignore lines with no etch segments if (line.Segments.Count > 0) { lines.Add(line); } } } return(lines); }