First time posting… I hope I’m not doing anything wrong.
I would love to have a script that finds and selects “orphan pixels”. By orphan pixels I mean a lone pixel that doesn’t touch any pixel cluster. A pixel that is not adjacent to any pixel with its same value.
I have no idea how aseprite scripting works but sounds like this might be a fairly simple code and it would save me tons of time.
Here you go. Source is below, but there’s a direct download here:
(it will also work on a selection, if you don’t want orphan pixels across the entire image)
------------------------------------------------------------------------
-- An Aseprite script that finds orphan pixels
--
-- by Willow Willis
-- MIT license: free for all uses, commercial or otherwise
------------------------------------------------------------------------
local sprite = app.activeSprite
if not sprite then
return app.alert("There is no active sprite")
end
-- Simple search. Inspects every pixel and its neighbors
-- (could be more efficient, but meh)
function selectOrphans()
local pc = app.pixelColor
local img = app.activeImage
local spr = app.activeSprite
local sel = spr.selection
local box = sel.bounds
if (box.width == 0 and box.height == 0) then
box = Rectangle(0,0,img.width,img.height)
else
sel:deselect()
end
for y = box.y, box.y + (box.height - 1) do
for x = box.x, box.x + (box.width - 1) do
local rgba = pc.rgba( img:getPixel(x,y) )
-- Check surrounding 8 pixels
if( rgba ~= pc.rgba( img:getPixel(x+1,y-1) ) and
rgba ~= pc.rgba( img:getPixel(x+1,y ) ) and
rgba ~= pc.rgba( img:getPixel(x+1,y+1) ) and
rgba ~= pc.rgba( img:getPixel(x, y-1) ) and
rgba ~= pc.rgba( img:getPixel(x, y+1) ) and
rgba ~= pc.rgba( img:getPixel(x-1,y-1) ) and
rgba ~= pc.rgba( img:getPixel(x-1,y ) ) and
rgba ~= pc.rgba( img:getPixel(x-1,y+1) ) ) then
-- found orphan. select it
sel:add( Rectangle( x, y, 1, 1 ) )
end
end
end
end
-- Run the script
do
selectOrphans()
end
What version of Aseprite are you using? And could you please attach your original image in its original size here? There must be something I overlooked.
This script is cool! Thanks for putting it together, and welcome to the forum.
In regards to the first issue, I’d start by looking at the animated diagram in the docs under Image:getPixel. Cel images will periodically be trimmed according to a mask color. After trimming, they’ll have a non-zero top-left corner offset.
You can diagnose with either cel position or cel bounds. I find it helpful to go to View > Show > Layer Edges. Cels can exceed the sprite canvas bounds, but get cropped when the Canvas Size command is called. There are cases where a selection can exceed the canvas bounds as well.
While I’m here, I’ll mention an issue that I noticed:
The script basically features a kernel moving across a pixel array. So neighbors where the kernel goes out of bounds are a special case.
There’s a bug with getPixel where it returns the color opaque white instead of the mask color when its input coordinates are out of bounds. (The signed integer -1 wraps around when cast to an unsigned integer.)
That means that orphan pixels of the color white will be treated differently than others.
@behreandtjeremyThank you! I’m new to both Lua scripting and the Aseprite API, so you just saved me a couple of hours.
I knew that I was checking out-of-bound neighbors, but as it didn’t seem to make any difference in my test cases, I figured I could ignore that. Should have known better. xD I’ll see if I can pin down OP’s bug, given this new information.
@Hector_Bometon Here’s an update that fixes the offsets issue. I wasn’t accounting for running it on individual layers before.
------------------------------------------------------------------------
-- An Aseprite script that finds orphan pixels
--
-- by Willow Willis
-- MIT license: free for all uses, commercial or otherwise
------------------------------------------------------------------------
local sprite = app.activeSprite
if not sprite then
return app.alert("There is no active sprite")
end
-- Simple search. Inspects every pixel and its neighbors
-- (could be more efficient, but meh)
function selectOrphans()
local pc = app.pixelColor
local img = app.activeImage
local spr = app.activeSprite
local cel = img.cel
local box = cel.bounds
local sel = spr.selection
if (sel.bounds.width == 0 and sel.bounds.height == 0) then
sel = Selection( Rectangle(0,0,img.width,img.height) )
end
-- We'll be building up a new selection
local newSel = Selection()
for y = 0, (box.height - 1) do
for x = 0, (box.width - 1) do
local rgba = pc.rgba( img:getPixel(x,y) )
-- Check surrounding 8 pixels
if( rgba ~= pc.rgba( img:getPixel(x+1,y-1) ) and
rgba ~= pc.rgba( img:getPixel(x+1,y ) ) and
rgba ~= pc.rgba( img:getPixel(x+1,y+1) ) and
rgba ~= pc.rgba( img:getPixel(x, y-1) ) and
rgba ~= pc.rgba( img:getPixel(x, y+1) ) and
rgba ~= pc.rgba( img:getPixel(x-1,y-1) ) and
rgba ~= pc.rgba( img:getPixel(x-1,y ) ) and
rgba ~= pc.rgba( img:getPixel(x-1,y+1) ) ) then
-- found orphan. select it
local px = Rectangle( x + box.x, y + box.y, 1, 1 )
if(sel.bounds:contains( px ) ) then -- for some reason, sel:contains() doesn't work
newSel:add( px )
end
end
end
end
-- Set the new selection as the active one
sprite.selection = newSel
end
-- Run the script
do
selectOrphans()
end
@behreandtjeremy I’m not checking for whites yet, but will do that next. I did run into one other problem that I was hoping you knew how to solve.
This expression doesn’t work:
sel:contains( px )
I have to check sel.bounds:contains(px). This, of course, means that finding orphan pixels in anything other than a rectangular selection is going to be imprecise since I’m having to use the selection’s bounding box to make the call. Any idea what’s wrong, or is this a bug?
Selection contains accepts either a Point or integers x and y; px is a Rectangle.
If interested, you could dodge the problem by using selection intersect and rearranging a bit:
------------------------------------------------------------------------
-- An Aseprite script that finds orphan pixels
--
-- by Willow Willis
-- MIT license: free for all uses, commercial or otherwise
------------------------------------------------------------------------
local sprite = app.activeSprite
if not sprite then
return app.alert("There is no active sprite")
end
-- Simple search. Inspects every pixel and its neighbors
-- (could be more efficient, but meh)
local function selectOrphans()
local img = app.activeImage
local cel = img.cel
local box = cel.bounds
local xCel = box.x
local yCel = box.y
local newSel = Selection()
for y = 0, img.height - 1 do
for x = 0, img.width - 1 do
local rgba = img:getPixel(x, y)
-- Check surrounding 8 pixels
if (rgba ~= img:getPixel(x + 1, y - 1) and
rgba ~= img:getPixel(x + 1, y) and
rgba ~= img:getPixel(x + 1, y + 1) and
rgba ~= img:getPixel(x, y - 1) and
rgba ~= img:getPixel(x, y + 1) and
rgba ~= img:getPixel(x - 1, y - 1) and
rgba ~= img:getPixel(x - 1, y) and
rgba ~= img:getPixel(x - 1, y + 1)) then
local px = Rectangle(xCel + x, yCel + y, 1, 1)
newSel:add(px)
end
end
end
local spr = app.activeSprite
local prevSel = spr.selection
if not prevSel.isEmpty then
newSel:intersect(prevSel)
end
-- Set the new selection as the active one
spr.selection = newSel
end
-- Run the script
do
selectOrphans()
end
For whatever reason, I got along without app.pixelColor.rgba. Test that the script works with RGB, grayscale and indexed color mode sprites before making a decision in your next version of the script. There would probably be a complication with tile map layers, but that could wait for another time unless you use that beta feature.
If you come to Lua from a strongly typed programming language and you plan on making scripts long term, I get a lot of help in debugging from the combo VS Code + a Lua language server extension + Aseprite type definitions. That highlighted issues for me right away. There might be similar for different code editors, I don’t know.
Sorry for the late response – I’ve been sick for over a week! I’ll fix the Point vs Rectangle mistake (yes, I do come from a strongly typed language and I’m used to the compiler catching stuff like that) and re-publish to my site.
Here’s an updated version of the script. This one addresses white pixels on the edge of the image, too Calling this one complete, unless someone finds any more bugs that need to be addressed.