[Script suggestion] Selecting 'orphan pixels'

Hi everyone!

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.

I would really appreciate help of any kind!

1 Like

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
2 Likes

Thank you for taking the time!

I just tried the script and… maybe I’m doing something wrong? It looks like it can locate the orphan pixels but the selection is offset??

1 Like

Hmmmm. It worked on a similar image on my machine.

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.

Hi @shemake,

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.

kernelAnim

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.)

issue2White

That means that orphan pixels of the color white will be treated differently than others.

Cheers,
Jeremy

2 Likes

@behreandtjeremy Thank 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.

2 Likes

@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?

2 Likes

Hi @shemake,

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.

Cheers,
Jeremy

2 Likes

Hey! Sorry I missed some posts.

I see that it wasn’t that simple… Thank you very much to both of you.

It is working for me now!

2 Likes

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.

Thanks for all of your help!

1 Like

I’m so glad! Thanks for testing it out and making sure things are working as expected.

Here’s an updated version of the script. This one addresses white pixels on the edge of the image, too :slight_smile: Calling this one complete, unless someone finds any more bugs that need to be addressed.

1 Like