Skip to content

Instantly share code, notes, and snippets.

@pfirsich
Created July 6, 2018 22:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pfirsich/d87f30136d1f760b3b9e059ce03a72f7 to your computer and use it in GitHub Desktop.
Save pfirsich/d87f30136d1f760b3b9e059ce03a72f7 to your computer and use it in GitHub Desktop.
Trace blobs in images to approximate polygons around them. Done for @MaChue on the löve Discord server. Images are here: https://imgur.com/a/sFQl7ax
local trace = require("trace")
local imageIndex = 1
local image
local outline
local boundingBox
function updateBoundingBox(padding)
-- minX, minY, maxX, maxY
boundingBox = {math.huge, math.huge, 0, 0}
for i = 1, #outline, 2 do
boundingBox[1] = math.min(boundingBox[1], outline[i+0])
boundingBox[2] = math.min(boundingBox[2], outline[i+1])
boundingBox[3] = math.max(boundingBox[3], outline[i+0])
boundingBox[4] = math.max(boundingBox[4], outline[i+1])
end
padding = padding or 5
boundingBox[1] = boundingBox[1] - padding
boundingBox[2] = boundingBox[2] - padding
boundingBox[3] = boundingBox[3] + padding
boundingBox[4] = boundingBox[4] + padding
-- change it to minX, minY, width, height
boundingBox[3] = boundingBox[3] - boundingBox[1]
boundingBox[4] = boundingBox[4] - boundingBox[2]
end
function updateImage()
imageIndex = math.max(1, math.min(5, imageIndex))
local path = ("sun_area_spr_%d.png"):format(imageIndex)
local imageData = love.image.newImageData(path)
image = love.graphics.newImage(imageData)
print(("tracing image '%s'"):format(path))
local imgW, imgH = imageData:getDimensions()
print(("width: %d, height: %d"):format(imgW, imgH))
local start = love.timer.getTime()
outline = trace.trace(imageData)
print(("%d points in outline"):format(#outline/2))
trace.simplify(outline, nil, 1)
print("time taken: ", love.timer.getTime() - start)
print(("%d points in outline (simplified)"):format(#outline/2))
updateBoundingBox()
end
function love.load()
updateImage()
love.graphics.setLineStyle("rough")
love.graphics.setLineJoin("none")
end
function love.draw()
local lg = love.graphics
local winW, winH = lg.getDimensions()
lg.push()
lg.scale(math.min(winW/boundingBox[3], winH/boundingBox[4]))
lg.translate(-boundingBox[1], -boundingBox[2])
lg.setColor(1, 1, 1)
lg.draw(image)
lg.setColor(0, 1, 0)
lg.line(outline)
lg.setColor(1, 0, 0)
for i = 1, #outline, 2 do
local pointWidth = 0.5
lg.rectangle("fill", outline[i] - pointWidth/2, outline[i+1] - pointWidth/2,
pointWidth, pointWidth)
end
lg.pop()
end
function love.keypressed(key)
if key == "left" then
imageIndex = imageIndex - 1
updateImage()
elseif key == "right" then
imageIndex = imageIndex + 1
updateImage()
end
end
local trace = {}
-- Visvalingam’s algorithm
function trace.simplify(points, targetNumPoints, minTriArea)
assert(targetNumPoints or minTriArea)
assert(not targetNumPoints or not minTriArea)
local function triArea(i)
local ax, ay = points[i], points[i+1]
local bx, by = points[i+2], points[i+3]
local cx, cy = points[i+4], points[i+5]
return math.abs(ax*(by-cy) + bx*(cy-ay) + cx*(ay-by)) / 2
end
while true do
local minArea, minIndex = nil, nil
for i = 1, #points - 6, 2 do
local area = triArea(i)
if not minArea or area < minArea then
minArea = area
minIndex = i
end
end
if minTriArea and minArea > minTriArea then
return
end
table.remove(points, minIndex + 2)
table.remove(points, minIndex + 2)
if targetNumPoints and #points/2 - 1 < targetNumPoints then
return
end
end
end
local pointsMap = {
-- 0 is nil
[1] = {{0.5, 1}, {0, 0.5}},
[2] = {{1, 0.5}, {0.5, 1}},
[3] = {{1, 0.5}, {0, 0.5}},
[4] = {{0.5, 0}, {1, 0.5}},
[5] = nil,
[6] = {{0.5, 0}, {0.5, 1}},
[7] = {{0.5, 0}, {0, 0.5}},
[8] = {{0, 0.5}, {0.5, 0}},
[9] = {{0.5, 1}, {0.5, 0}},
[10] = nil,
[11] = {{1, 0.5}, {0.5, 0}},
[12] = {{0, 0.5}, {1, 0.5}},
[13] = {{0.5, 1}, {1, 0.5}},
[14] = {{0, 0.5}, {0.5, 1}},
[15] = nil,
}
local dirMap = {
-- 0 is nil
[1] = {-1, 0},
[2] = {0, 1},
[3] = {-1, 0},
[4] = {1, 0},
[5] = nil,
[6] = {0, 1},
[7] = {-1, 0},
[8] = {0, -1},
[9] = {0, -1},
[10] = nil,
[11] = {0, -1},
[12] = {1, 0},
[13] = {1, 0},
[14] = {0, 1},
[15] = nil,
}
local function bool2num(b)
return b and 1 or 0
end
-- pass an ImageData object with greyscale data containing a single connected region
function trace.trace(imageData)
-- cell coordinates (like startX and startY) describe the upper left corner of the cell
-- so cell 1, 1 has pixel 1, 1 at the top left corner, 2, 0 at the top right, 2, 2, at bottom right
local imgW, imgH = imageData:getDimensions()
local data = {}
local startX, startY = nil, nil
for y = 1, imgH do
data[y] = {}
for x = 1, imgW do
local r, g, b, a = imageData:getPixel(x-1, y-1)
data[y][x] = a > 0.25
if data[y][x] and not startX and not startY then
startX, startY = x - 1, y - 1
end
end
end
local points = {}
local curX, curY = startX, startY
local iterations = 0
while iterations < imgW*imgH do
iterations = iterations + 1
local index = bool2num(data[curY][curX]) * 8 + bool2num(data[curY][curX+1]) * 4
+ bool2num(data[curY+1][curX+1]) * 2 + bool2num(data[curY+1][curX]) * 1
local newPoints = pointsMap[index]
if not newPoints then
error("Found cell index " .. index)
end
for _, point in ipairs(newPoints) do
table.insert(points, curX + point[1] - 0.5)
table.insert(points, curY + point[2] - 0.5)
end
local dir = dirMap[index]
curX = curX + dir[1]
curY = curY + dir[2]
if curX == startX and curY == startY then
return points
end
if curX < 1 or curX >= imgW or curY < 1 or curY >= imgH then
error("Current cell coordinates out of bounds.")
end
end
error("Exceeded maximum number of iterations.")
end
return trace
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment