【问题标题】:How to automatically detect and crop individual sprite bounds in sprite sheet?Sprite Sheet 自动检测单个 Sprite 边界
【发布时间】:2012-11-15 02:16:36
【问题描述】:

给定一个像这样的精灵表:

我想编写一个算法,可以循环遍历像素数据并确定每个离散精灵的边界矩形。

如果我们假设对于每个像素 X、Y,我可以拉出真(像素不完全透明)或假(像素完全透明),我将如何为每个精灵自动生成边界矩形?

结果数据应该是具有 {x, y, width, height} 的矩形对象数组。

这是同一张图片,但前四个精灵的边界用浅蓝色标记:

任何人都可以逐步说明如何检测上述边界吗?

【问题讨论】:

  • 精灵总是全连接的吗?
  • 并非总是如此,但重要的是它们之间会有一条直线。
  • 对于仍在寻找自动从精灵表中剪切精灵的算法的人,我想指出这篇论文:pdfs.semanticscholar.org/9f78/… 它使用 Blob 检测算法,并且通常可以成功检测未连接精灵(例如,粒子向各个方向飞行的爆炸)。示例实现源代码在 GitHub 上:github.com/marcelomesmo/MuSSE

标签: image algorithm image-processing


【解决方案1】:

这是一种方法

  • 将图像转换为灰度
  • Otsu 获取二值图像的阈值
  • 执行形态变换以平滑图像
  • 寻找轮廓
  • 遍历轮廓以绘制边界矩形并提取 ROI

转换成灰度后,我们用Otsu的阈值得到二值图像

接下来我们进行形态变换,将每个精灵合并成一个轮廓

从这里我们找到轮廓,遍历每个轮廓,绘制边界矩形,并提取每个 ROI。这是结果

这是每个保存的精灵 ROI

我已经使用 OpenCV 和 Python 实现了这个方法,但是您可以将该策略调整到任何语言

import cv2

image = cv2.imread('1.jpg')
original = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
close = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=2)
dilate = cv2.dilate(close, kernel, iterations=1)

cnts = cv2.findContours(dilate, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]

sprite_number = 0
for c in cnts:
    x,y,w,h = cv2.boundingRect(c)
    ROI = image[y:y+h, x:x+w]
    cv2.imwrite('sprite_{}.png'.format(sprite_number), ROI)
    cv2.rectangle(image, (x, y), (x + w, y + h), (36,255,12), 2)
    sprite_number += 1

cv2.imshow('thresh', thresh)
cv2.imshow('dilate', dilate)
cv2.imshow('image', image)
cv2.waitKey()

【讨论】:

  • 感谢分享!这很好用。
【解决方案2】:

这个怎么样?唯一的缺点是您需要图像的可写版本来标记访问过的像素,否则填充将永远不会终止。

Process each* scan line in turn
  For each scanline, walk from left to right, until you find a non-transparent pixel P.
    If the location of P is already inside a known bounded box
      Continue to the right of the bounded box
    Else
      BBox = ExploreBoundedBox(P)
      Add BBox to the collection of known bounded boxes

Function ExploreBoundedBox(pStart)
  Q = new Queue(pStart)
  B = new BoundingBox(pStart)

  While Q is not empty
    Dequeue the front element as P
    Expand B to include P

    For each of the four neighbouring pixels N
      If N is not transparent and N is not marked
        Mark N
        Enqueue N at the back of Q

  return B

您不需要处理每条扫描线,您可以每 10 条或每 30 条扫描线处理一次。只要不超过最小精灵高度即可。

