- Home /
Coding my own auto-slicer, getting "islands" of pixels at runtime?
I want to take a sprite sheet image like this:
And determine through code at runtime where there are non-connected shapes (after getting information from the texture2d with getpixels32), like in this case there would be 4 separate shapes for those three dudes and the sword thing (lets call em "islands" cuz it sounds cool)... and in turn I want to find the bounds of these islands of pixels (a 2d box which surrounds their extents) so I can determine where to slice the sprites up at. After doing that magic I could have auto sliced sprite sheets! and without having unity pro that would sure be helpful.
Could this be done somehow by determining the alpha areas that completely separate the "islands" of pixels that represent each of the four objects? I keep thinking of ways to do it, but I am drawing a blank here - I mean I know I can check the alpha of each pixel, and by that I know where there are no pixels... but how can I check where there are some kind of connected shapes, then decide the borders around them in a square?
Answer by Seregon · Nov 16, 2013 at 09:42 PM
A relatively straight forward way of doing this (if not necessarily the most efficient), is to flood each shape to see if they're connected. This is a lot easier to explain with code:
EDIT - This is now code for a complete component, see comments below for further explanation. For the original code, skip to the CutSpriteSheet function below.
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using System;
using System.IO;
public class SpriteSheetCutter : MonoBehaviour
{
/// <summary>
/// An sprite sheet from which to cut
/// </summary>
public Texture2D inputTex;
/// <summary>
/// The background color to ignore
/// </summary>
public Color backGroundColor = Color.clear;
// Use this for initialization
void Start()
{
List<int[]> Islands = CutSpriteSheet(inputTex);
Texture2D DisplayTex = HighlightIslands(inputTex, Islands);
// Render the sprite sheet with highlighting:
MeshRenderer Mr = GetComponent<MeshRenderer>();
Mr.material.mainTexture = DisplayTex;
DisplayTex.wrapMode = TextureWrapMode.Clamp;
DisplayTex.Apply(true);
// Extract sprites as individual textures:
Texture2D[] Sprites = GetSprites(inputTex, Islands);
Directory.CreateDirectory(Application.dataPath + "/sprites/");
for (int i = 0; i < Sprites.Length; i++)
{
byte[] bytes = Sprites[i].EncodeToPNG();
FileStream file = File.Open(Application.dataPath + "/sprites/sprite" + i + ".png", FileMode.Create);
BinaryWriter bw = new BinaryWriter(file);
bw.Write(bytes);
file.Close();
}
}
/// <summary>
/// Extract sprites from sheet as individual textures
/// </summary>
Texture2D[] GetSprites(Texture2D input, List<int[]> Islands)
{
Texture2D[] output = new Texture2D[Islands.Count];
int i = 0;
foreach (int[] coords in Islands)
{
int width = coords[2] - coords[0];
int height = coords[3] - coords[1];
Texture2D sprite = new Texture2D(width, height);
Color[] pix = input.GetPixels(coords[0], coords[1], width, height);
sprite.SetPixels(pix);
sprite.Apply(true);
output[i++] = sprite;
}
return output;
}
/// <summary>
/// Highlight each sprite with a red border
/// </summary>
Texture2D HighlightIslands(Texture2D Sprites, List<int[]> Islands)
{
Color[] pix = Sprites.GetPixels();
int width = Sprites.width;
int height = Sprites.height;
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
{
foreach (int[] coords in Islands)
{
if ((x == coords[0] || x == coords[2]) &&
(y >= coords[1] && y <= coords[3]))
pix[x + width * y] = Color.red;
if ((y == coords[1] || y == coords[3]) &&
(x >= coords[0] && x <= coords[2]))
pix[x + width * y] = Color.red;
}
}
Texture2D ret = new Texture2D(width, height);
ret.SetPixels(pix);
ret.Apply();
return ret;
}
/// <summary>
/// Find contiguous islands of pixels in a sprite sheet.
/// </summary>
/// <returns>
/// A list of coordinates for each sprite, in the form: [x_min, y_min, x_max, y_max]
/// </returns>
List<int[]> CutSpriteSheet(Texture2D Sprites)
{
// Get pixels, width and height from texture:
Color[] pix = Sprites.GetPixels();
int width = Sprites.width;
int height = Sprites.height;
// Create a new array which identifies which island each pixel belongs to.
int[] Islands = new int[pix.Length];
// Each pixel starts as it's own island, unless it's transparent, in which case we set it to -1
for (int i = 0; i < Islands.Length; i++)
{
if (pix[i] == backGroundColor)
Islands[i] = -1;
else
Islands[i] = i;
}
// For simplicity, we'll convert this to a 2d array
int[,] Islands2d = new int[width, height];
for (int i = 0; i < Islands.Length; i++)
{
int x = i % width;
int y = (i - x) / width;
Islands2d[x, y] = Islands[i];
}
// Now we spread each island
bool Changed = true;
while (Changed)
{
// If no changes are made this loop, we're done
Changed = false;
// For each pixel
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
// If this pixel is transparent, do nothing and continue to the next pixel
if (Islands2d[x, y] == -1)
continue;
// For each pixel neighbouring that pixel
for (int i = -1; i <= 1; i++)
{
// Check the neighbouring pixel is within bounds
if ((x + i) < 0 || (x + i) >= width)
continue;
for (int j = -1; j <= 1; j++)
{
// Check the neighbouring pixel is within bounds
if ((y + j) < 0 || (y + j) >= height)
continue;
// If this and the neighbouring pixel are not in the same island, join them
if (Islands2d[x, y] > Islands2d[x + i, y + j] && Islands2d[x + i, y + j] != -1)
{
Islands2d[x, y] = Islands2d[x + i, y + j];
Changed = true;
}
}
}
}
}
}
// Now all connected islands of points should have the same number (ID)
// We get the number of each island
List<int> Island_IDs = new List<int>();
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
if (Islands2d[x, y] != -1 && !Island_IDs.Contains(Islands2d[x, y]))
Island_IDs.Add(Islands2d[x, y]);
}
}
List<int[]> output = new List<int[]>();
// For each ID, get the upper and lower bounds of that island's coordinates
for (int i = 0; i < Island_IDs.Count; i++)
{
int x_min = int.MaxValue;
int x_max = int.MinValue;
int y_min = int.MaxValue;
int y_max = int.MinValue;
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
// If this point belongs to this island ID
if (Islands2d[x, y] == Island_IDs[i])
{ // Check if its coordinates extend the bounds of this island:
x_min = x < x_min ? x : x_min;
x_max = x > x_max ? x : x_max;
y_min = y < y_min ? y : y_min;
y_max = y > y_max ? y : y_max;
}
}
}
// What you do with this information now is up to you, for now I'll print it to the console:
print("-----");
print("Island ID: " + Island_IDs[i]);
print("Top left corner: " + x_min + ", " + y_min);
print("Bottom right corner: " + x_max + ", " + y_max);
// Also output as an array of coordinates
output.Add(new int[] { x_min, y_min, x_max, y_max });
}
return output;
}
}
A few notes:
- This code is untested, I'm testing it now and will edit with any corrections. - I'm assuming that the top left corner of a texture is (0, 0), I may have this wrong. - This code is horrifically inefficient, in the interest of being readable. If needed I'll write a more efficient version and post later.
- If a sprite consists of two areas of unconnected pixels, this method will treat each area as a separate sprite. You could possibly fix this by testing when sprites overlap.
Let me know if this works/helps, and if I need to explain anything better.
EDIT - see comments below for complete explanation
this is an epic answer haha - give me a few hours to test it out and see what I come up with, but man that is definitely far above and beyond what I expected anyone to answer :P so major thanks for effort!
No worries, looked like a nice challenge. Still testing it myself, and will update in a little while.
Tested it, and it works fine. I used your picture above, and tweaked the script a little to cut out white ins$$anonymous$$d of transparent pixels:
@$$anonymous$$D_Reptile Was just about to post back with exactly that. I've updated the code in the answer above to a complete script (was too long to post in a comment). It includes the original function (with a few tweaks), the function for drawing the red borders above, and a new function for cutting out the individual sprites.
That should be enough code to work from, but just in case I'll explain how you'd do what you asked. I don't actually work with sprites much, and don't know if your using a particular library, so I'm not sure the exact arguments your looking for, but you can construct the rectangle bounds like this (near the end of the CuteSpriteSheet function):
Rect spriteRect = new Rect(x_$$anonymous$$, y_$$anonymous$$, (x_max - x_$$anonymous$$), (y_max - y_$$anonymous$$));
Great answer. In case anyone else comes here from the future like me, line 181 should read:
for (int y = 0; y < height; y++)
:)
Your answer
Follow this Question
Related Questions
How do I access a texture slice's rect via scripting? 1 Answer
Slicing sliced sprite via script 0 Answers
Sprite Rendering Cost Calculation 1 Answer
Lines appear between sprites from sprite sheets 7 Answers
Cast Texture2d to Sprite 4 Answers