如何自动检测并裁剪精灵表中的各个精灵边界?

问题描述 投票:0回答:3

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

Sprite Sheet Example

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

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

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

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

Sprite Sheet With Bounds

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

algorithm image image-processing computer-vision sprite
3个回答
10
投票

这里有一个方法

  • 将图像转换为灰度
  • 获取二值图像的大津阈值
  • 执行形态变换以平滑图像
  • 寻找轮廓
  • 迭代轮廓以绘制边界矩形并提取 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()

4
投票

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

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 条扫描线处理一次。只要不超过最小精灵高度即可。


-2
投票

使用 Pillow 附加 Python 实现:

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

网址

#!/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
© www.soinside.com 2019 - 2024. All rights reserved.