【讨论】:

  • 好答案。我有两个 cmets: 1. 如果您没有图像的可写版本,则可以使用 Set 来跟踪标记的像素。 2. 在ExploreBoundedBox 中查找相邻像素时,最好获取 8 个相邻像素(即对角线)。看到第一排的松树;它最右边的像素只与主体对角连接。
  • 感谢@Astrotrain 和凯文。我要实现这个并让你知道结果!
  • @Kevin:你说得对,好点。但我建议使用 Hashtable 而不是 Set,因为它在大型集合上的性能更好(并且精灵中的像素数量非常大)。在图像内部绘制仍然是最快的,所以如果这是一个选项......
  • 太棒了,这很好用……而且速度也很快。在 JavaScript 中使用上面的图像并使用 HTML 画布大约需要 59 毫秒 :)
【解决方案3】:

使用 Pillow 在 Python 上附加实现:

网址:https://gist.github.com/tuaplicacionpropia/f5bd6b0f69a11141767387eb789f5093

URL

#!/usr/bin/env python
#coding:utf-8

from __future__ import print_function
from PIL import Image

class Sprite:
  def __init__(self):
    self.start_x = -1
    self.start_y = -1
    self.end_x = -1
    self.end_y = -1

  def expand (self, point):
    if (self.start_x < 0 and self.start_y < 0 and self.end_x < 0 and self.end_y < 0):
      self.start_x = point[0]
      self.start_y = point[1]
      self.end_x = point[0]
      self.end_y = point[1]
    else:
      if (point[0] < self.start_x):
        self.start_x = point[0]
      if (point[0] > self.end_x):
        self.end_x = point[0]
      if (point[1] < self.start_y):
        self.start_y = point[1]
      if (point[1] > self.end_y):
        self.end_y = point[1]

  def belongs (self, point):
    result = False
    result = True
    result = result and point[0] >= self.start_x and point[0] <= self.end_x
    result = result and point[1] >= self.start_y and point[1] <= self.end_y
    return result

  def __str__(self):
    result = ""
    result = result + "("
    result = result + str(self.start_x)
    result = result + ", "
    result = result + str(self.start_y)
    result = result + ", "
    result = result + str(self.end_x)
    result = result + ", "
    result = result + str(self.end_y)
    result = result + ")"
    return result

def loadSprite (pos, sprites):
  result = None
  for sprite in sprites:
    if sprite.belongs(pos):
      result = sprite
      break
  return result


def exploreBoundedBox (pStart, img):
  result = None
  q = []
  q.append(pStart)
  result = Sprite()
  result.expand(pStart)
  marks = []
  while (len(q) > 0):
    p = q.pop(0)
    result.expand(p)
    neighbouring = loadEightNeighbouringPixels(p, img)
    for n in neighbouring:
      if img.getpixel(n)[3] > 0 and not n in marks:
        marks.append(n)
        q.append(n)
  return result

def loadFourNeighbouringPixels (point, img):
  result = None
  result = []

  newPoint = (point[0], point[1] - 1)
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0] - 1, point[1])
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0] + 1, point[1])
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0], point[1] + 1)
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  return result

def loadEightNeighbouringPixels (point, img):
  result = None
  result = []

  newPoint = (point[0], point[1] - 1)
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0] - 1, point[1])
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0] + 1, point[1])
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0], point[1] + 1)
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0] - 1, point[1] - 1)
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0] + 1, point[1] - 1)
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0] - 1, point[1] + 1)
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0] + 1, point[1] + 1)
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  return result

im = Image.open("test2.png")
print(im.format, im.size, im.mode)
#PNG (640, 252) RGBA
#im.show()
print("width = " + str(im.width))
print("height = " + str(im.height))



sprites = []
for y in range(im.height):
  for x in range(im.width):
    pixel = im.getpixel((x, y))
    haycolor = True if pixel[3] > 0 else False
    if (haycolor):
      pos = (x, y)
      #print("(" + str(x) + ", " + str(y) + ") -> " + str(pixel))
      pixelP = pixel
      sprite = loadSprite(pos, sprites)
      if (sprite != None):
        x = sprite.end_x
      else:
        sprite = exploreBoundedBox(pos, im)
        sprites.append(sprite)

