How to efficiently erase pixels from an image

Hi, when I’m removing pixels from and image (setting them to 0,0,0,0 rather) I get about 10 FPS… Bearing in mind this has a for loop of around 50 different pixels. This doesnt work well if I want to erase an part of an image without it being all choppy and slow so I can’t just swipe and erase a section. Is it possible to make this any faster but still being able to erase with a radius of 20 pixels or more? This is my code at the moment which uses image:set() to erase pixels.

for x=-width/2,width/2 do
 for y=-width/2,width/2 do
  local pix = vec2(td.x+x,td.y+y)
  if (vec2()-vec2(x,y)):len()<width/2 and inScreen(pix) then
   screen:set(pix.x,pix.y,0,0,0,0)
  end
 end
end

@Luatee If you’re erasing a rectangle or square, you can use image.copy to erase a section. That’s a lot faster.

EDIT: maybe not. Let me check for sure.

It doesn’t take it out of the image it just copies that section of the image so no pixels are erased.

@Luatee I thought it would allow you to copy a section from a blank image into your image. Apparently it doesn’t allow copying a section from one image into another.

@Luatee Use setContext() and rect or ellipse to erase pixels. That works, I tried it.

That just draws on top of what’s there, if you’re not talking about drawing an ellipse with no alpha then please expand

You said you had an image that you want to remove pixels from. If you do a setContext() using your image, then any rect or ellipse command will be drawn on that image. If the fill() is set to 0, then the result will 0 out those pixels. Here’s an example.


displayMode(FULLSCREEN)
    
function setup()
    img1=readImage("Cargo Bot:Codea Icon")
    setContext(img1)
    fill(0)
    rect(100,100,50,50)
end

function draw()
    background(40,40,50)
    sprite(img1,WIDTH/2,HEIGHT/2)
end

@Dave1707 - your method blacks out pixels but doesn’t delete them. This code compares your method with the slower looping method. The result is supposed to be a transparent square in each case.


displayMode(FULLSCREEN)
    
function setup()
    img1=readImage("Cargo Bot:Codea Icon")
    setContext(img1)
    fill(0)
    rect(100,100,50,50)
    for i=200,250 do
        for j=200,250 do
            img1:set(i,j,color(0,0,0,0))
        end
    end
end

function draw()
    background(255,255,0)
    sprite(img1,WIDTH/2,HEIGHT/2)
end

@Ignatz I guess I didn’t read the original post well enough. I saw the 0,0,0,0 and thought that the result was black. I’m wondering if there’s a way that my routine can do transparent. I haven’t been able to do it yet.

@dave1707 @Luatee - try this. I haven’t tested how fast it is, but it uses a shader.

All you do is choose a unique colour, draw any shape you like on the area you want erased, and then call the Erase function. It will delete all the pixels in your chosen colour.

