Difference between revisions of "ImageData:mapPixel"

(Add colored stripes to an image:)
(Add sentence about thread safety.)
 
(9 intermediate revisions by 5 users not shown)
Line 1: Line 1:
 
Transform an image by applying a function to every pixel.
 
Transform an image by applying a function to every pixel.
  
This function is a higher order function. It takes another function as a parameter, and calls it once for each pixel in the ImageData.
+
This function is a [https://en.wikipedia.org/wiki/Higher-order_function higher-order function]. It takes another function as a parameter, and calls it once for each pixel in the ImageData.
  
 
The passed function is called with six parameters for each pixel in turn. The parameters are numbers that represent the x and y coordinates of the pixel and its red, green, blue and alpha values. The function should return the new red, green, blue, and alpha values for that pixel.
 
The passed function is called with six parameters for each pixel in turn. The parameters are numbers that represent the x and y coordinates of the pixel and its red, green, blue and alpha values. The function should return the new red, green, blue, and alpha values for that pixel.
Line 14: Line 14:
 
</source>
 
</source>
  
== Function ==
+
In versions prior to [[11.0]], color component values were within the range of 0 to 255 instead of 0 to 1.
=== Synopsis ===
 
<source lang="lua">
 
ImageData:mapPixel( pixelFunction )
 
</source>
 
=== Arguments ===
 
{{param|function|pixelFunction|Function to apply to every pixel.}}
 
=== Returns ===
 
Nothing.
 
  
 +
This function locks the ImageData until it is done, making it safe to use from multiple Threads, albeit without any performance gains.
 
== Function ==
 
== Function ==
{{newin|[[0.9.0]]|090|type=variant}}
 
 
=== Synopsis ===
 
=== Synopsis ===
 
<source lang="lua">
 
<source lang="lua">
Line 32: Line 24:
 
=== Arguments ===
 
=== Arguments ===
 
{{param|function|pixelFunction|Function to apply to every pixel.}}
 
{{param|function|pixelFunction|Function to apply to every pixel.}}
{{param|number|x|The x-axis of the top-left corner of the area within the ImageData to apply the function to.}}
+
{{New feature|0.9.0|
{{param|number|y|The y-axis of the top-left corner of the area within the ImageData to apply the function to.}}
+
{{param|number|x (0)|The x-axis of the top-left corner of the area within the ImageData to apply the function to.}}
{{param|number|width|The width of the area within the ImageData to apply the function to.}}
+
{{param|number|y (0)|The y-axis of the top-left corner of the area within the ImageData to apply the function to.}}
{{param|number|height|The height of the area within the ImageData to apply the function to.}}
+
{{param|number|width (ImageData:getWidth())|The width of the area within the ImageData to apply the function to.}}
 +
{{param|number|height (ImageData:getHeight())|The height of the area within the ImageData to apply the function to.}}
 +
}}
 
=== Returns ===
 
=== Returns ===
 
Nothing.
 
Nothing.
  
 
== Examples ==
 
== Examples ==
=== Brighten an image: ===
+
 
 +
=== Brighten an image ===
 
<source lang="lua">
 
<source lang="lua">
function brighten( x, y, r, g, b, a )
+
local function brighten(x,y, r,g,b,a)
   r = math.min(r * 3, 255)
+
   r = math.min(r*3, 1)
   g = math.min(g * 3, 255)
+
   g = math.min(g*3, 1)
   b = math.min(b * 3, 255)
+
   b = math.min(b*3, 1)
 
   return r,g,b,a
 
   return r,g,b,a
 
end
 
end
 +
imageData:mapPixel(brighten)
 +
</source>
  
imageData:mapPixel( brighten )
+
=== Add colored stripes to an image ===
 +
<source lang="lua">
 +
local function stripey(x,y, r,g,b,a)
 +
  r = math.min(r * math.sin(x*100)*2, 1)
 +
  g = math.min(g * math.cos(x*150)*2, 1)
 +
  b = math.min(b * math.sin(x* 50)*2, 1)
 +
  return r,g,b,a
 +
end
 +
imageData:mapPixel(stripey)
 
</source>
 
</source>
=== Add colored stripes to an image: ===
+
(Source: https://web.archive.org/web/20150515111551/http://khason.net/blog/hlsl-pixel-shader-effects-tutorial/)
 +
 
 +
=== Tint grayscaled image ===
 
<source lang="lua">
 
<source lang="lua">
function stripey( x, y, r, g, b, a )
+
local tintColor = {r=0.6, g=0.5, b=0.3} -- Sepia tone.
  r = math.min(r * math.sin(x*100)*2, 255)
+
 
  g = math.min(g * math.cos(x*150)*2, 255)
+
local function tint(x,y, r,g,b,a)
   b = math.min(b * math.sin(x*50)*2, 255)
+
local gray = (r+g+b) / 3
  return r,g,b,a
+
if gray < 0.5 then
 +
-- Between black and tintColor.
 +
r = 2 * gray*tintColor.r
 +
g = 2 * gray*tintColor.g
 +
b = 2 * gray*tintColor.b
 +
else
 +
-- Between tintColor and white.
 +
r = 2 * (gray + tintColor.r - gray*tintColor.r) - 1
 +
g = 2 * (gray + tintColor.g - gray*tintColor.g) - 1
 +
b = 2 * (gray + tintColor.b - gray*tintColor.b) - 1
 +
end
 +
return r,g,b,a
 +
end
 +
 
 +
imageData:mapPixel(tint)
 +
</source>
 +
 
 +
=== A more efficient method using [https://luajit.org/ext_ffi.html FFI] ===
 +
Here we invert the colors for two images - one using <code>mapPixel</code> and one using FFI functionality. What we should see is that the FFI method is a lot faster (albeit a bit less convenient).
 +
 
 +
Note: We're reading and writing directly from/to memory. Not being careful will likely lead to crashes, or worse!
 +
 
 +
<source lang="lua">
 +
function love.load()
 +
collectgarbage("stop")
 +
 
 +
-- Use mapPixel.
 +
local imageData = love.image.newImageData("bigTexture.png")
 +
local startTime = love.timer.getTime()
 +
 
 +
imageData:mapPixel(function(x,y, r,g,b,a)
 +
return 1-r, 1-g, 1-b, a
 +
end)
 +
 
 +
local time1 = love.timer.getTime() - startTime
 +
_G.image1  = love.graphics.newImage(imageData)
 +
 
 +
-- Use FFI.
 +
local imageData  = love.image.newImageData("bigTexture.png")
 +
local pointer   = require("ffi").cast("uint8_t*", imageData:getFFIPointer()) -- imageData has one byte per channel per pixel.
 +
local pixelCount = imageData:getWidth() * imageData:getHeight()
 +
local startTime  = love.timer.getTime()
 +
 
 +
for i = 0, 4*pixelCount-1, 4 do -- Loop through the pixels, four values at a time (RGBA).
 +
pointer[i  ] = 255 - pointer[i  ] -- r  (Remember that we're working with bytes and not normalized color values.)
 +
pointer[i+1] = 255 - pointer[i+1] -- g
 +
pointer[i+2] = 255 - pointer[i+2] -- b
 +
end
 +
 
 +
local time2 = love.timer.getTime() - startTime
 +
_G.image2  = love.graphics.newImage(imageData)
 +
 
 +
-- Results. (FFI can be more than ten times faster!)
 +
print(string.format("Times: mapPixel=%.4f FFI=%.4f", time1, time2))
 +
print(string.format("Difference: FFI is %.1f times faster", time1/time2))
 
end
 
end
  
imageData:mapPixel( stripey )
+
function love.draw()
 +
-- Show that the resulting images look the same.
 +
local windowWidth = love.graphics.getWidth()
 +
local x1          = 0
 +
local x2          = windowWidth / 2
 +
love.graphics.draw(image1, x1,0, 0, windowWidth/image1:getWidth())
 +
love.graphics.draw(image2, x2,0, 0, windowWidth/image2:getWidth())
 +
end
 
</source>
 
</source>
source: http://khason.net/blog/hlsl-pixel-shader-effects-tutorial/ (broken 11/16)
 
  
 
== See Also ==
 
== See Also ==
 
* [[parent::ImageData]]
 
* [[parent::ImageData]]
 +
* [[ImageData:setPixel]]
 +
 
[[Category:Functions]]
 
[[Category:Functions]]
 
{{#set:Description=Transform an image by applying a function to every pixel.}}
 
{{#set:Description=Transform an image by applying a function to every pixel.}}
 
{{#set:Since=000}}
 
{{#set:Since=000}}
 +
 
== Other Languages ==
 
== Other Languages ==
 
{{i18n|ImageData:mapPixel}}
 
{{i18n|ImageData:mapPixel}}

Latest revision as of 16:23, 25 November 2023

Transform an image by applying a function to every pixel.

This function is a higher-order function. It takes another function as a parameter, and calls it once for each pixel in the ImageData.

The passed function is called with six parameters for each pixel in turn. The parameters are numbers that represent the x and y coordinates of the pixel and its red, green, blue and alpha values. The function should return the new red, green, blue, and alpha values for that pixel.

function pixelFunction(x, y, r, g, b, a)
    -- template for defining your own pixel mapping function
    -- perform computations giving the new values for r, g, b and a
    -- ...
    return r, g, b, a
end

In versions prior to 11.0, color component values were within the range of 0 to 255 instead of 0 to 1.

This function locks the ImageData until it is done, making it safe to use from multiple Threads, albeit without any performance gains.

Function

Synopsis

ImageData:mapPixel( pixelFunction, x, y, width, height )

Arguments

function pixelFunction
Function to apply to every pixel.
Available since LÖVE 0.9.0
number x (0)
The x-axis of the top-left corner of the area within the ImageData to apply the function to.
number y (0)
The y-axis of the top-left corner of the area within the ImageData to apply the function to.
number width (ImageData:getWidth())
The width of the area within the ImageData to apply the function to.
number height (ImageData:getHeight())
The height of the area within the ImageData to apply the function to.

Returns

Nothing.

Examples

Brighten an image

local function brighten(x,y, r,g,b,a)
   r = math.min(r*3, 1)
   g = math.min(g*3, 1)
   b = math.min(b*3, 1)
   return r,g,b,a
end
imageData:mapPixel(brighten)

Add colored stripes to an image

local function stripey(x,y, r,g,b,a)
   r = math.min(r * math.sin(x*100)*2, 1)
   g = math.min(g * math.cos(x*150)*2, 1)
   b = math.min(b * math.sin(x* 50)*2, 1)
   return r,g,b,a
end
imageData:mapPixel(stripey)

(Source: https://web.archive.org/web/20150515111551/http://khason.net/blog/hlsl-pixel-shader-effects-tutorial/)

Tint grayscaled image

local tintColor = {r=0.6, g=0.5, b=0.3} -- Sepia tone.

local function tint(x,y, r,g,b,a)
	local gray = (r+g+b) / 3
	if gray < 0.5 then
		-- Between black and tintColor.
		r = 2 * gray*tintColor.r
		g = 2 * gray*tintColor.g
		b = 2 * gray*tintColor.b
	else
		-- Between tintColor and white.
		r = 2 * (gray + tintColor.r - gray*tintColor.r) - 1
		g = 2 * (gray + tintColor.g - gray*tintColor.g) - 1
		b = 2 * (gray + tintColor.b - gray*tintColor.b) - 1
	end
	return r,g,b,a
end

imageData:mapPixel(tint)

A more efficient method using FFI

Here we invert the colors for two images - one using mapPixel and one using FFI functionality. What we should see is that the FFI method is a lot faster (albeit a bit less convenient).

Note: We're reading and writing directly from/to memory. Not being careful will likely lead to crashes, or worse!

function love.load()
	collectgarbage("stop")

	-- Use mapPixel.
	local imageData = love.image.newImageData("bigTexture.png")
	local startTime = love.timer.getTime()

	imageData:mapPixel(function(x,y, r,g,b,a)
		return 1-r, 1-g, 1-b, a
	end)

	local time1 = love.timer.getTime() - startTime
	_G.image1   = love.graphics.newImage(imageData)

	-- Use FFI.
	local imageData  = love.image.newImageData("bigTexture.png")
	local pointer    = require("ffi").cast("uint8_t*", imageData:getFFIPointer()) -- imageData has one byte per channel per pixel.
	local pixelCount = imageData:getWidth() * imageData:getHeight()
	local startTime  = love.timer.getTime()

	for i = 0, 4*pixelCount-1, 4 do -- Loop through the pixels, four values at a time (RGBA).
		pointer[i  ] = 255 - pointer[i  ] -- r  (Remember that we're working with bytes and not normalized color values.)
		pointer[i+1] = 255 - pointer[i+1] -- g
		pointer[i+2] = 255 - pointer[i+2] -- b
	end

	local time2 = love.timer.getTime() - startTime
	_G.image2   = love.graphics.newImage(imageData)

	-- Results. (FFI can be more than ten times faster!)
	print(string.format("Times: mapPixel=%.4f FFI=%.4f", time1, time2))
	print(string.format("Difference: FFI is %.1f times faster", time1/time2))
end

function love.draw()
	-- Show that the resulting images look the same.
	local windowWidth = love.graphics.getWidth()
	local x1          = 0
	local x2          = windowWidth / 2
	love.graphics.draw(image1, x1,0, 0, windowWidth/image1:getWidth())
	love.graphics.draw(image2, x2,0, 0, windowWidth/image2:getWidth())
end

See Also


Other Languages