print("sprites")
print(str(sprites))
idx = 1
for sprite in sprites:
  print("sprite " + str(idx) + ". -> " + str(sprite))
  imSprite = im.crop((sprite.start_x, sprite.start_y, sprite.end_x + 1, sprite.end_y + 1))
  #imSprite.show()
  imSprite.save("sprite" + str(idx) + ".png")
  idx += 1

为了避免留下小部分精灵,我们必须添加以下改进:

MINIMUM_SPRITE = 8

def firstNonSprites (sprites):
  result = None
  for sprite in sprites:
    if (sprite.end_x - sprite.start_x + 1) < MINIMUM_SPRITE or (sprite.end_y - sprite.start_y + 1) < MINIMUM_SPRITE:
      result = sprite
      break
  return result

def mergeSprites (sprite1, sprite2):
  result = None
  if (sprite1 != None and sprite2 != None):
    result = Sprite()
    result.start_x = min(sprite1.start_x, sprite2.start_x)
    result.start_y = min(sprite1.start_y, sprite2.start_y)
    result.end_x = max(sprite1.end_x, sprite2.end_x)
    result.end_y = max(sprite1.end_y, sprite2.end_y)
  return result

def findNextSprite (pivot, sprites):
  result = None
  distance = 99999999
  for sprite in sprites:
    if sprite != pivot:
      itemDistance = distanceSprites(pivot, sprite)
      if (itemDistance < distance):
        distance = itemDistance
        result = sprite
  return result

#Pitagoras
def distancePoints (point1, point2):
  result = 99999999
  if (point1 != None and point2 != None):
    a = abs(point2[0] - point1[0])
    b = abs(point2[1] - point1[1])
    result = math.sqrt(math.pow(a, 2) + math.pow(b, 2))
  return result

def distancePointSprite (point, sprite):
  result = 99999999
  if (point != None and sprite != None):
    distance = distancePoints(point, (sprite.start_x, sprite.start_y))
    if (distance < result):
      result = distance
    distance = distancePoints(point, (sprite.end_x, sprite.start_y))
    if (distance < result):
      result = distance
    distance = distancePoints(point, (sprite.start_x, sprite.end_y))
    if (distance < result):
      result = distance
    distance = distancePoints(point, (sprite.end_x, sprite.end_y))
    if (distance < result):
      result = distance
  return result


def distanceSprites (sprite1, sprite2):
  result = 99999999
  if (sprite1 != None and sprite2 != None):
    distance = distancePointSprite((sprite1.start_x, sprite1.start_y), sprite2)
    if (distance < result):
      result = distance
    distance = distancePointSprite((sprite1.end_x, sprite1.start_y), sprite2)
    if (distance < result):
      result = distance
    distance = distancePointSprite((sprite1.start_x, sprite1.end_y), sprite2)
    if (distance < result):
      result = distance
    distance = distancePointSprite((sprite1.end_x, sprite1.end_y), sprite2)
    if (distance < result):
      result = distance
  return result

def fixMergeSprites (sprites):
  result = []
  pivotNonSprite = firstNonSprites(sprites)
  while (pivotNonSprite != None):
    nextSprite = findNextSprite(pivotNonSprite, sprites)
    if nextSprite == None:
      break
    mergeSprite = mergeSprites(pivotNonSprite, nextSprite)
    sprites.remove(nextSprite)
    sprites.remove(pivotNonSprite)
    sprites.append(mergeSprite)
    pivotNonSprite = firstNonSprites(sprites)
  result = sprites
  return result

#BEFORE CROP
sprites = fixMergeSprites(sprites)

完整代码:

#!/usr/bin/env python
#coding:utf-8

from __future__ import print_function
from PIL import Image
import math