function setup()
    --draw a BIG image
    img=image(WIDTH,HEIGHT)
    setContext(img)
    sprite("Cargo Bot:Starry Background",WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
    setContext()
    print("Press the button to erase stuff")
    parameter.action("Erase",TestErase)
end

function TestErase()
    --Step 1 - choose a colour that will never be in your image
    pushStyle()
    local c=color(99,99,99,255) 
    fill(c)
    --Step 2 - draw a circle and rectangle to erase
    setContext(img)
        ellipse(150,150,200,300) --draw ellipse
        rect(400,400,200,300) --now a rectangle
    setContext()
    popStyle()
    -- Step 3 - erase everything in the chosen colour
    img=Erase(img,c)
    print("Yellow background should show through the ellipse and rectangle")    
end

function draw()
    background(255,255,0)
    sprite(img,WIDTH/2,HEIGHT/2)
end

function Erase(img,c) --image and colour to erase, returns new image
    local m=mesh()
    m.addRect(m,img.width/2,img.height/2,img.width,img.height)
    m.texture=img
    m.shader=shader(EraseShader.vertexShader,EraseShader.fragmentShader)
    m:setColors(c)
    local img1=image(img.width,img.height)
    setContext(img1)
    m:draw()
    setContext()
    return img1
end

EraseShader = {
vertexShader = [[

uniform mat4 modelViewProjection;
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;

void main()
{
    vColor=color;
    vTexCoord = texCoord;
    gl_Position = modelViewProjection * position;
}

]],
fragmentShader = [[

precision highp float;
uniform lowp sampler2D texture;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;

void main()
{
    lowp vec4 col = texture2D( texture, vTexCoord);
    if( vColor == col ) discard;
    else gl_FragColor = col;
}

]]}

@Ignatz That works nice. I’ve got to learn how to use the shader stuff. One thing I noticed, comment out the sprite line in the setup routine so you just show the yellow background. Press the erase button. There’s a black outline where the ellipse and rect got erased, so all of the ellipse or rect didn’t get erased.

The black outline is because Codea insists on anti aliasing images, so it blends different pixel colours together. This creates an outline that is slightly different from either colour, so the shader doesn’t pick it up. Which means this isn’t a complete solution.

The alternative is to do it all in the shader, but this means the shader has to be told what shapes to cut out.

And I encourage you to learn shaders. They looked awful to me at first, but I found they were all bark and no bite - except being hard to debug because they either work or they don’t! I have stacks of tutorials on them, go for it.

@Ignatz I looked thru the tutorials, but I haven’t really started to play with code yet. As for the erase, I think the best way so far was the original “for loops” with set. It’s slow, but it does the job.

@dave1707 - how about this? The shader does all the work so it gives a clean result.

 
function setup()
    --draw a BIG image
    img=image(WIDTH,HEIGHT)
    setContext(img)
    sprite("Cargo Bot:Starry Background",WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
    setContext()
    print("Press the button to erase stuff")
    parameter.action("Erase",TestErase)
end

function TestErase()
    img=Erase(img,"ellipse",400,500,300,100)
    img=Erase(img,"rect",100,100,100,150)
    print("Yellow background should show through the ellipse and rectangle")    
end

function draw()
    background(255,255,0) --erased pixels should show this yellow background
    sprite(img,WIDTH/2,HEIGHT/2)
end

--img = image to edit
--shape = "ellipse" or "rect"
-- x,y is centre of ellipse or bottom left of rectangle
-- w,h is x,y radius for ellipse, and width and height for rect
function Erase(img,shape,x,y,w,h) 
    local m=mesh()
    m.addRect(m,img.width/2,img.height/2,img.width,img.height)
    m.texture=img
    m.shader=shader(EraseShader.vertexShader,EraseShader.fragmentShader)
    local W,H=img.width,img.height
    if shape=="ellipse" then m.shader.ellipse=vec4(x/W,y/H,w/W,h/H) m.shader.rect=vec4(0,0,0,0)
    elseif shape=="rect" then m.shader.rect=vec4(x/W,y/H,(x+w)/W,(y+h)/H) m.shader.ellipse=vec4(0,0,0,0)
    end
    m:setColors(color(255))
    local img1=image(img.width,img.height)
    setContext(img1)
    m:draw()
    setContext()
    return img1
end

EraseShader = {
vertexShader = [[

uniform mat4 modelViewProjection;
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;

void main()
{
    vColor=color;
    vTexCoord = texCoord;
    gl_Position = modelViewProjection * position;
}

]],
fragmentShader = [[

precision highp float;
uniform lowp sampler2D texture;
uniform vec4 ellipse;
uniform vec4 rect;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;

void main()
{
    lowp vec4 col = texture2D( texture, vTexCoord);
    if (rect.z>0.0) {
        if( vTexCoord.x>=rect.x && vTexCoord.x<rect.z && vTexCoord.y>=rect.y && vTexCoord.y<rect.w) discard;
        else gl_FragColor = col;
    }
    if (ellipse.z>0.0) {
        float a = (vTexCoord.x-ellipse.x) * (vTexCoord.x-ellipse.x) / ( ellipse.z * ellipse.z );
        float b = (vTexCoord.y-ellipse.y) * (vTexCoord.y-ellipse.y) / ( ellipse.w * ellipse.w );
        if ( a + b <= 1.0 ) discard;
        else gl_FragColor = col;
    }
}

]]}

Shaders aren’t necessary here, nor is looping. What you need is an alpha mask. These are really easy to implement using blendMode. We need three images: a source image, a mask image, and a scratch image to hold the resultant picture. We first put the source image onto the scratch one (actually, first we blank the scratch image to fully transparent). Then we apply blendMode(ZERO,ONE,DST_ALPHA,ZERO) before we overlay the alpha mask on top. This has the effect of only allowing through those parts of the image that the mask allows.

What this does is:

final colour = 0 * mask colour + 1 * image colour = image colour
final alpha = (image alpha) * (mask alpha) + 0 * (image alpha)

so the final colour is that of the image, but the alpha is the product of the alpha of the image with that of the mask. So setting the alpha on the mask to 0 sets the alpha on the final image to 0, whilst setting the alpha on the mask to 1 sets the alpha on the final image to that of the source image.

We need the premultiplied flag to be false.

function setup()
    mask = image(WIDTH,HEIGHT)
    setContext(mask)
    background(0,0,0,0)
    setContext()
    canvas = image(WIDTH,HEIGHT)
    parameter.number("radius",1,50,20)
    fps = {}
    nfps = 20
    for k=1,nfps do
        table.insert(fps,1/60)
    end
    parameter.watch("math.floor(nfps/afps)")
end

function draw()
    table.remove(fps,1)
    table.insert(fps,DeltaTime)
    afps = 0
    for k,v in ipairs(fps) do
        afps = afps + v
    end
    background(37, 147, 155, 255)
    spriteMode(CORNER)
    pushStyle()
    setContext(canvas)
    background(0,0,0,0)
    sprite("Cargo Bot:Codea Icon",0,0,WIDTH,HEIGHT)
    blendMode(ZERO,ONE,DST_ALPHA,ZERO)
    sprite(mask,0,0)
    setContext()
    canvas.premultiplied = false
    popStyle()
    sprite(canvas,0,0)
end

function touched(touch)
    setContext(mask)
    noStroke()
    fill(0,0,0, 255)
    ellipse(touch.x,touch.y,radius)
    setContext()
end

@Andrew_Stacey - in the context of a drawing app, suppose you have a picture you have drawn, and you choose to delete a rectangle from it, then you draw some more, and you choose to delete another rectangle from it.

It’s not clear to me how your code would achieve this unless you generate a fresh mask for each drawing operation, and apply the mask to a background image to make it permanent.

@Ignatz I’m not sure that I understand the question. Generating a fresh mask is cheap: blank the existing mask and then draw on it again.

There are several ways that one can implement “erasing” but all can be realised by alpha masks. All that changes is when the alpha gets applied to the image. In the above code the image is not changed but the alpha is applied at render time. You could also have it so that the alpha was applied directly to the image.

I also have code which uses shaders to apply the alpha mask instead of drawing on a canvas image and then displaying the canvas.

@Andrew I’m not quite sure what the code you posted does, I ran it and I got no difference when I touched the screen… I understand the alpha mask concept though

@Luatee Whoops! Try again - or change the background(0) in setup to background(0,0,0,0). I changed the code and forgot to include that change.

But this doesnt erase from the main image if I’ve understood correctly