chmodseven

FloodFill function

Jul 12th, 2020
135
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 17.86 KB | None | 0 0
  1. using System.Collections.Generic;
  2. using UnityEngine;
  3.  
  4. #region MIT LICENSE
  5.  
  6. /*
  7.     License: The MIT License (MIT)
  8.     Copyright (C) 2020 Shannon Rowe
  9.    
  10.     Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
  11.     documentation files (the "Software"), to deal in the Software without restriction, including without limitation
  12.     the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
  13.     and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
  14.  
  15.     The above copyright notice and this permission notice shall be included in all copies or substantial portions of
  16.     the Software.
  17.    
  18.     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
  19.     TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  20.     THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
  21.     CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  22.     DEALINGS IN THE SOFTWARE.
  23. */
  24.  
  25. #endregion
  26.  
  27. // This class being static means it can be called from anywhere and doesn't need to be instantiated
  28. // Note that this is a pure C# class, not a MonoBehaviour
  29. public static class ImageFunctions
  30. {
  31.     /// <summary>
  32.     ///     <para>
  33.     ///         To use this, pass it a texture and the from and to colors, and a starting position inside the texture
  34.     ///         where you want the fill to start from, and it will return a filled texture.
  35.     ///         Note that alpha transparency values are ignored and the image assumed to be opaque.
  36.     ///     </para>
  37.     ///     <para>
  38.     ///         For example, say you have a texture that is all white except for an outline pattern that's some other color,
  39.     ///         for example a black outline circle but it's still white inside the outline, and you want to fill that inside
  40.     ///         with red, you would pass fillFromColor as white, fillToColor as red, and startPosition as some point inside
  41.     ///         the circle based on either maths or a mouse click or something.
  42.     ///     </para>
  43.     ///     <para>
  44.     ///         The colorTolerance parameter determines how far to either side of the color's RGB values to process,
  45.     ///         which can be useful when you want to fill an image with a gradient of colors in an approximate range.
  46.     ///         With the default of 0 it will only match to exact colors, but if you set it to say 10 for example,
  47.     ///         and your fillFromColor is something like Red 127/Green 66/Blue 32 then the fill will be applied to
  48.     ///         any color combination in the range Red 117-137/Green 56-76/Blue 22-42.  It will also preserve the
  49.     ///         gradient, so your fillToColor will also be adjusted by up to -10 and +10 depending on how far it is.
  50.     ///         If you want to just have a single to color replace all colors in the from range, however, just set
  51.     ///         preserveGradientsWithTolerance to false
  52.     ///     </para>
  53.     /// </summary>
  54.     public static Texture2D FloodFill (
  55.         Texture2D sourceImage,
  56.         Vector2 startPosition,
  57.         Color32 fillFromColor,
  58.         Color32 fillToColor,
  59.         int colorTolerance = 0,
  60.         bool preserveGradientsWithTolerance = true)
  61.     {
  62.         // Cache these size properties in local variables for a slight performance boost, as it costs
  63.         // less to read from a local variable than to get the property more than once
  64.         int sourceImageWidth = sourceImage.width;
  65.         int sourceImageHeight = sourceImage.height;
  66.  
  67.         // Now we read in the pixels as colors into an array for processing
  68.         Color32 [] imageColors = sourceImage.GetPixels32 ();
  69.  
  70.         // Although the image is 2D, the array we read the pixels into is single dimension.
  71.         // We need a way to take the horizontal X position of the point and the vertical Y position of the point
  72.         // and find the one-dimensional index that corresponds to that pixel in the array.
  73.         // In Unity, note also that vertical points usually start at the bottom, so 0,0 would be the bottom left
  74.         // Imagine in the image that each horizontal line has "width" number of pixels, and there are "height"
  75.         // number of these lines in the image, going from the bottom to the the top.
  76.         // So if we multiply the Y value by the width, we get the starting index of the first X pixel on that
  77.         // vertical line.  Say our image is 100*100.  For Y=0, the first pixel is 0, for Y=1 the first pixel is
  78.         // 100, then Y=2 is 200, and so on.  Then we *add* the X position to that to get the final index,
  79.         // so for X=0,Y=2 we get 200, for X=1,Y=2 we get 201, all the way over to X=99,Y=2 is 299, and then it
  80.         // moves on to the next line, so X=0,Y=3 is 300 for the index to that point in the array
  81.         int startIndex = (int) startPosition.y * sourceImageWidth + (int) startPosition.x;
  82.  
  83.         // We are optionally going to adjust these values, so cache them here
  84.         Color32 minColorRange = fillFromColor;
  85.         Color32 maxColorRange = fillFromColor;
  86.  
  87.         // When the color tolerance is set to a non zero value, that means we want to look to either side of
  88.         // the from color by that amount.  In Unity, the Color32 struct uses byte values from 0-255 for
  89.         // the Red, Green, Blue, and Alpha values (we are ignoring alpha in this function).
  90.         // So all we do here is change the min (by subtracting tolerance) and max (by adding tolerance)
  91.         // values, and then clamping them with the math functions to make sure they stay between 0 and 255
  92.         if (colorTolerance != 0)
  93.         {
  94.             minColorRange.r = (byte) Mathf.Max (fillFromColor.r - colorTolerance, 0);
  95.             minColorRange.g = (byte) Mathf.Max (fillFromColor.g - colorTolerance, 0);
  96.             minColorRange.b = (byte) Mathf.Max (fillFromColor.b - colorTolerance, 0);
  97.             maxColorRange.r = (byte) Mathf.Min (fillFromColor.r + colorTolerance, 255);
  98.             maxColorRange.g = (byte) Mathf.Min (fillFromColor.g + colorTolerance, 255);
  99.             maxColorRange.b = (byte) Mathf.Min (fillFromColor.b + colorTolerance, 255);
  100.         }
  101.  
  102.         // This calls a second function to make sure the color that sits on the pixel where the start position was
  103.         // mapped to in the array is actually within the color range we want.
  104.         // So if your from color is white but the pixel under your start position is actually black, it will return
  105.         // an error.  If you have a tolerance value and the pixel is nearly white and within the tolerance, it will pass
  106.         if (!IsColorWithinTolerance (imageColors [startIndex], minColorRange, maxColorRange))
  107.         {
  108.             Debug.Log ("Start location index " + startIndex + " does not contain the old color to be replaced; " +
  109.                        "expected range " + minColorRange + " to " + maxColorRange + " but got " + imageColors [startIndex]);
  110.             return sourceImage;
  111.         }
  112.  
  113.         // Now we get to the meat of the processing.
  114.         // We want to start from the start position and then progressively look up, down, left, and right by one pixel
  115.         // If that pixel is within our from color range, and has not been previously processed, then we want to add
  116.         // it to our set of pixels to be processed.  We don't need to consider diagonals because, for example, if the
  117.         // pixel above our current pixel is valid, then when that pixel gets assessed, the pixel to its left will be
  118.         // checked, and that pixel corresponds to the diagonally up-and-left pixel of the one we are currently on.
  119.         // In that way, the 4-way flood fill will check every pixel until it finds a border that is no longer valid
  120.         // within the from color range, and when there are no pixels left to check, then it has completed the fill.
  121.  
  122.         // The best data structure to handle this recording of which pixel indexes are flagged to be checked is a Queue.
  123.         // Queues in C# are first-in-first-out (or FIFO) which means that if I have a Queue of int and I pass in
  124.         // first 1, then 3, then 2 (adding to a queue is called "enqueueing") and then dequeue three times, I will
  125.         // get them back in the order 1, then 3, then 2.
  126.         // This differs to a Stack which is last-in-first-out (LIFO), where "pushing" 1, then 3, then 2 would get
  127.         // "popped" back as 2, then 3, then 1.
  128.         Queue<int> floodQueue = new Queue<int> ();
  129.  
  130.         // For keeping track of which indexes we have already processed (to avoid doing extra work) a HashSet is the
  131.         // best data structure.  This is much faster for looking up values than say a List, as it doesn't need to
  132.         // store the order of the values.  It also doesn't care about duplicate values, although that isn't important here
  133.         HashSet<int> floodDone = new HashSet<int> ();
  134.  
  135.         // To kick off the loop, first we add the start index we derived earlier that corresponds to the start position.
  136.         // We add this to the queue as the first index to be flagged for checking, and also the done set since we
  137.         // already know from before that it is within the color range
  138.         floodQueue.Enqueue (startIndex);
  139.         floodDone.Add (startIndex);
  140.  
  141.         // This while loop will continue until nothing left to check is in the queue.
  142.         // When we first come here, the start index enqueued above is the only item present, but as we keep looking
  143.         // to the sides, then the queue will grow, and will only start to shrink once we begin hitting borders
  144.         // where no new indexes to check are getting added
  145.         while (floodQueue.Count > 0)
  146.         {
  147.             // Dequeue takes the next queued index and removes it from the queue
  148.             int currentIndex = floodQueue.Dequeue ();
  149.  
  150.             // This is the reverse of the formula earlier on where we calculated the index from the point;
  151.             // in these two lines, we are calculating the X,Y position values of the pixel that correspond
  152.             // to the integer index.  So we divide by width this time to get back to the Y vertical line,
  153.             // and we also use a math floor operation (removes the decimal, so say 1.63 becomes 1), and then
  154.             // calculate the X value in a similar way by removing all the vertical lines worth of indexes from
  155.             // the value until only the X position remains
  156.             int currentY = Mathf.FloorToInt ((float) currentIndex / sourceImageHeight);
  157.             int currentX = currentIndex - currentY * sourceImageHeight;
  158.  
  159.             if (colorTolerance == 0 || !preserveGradientsWithTolerance)
  160.             {
  161.                 // If tolerance is not being used then we can just set the to color straight away
  162.                 imageColors [currentIndex] = fillToColor;
  163.             }
  164.             else
  165.             {
  166.                 // Otherwise, we want to apply the same gradient difference that was present in the from value
  167.                 // For example with tolerance of 10 again, if our from was Red 127/Green 66/Blue 32 and the to
  168.                 // color was Red 88/Green 88/Blue 88, and the color of this pixel happens to be Red 127/Green 60/Blue 32
  169.                 // (a Green difference of -6, so within the tolerance of +-10) then our new color will be set as
  170.                 // a gradient-adjusted value of Red 88/Green 82/Blue 88.
  171.                 // Alpha is just set to the same as the original, and not adjusted for gradient, although you could
  172.                 // put that in if you wanted
  173.                 imageColors [currentIndex].r = (byte) Mathf.Clamp (fillToColor.r + (imageColors [currentIndex].r - fillFromColor.r), 0, 255);
  174.                 imageColors [currentIndex].g = (byte) Mathf.Clamp (fillToColor.g + (imageColors [currentIndex].g - fillFromColor.g), 0, 255);
  175.                 imageColors [currentIndex].b = (byte) Mathf.Clamp (fillToColor.b + (imageColors [currentIndex].b - fillFromColor.b), 0, 255);
  176.                 imageColors [currentIndex].a = fillToColor.a;
  177.             }
  178.  
  179.             // Now that the current pixel is processed, we want to check the neighbours on all four sides
  180.  
  181.             // Up - only process if we are on Y=1 or higher, because when Y=0, there is no valid Y-1 to use
  182.             // Remember also the weird convention that Unity starts at 0,0 in bottom left, so technically
  183.             // this is more like a downwards in terms of eyeball visualising of the direction, but that doesn't matter
  184.             if (currentY > 0)
  185.             {
  186.                 // To calculate the correct index of the pixel above this one, we just add the unchanged X
  187.                 // to the start of the line at Y-1, using the same formula as before
  188.                 int testIndex = (currentY - 1) * sourceImageWidth + currentX;
  189.  
  190.                 // If that neighbouring pixel has been previously processed, then skip any further work
  191.                 if (!floodDone.Contains (testIndex))
  192.                 {
  193.                     // Otherwise check it using the shared function below to see if the new pixel
  194.                     // is within the from tolerance range
  195.                     if (IsColorWithinTolerance (imageColors [testIndex], minColorRange, maxColorRange))
  196.                     {
  197.                         // And if it is, enqueue it for processing in some later iteration of the while loop
  198.                         floodQueue.Enqueue (testIndex);
  199.                     }
  200.  
  201.                     // Whether or not the pixel was flagged for processing, we are done with checking it, so
  202.                     // add it to the set of indexes not to be processed again
  203.                     floodDone.Add (testIndex);
  204.                 }
  205.             }
  206.  
  207.             // Down - this is the same as before, although this time we only process if we are at least 1
  208.             // away from the last Y row of the image, i.e. height minus 1, since last Y + 1 would not be valid
  209.             if (currentY < sourceImageHeight - 1)
  210.             {
  211.                 // All of this is as before, only this time we add a row
  212.                 int testIndex = (currentY + 1) * sourceImageWidth + currentX;
  213.                 if (!floodDone.Contains (testIndex))
  214.                 {
  215.                     if (IsColorWithinTolerance (imageColors [testIndex], minColorRange, maxColorRange))
  216.                     {
  217.                         floodQueue.Enqueue (testIndex);
  218.                     }
  219.  
  220.                     floodDone.Add (testIndex);
  221.                 }
  222.             }
  223.  
  224.             // Left - similarly to Up, we only check from X=1 or greater, because X=0 - 1 would be invalid
  225.             if (currentX > 0)
  226.             {
  227.                 // This time the formula is taking the unchanged Y and adding X - 1 to that to get
  228.                 // the index of the pixel to the left
  229.                 int testIndex = currentY * sourceImageWidth + (currentX - 1);
  230.                 if (!floodDone.Contains (testIndex))
  231.                 {
  232.                     if (IsColorWithinTolerance (imageColors [testIndex], minColorRange, maxColorRange))
  233.                     {
  234.                         floodQueue.Enqueue (testIndex);
  235.                     }
  236.  
  237.                     floodDone.Add (testIndex);
  238.                 }
  239.             }
  240.  
  241.             // Right - again, similar to Down, we check to make sure it's not the last X value on the
  242.             // current Y line, as X=last + 1 would be invalid
  243.             if (currentX < sourceImageWidth - 1)
  244.             {
  245.                 int testIndex = currentY * sourceImageWidth + currentX + 1;
  246.                 if (!floodDone.Contains (testIndex))
  247.                 {
  248.                     if (IsColorWithinTolerance (imageColors [testIndex], minColorRange, maxColorRange))
  249.                     {
  250.                         floodQueue.Enqueue (testIndex);
  251.                     }
  252.  
  253.                     floodDone.Add (testIndex);
  254.                 }
  255.             }
  256.         }
  257.  
  258.         // Finally, we have exited the while loop as there are no more indexes to check, so we know
  259.         // we have successfully altered all of the color values in the array that matched the flood fill.
  260.         // The last thing to do is copy these back from the array to the actual pixels in the resulting image.
  261.         // First make a copy of the texture with the same size and image format as the original, with mip-maps turned off
  262.         // as those can cause the fill to have weird artifacts, and force the format to RGBA32 so we get
  263.         // the right copied alpha values as well.
  264.         // This first line creates a blank texture of the right settings
  265.         Texture2D result = new Texture2D (sourceImageWidth, sourceImageHeight, TextureFormat.RGBA32, false);
  266.  
  267.         // And then this line copies the actual changed pixel data from the color array to the new image
  268.         result.SetPixels32 (imageColors);
  269.  
  270.         // When you "set" something in a texture, you need to do an Apply to "save" the changes
  271.         result.Apply ();
  272.  
  273.         // Finally, we return the altered texture!
  274.         return result;
  275.     }
  276.  
  277.     private static bool IsColorWithinTolerance (Color32 checkColor, Color32 minColor, Color32 maxColor)
  278.     {
  279.         // All this does is check to make sure each channel (Red, Green, and Blue) is greater than or equal
  280.         // to the min range value derived earlier (or is an exact match, if tolerance was 0) and less than or
  281.         // equal to the max range value.  If all channels fit within those range values, it will be true
  282.         return checkColor.r >= minColor.r &&
  283.                checkColor.g >= minColor.g &&
  284.                checkColor.b >= minColor.b &&
  285.                checkColor.r <= maxColor.r &&
  286.                checkColor.g <= maxColor.g &&
  287.                checkColor.b <= maxColor.b;
  288.     }
  289. }
Add Comment
Please, Sign In to add comment