#https://stackoverflow.com/questions/13584586/sprite-sheet-detect-individual-sprite-bounds-automatically?rq=1
'''
Process each* scan line in turn
  For each scanline, walk from left to right, until you find a non-transparent pixel P.
    If the location of P is already inside a known bounded box
      Continue to the right of the bounded box
    Else
      BBox = ExploreBoundedBox(P)
      Add BBox to the collection of known bounded boxes

Function ExploreBoundedBox(pStart)
  Q = new Queue(pStart)
  B = new BoundingBox(pStart)

  While Q is not empty
    Dequeue the front element as P
    Expand B to include P

    For each of the four neighbouring pixels N
      If N is not transparent and N is not marked
        Mark N
        Enqueue N at the back of Q

  return B
'''

class Sprite:
  def __init__(self):
    self.start_x = -1
    self.start_y = -1
    self.end_x = -1
    self.end_y = -1

  def expand (self, point):
    if (self.start_x < 0 and self.start_y < 0 and self.end_x < 0 and self.end_y < 0):
      self.start_x = point[0]
      self.start_y = point[1]
      self.end_x = point[0]
      self.end_y = point[1]
    else:
      if (point[0] < self.start_x):
        self.start_x = point[0]
      if (point[0] > self.end_x):
        self.end_x = point[0]
      if (point[1] < self.start_y):
        self.start_y = point[1]
      if (point[1] > self.end_y):
        self.end_y = point[1]

  def belongs (self, point):
    result = False
    result = True
    result = result and point[0] >= self.start_x and point[0] <= self.end_x
    result = result and point[1] >= self.start_y and point[1] <= self.end_y
    return result

  def __str__(self):
    result = ""
    result = result + "("
    result = result + str(self.start_x)
    result = result + ", "
    result = result + str(self.start_y)
    result = result + ", "
    result = result + str(self.end_x)
    result = result + ", "
    result = result + str(self.end_y)
    result = result + ")"
    return result

def loadSprite (pos, sprites):
  result = None
  for sprite in sprites:
    if sprite.belongs(pos):
      result = sprite
      break
  return result


'''
Function ExploreBoundedBox(pStart)
  Q = new Queue(pStart)
  B = new BoundingBox(pStart)

  While Q is not empty
    Dequeue the front element as P
    Expand B to include P

    For each of the four neighbouring pixels N
      If N is not transparent and N is not marked
        Mark N
        Enqueue N at the back of Q

  return B
'''
def exploreBoundedBox (pStart, img):
  result = None
  q = []
  q.append(pStart)
  result = Sprite()
  result.expand(pStart)
  marks = []
  while (len(q) > 0):
    p = q.pop(0)
    result.expand(p)
    neighbouring = loadEightNeighbouringPixels(p, img)
    for n in neighbouring:
      if img.getpixel(n)[3] > 0 and not n in marks:
        marks.append(n)
        q.append(n)
  return result

def loadFourNeighbouringPixels (point, img):
  result = None
  result = []

  newPoint = (point[0], point[1] - 1)
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0] - 1, point[1])
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0] + 1, point[1])
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0], point[1] + 1)
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  return result

def loadEightNeighbouringPixels (point, img):
  result = None
  result = []

  newPoint = (point[0], point[1] - 1)
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0] - 1, point[1])
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0] + 1, point[1])
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0], point[1] + 1)
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0] - 1, point[1] - 1)
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0] + 1, point[1] - 1)
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0] - 1, point[1] + 1)
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  newPoint = (point[0] + 1, point[1] + 1)
  if (newPoint[0] >= 0 and newPoint[1] >= 0 and newPoint[0] < img.width and newPoint[1] < img.height):
    result.append(newPoint)

  return result

MINIMUM_SPRITE = 8

def firstNonSprites (sprites):
  result = None
  for sprite in sprites:
    if (sprite.end_x - sprite.start_x + 1) < MINIMUM_SPRITE or (sprite.end_y - sprite.start_y + 1) < MINIMUM_SPRITE:
      result = sprite
      break
  return result

