image:transformedImage transforms seem to contain an off-by-one error

I used image:transformedImage for the first time and it seems there is an off-by-one error — when flipping the image around its x axis by a transformation that contains scale(-1, 1), the image output is moved one pixel to the right. The same happens when flipping around y axis or both axes.

The expected result in the next picture is 4 identical square/circle images, but when applying X, Y, and XY flips on the respective images, the result looks like this (code and a zipped minimal example are below):

off-axis

I made sure to use an image with an even resolution in both axes. My theory is that somewhere along the way 0- and 1-based numbering collides when relaying the data between lua and C parts of the SDK, but I’m not sure how that could apply in the gfx/image context.

Related

At first I couldn’t find any similar issues, but i’d say that this is the underlying problem in several more specific longstanding issues (esp around image rotation), such as:

I recently encountered a similar weird issue where Image:sample() returns phantom values — where there’s seems to be a problem with boundary check being aligned with 1-based numbering in a bitmap context.

Code

This is the code that generated the image (full minimal example: transformed-image-bug.zip (10.8 KB)):

-- image with a circle drawn inside
local img = gfx.image.new(10, 10)
gfx.pushContext(img)
gfx.drawCircleAtPoint(5, 5, 5)
gfx.popContext()


-- unflipped image
gfx.setDrawOffset(10, 10)
gfx.drawRect(-1, -1, 12, 12)
img:draw(0, 0)


-- flipped X
gfx.setDrawOffset(30, 10)
gfx.drawRect(-1, -1, 12, 12)

local tx = affineTransform.new()
tx:scale(-1, 1)

img:transformedImage(tx):draw(0, 0)


-- flipped Y
gfx.setDrawOffset(50, 10)
gfx.drawRect(-1, -1, 12, 12)

local ty = affineTransform.new()
ty:scale(1, -1)

img:transformedImage(ty):draw(0, 0)


-- flipped XY
gfx.setDrawOffset(70, 10)
gfx.drawRect(-1, -1, 12, 12)

local txy = affineTransform.new()
txy:scale(-1, -1)

img:transformedImage(txy):draw(0, 0)
1 Like

Can you work around it by making the image an odd number of pixels wide/high, leaving an extra empty row/column?

I can save the cropped-out pixels by expanding the image and then shift the drawing back into place (interestingly, applying translate on the affineTransform does nothing) but the code gets quickly less performant and more complex (and the required translation depends on the applied transforms).

If I need a bitmap as an output, it’s easier to use the draw method with a flip flag directly (and it works correctly, without any need to shift it back into place). I’m using this to flip tilesets, so there’s a lot of small operations and the transforms would probably be much more efficient there (in many aspects — cpu, mem, and gc congestion).

-- desired
local tx = affineTransform.new()
tx:scale(-1, 1)

local flippedX = image:transformedImage(tx)

-- vs workaround
local function flipImage(src, flip)
	local w, h = src:getSize()
	local out = gfx.image.new(w, h)
	gfx.pushContext(out)
	src:draw(0, 0, flip)
	gfx.popContext()
	return out
end

local flippedX = flipImage(image, gfx.kImageFlippedX)

EDIT: uhm, i edited the first paragraph a bit to be less confusing.

1 Like