Created
July 6, 2018 22:15
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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