def mergeSprites (sprite1, sprite2):
  result = None
  if (sprite1 != None and sprite2 != None):
    result = Sprite()
    result.start_x = min(sprite1.start_x, sprite2.start_x)
    result.start_y = min(sprite1.start_y, sprite2.start_y)
    result.end_x = max(sprite1.end_x, sprite2.end_x)
    result.end_y = max(sprite1.end_y, sprite2.end_y)
  return result

def findNextSprite (pivot, sprites):
  result = None
  distance = 99999999
  for sprite in sprites:
    if sprite != pivot:
      itemDistance = distanceSprites(pivot, sprite)
      if (itemDistance < distance):
        distance = itemDistance
        result = sprite
  return result

#Pitagoras
def distancePoints (point1, point2):
  result = 99999999
  if (point1 != None and point2 != None):
    a = abs(point2[0] - point1[0])
    b = abs(point2[1] - point1[1])
    result = math.sqrt(math.pow(a, 2) + math.pow(b, 2))
  return result

def distancePointSprite (point, sprite):
  result = 99999999
  if (point != None and sprite != None):
    distance = distancePoints(point, (sprite.start_x, sprite.start_y))
    if (distance < result):
      result = distance
    distance = distancePoints(point, (sprite.end_x, sprite.start_y))
    if (distance < result):
      result = distance
    distance = distancePoints(point, (sprite.start_x, sprite.end_y))
    if (distance < result):
      result = distance
    distance = distancePoints(point, (sprite.end_x, sprite.end_y))
    if (distance < result):
      result = distance
  return result


def distanceSprites (sprite1, sprite2):
  result = 99999999
  if (sprite1 != None and sprite2 != None):
    distance = distancePointSprite((sprite1.start_x, sprite1.start_y), sprite2)
    if (distance < result):
      result = distance
    distance = distancePointSprite((sprite1.end_x, sprite1.start_y), sprite2)
    if (distance < result):
      result = distance
    distance = distancePointSprite((sprite1.start_x, sprite1.end_y), sprite2)
    if (distance < result):
      result = distance
    distance = distancePointSprite((sprite1.end_x, sprite1.end_y), sprite2)
    if (distance < result):
      result = distance
  return result

def fixMergeSprites (sprites):
  result = []
  pivotNonSprite = firstNonSprites(sprites)
  while (pivotNonSprite != None):
    nextSprite = findNextSprite(pivotNonSprite, sprites)
    if nextSprite == None:
      break
    mergeSprite = mergeSprites(pivotNonSprite, nextSprite)
    sprites.remove(nextSprite)
    sprites.remove(pivotNonSprite)
    sprites.append(mergeSprite)
    pivotNonSprite = firstNonSprites(sprites)
  result = sprites
  return result

im = Image.open("test.png")
print(im.format, im.size, im.mode)
#PNG (640, 252) RGBA
#im.show()
print("width = " + str(im.width))
print("height = " + str(im.height))



sprites = []
for y in range(im.height):
  for x in range(im.width):
    pixel = im.getpixel((x, y))
    haycolor = True if pixel[3] > 0 else False
    if (haycolor):
      pos = (x, y)
      #print("(" + str(x) + ", " + str(y) + ") -> " + str(pixel))
      pixelP = pixel
      sprite = loadSprite(pos, sprites)
      if (sprite != None):
        x = sprite.end_x
      else:
        sprite = exploreBoundedBox(pos, im)
        sprites.append(sprite)

sprites = fixMergeSprites(sprites)

print("sprites")
print(str(sprites))
idx = 1
for sprite in sprites:
  print("sprite " + str(idx) + ". -> " + str(sprite))
  imSprite = im.crop((sprite.start_x, sprite.start_y, sprite.end_x + 1, sprite.end_y + 1))
  #imSprite.show()
  imSprite.save("sprite" + str(idx) + ".png")
  idx += 1

【讨论】:

    猜你喜欢
    • 2014-02-02
    • 2014-10-03
    • 2014-02-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-12
    • 2016-01-14
    相关资源
    最近更新 更多