I was playing around with Dithering the other day and thought I'd add my script here. It's nothing that hasn't been done (better) above but it was fun to create.
There are a few different dithers and colour reduction methods you can do by changing the arguments of the dither function. My dither matrices are stored in a kind of unusual way, instead of being a proper matrix they're a table of tables with an xoffset, yoffset and then the proportion of error to be diffused to that position.
I had used this originally to dither images for pico-8 that could then be loaded directly to vram from a string so there are a few lines commented out that create that string and print it to the console, I don't reccomend turning them on for a large image, they're very slow.
Finally, depending on the size of the picture you're doing you will need to change the "scale" variable. I could have made it autoadjust but didn't really feel that was necessary for what I wanted it to do.
Dithering to 5 bit colour (4 values of red, 4 of green, 2 of blue) (Floyd-Steinberg)
The pico-8 code I was using to load an image was simply the below that replaces everything in vram with the contents of a string. Note, this string could be a lot shorter with some simple RLE but I was just wanting to see something on screen.
Code: Select all
-- Disable output buffer so debug messages print in real time
io.stdout:setvbuf("no")
function love.load()
love.graphics.setDefaultFilter("nearest", "nearest", 1)
love.window.setMode(1536, 768)
love.window.setTitle("Error Diffusion Dithering")
scale = 6 -- scale to draw the result at
imgPath = "Images/Rainbow128.jpg" -- image to use
-- pico-8 palette for paletted quantizing
pal = {}
pal.Pico8 = {
{29/255, 43/255, 83/255},
{126/255, 37/255, 83/255},
{0/255, 135/255, 81/255},
{171/255, 82/255, 54/255},
{95/255, 87/255, 79/255},
{194/255, 195/255, 199/255},
{255/255, 241/255, 232/255},
{255/255, 0/255, 77/255},
{255/255, 163/255, 0/255},
{255/255, 236/255, 39/255},
{0/255, 228/255, 54/255},
{41/255, 173/255, 255/255},
{131/255, 118/255, 156/255},
{255/255, 119/255, 168/255},
{255/255, 204/255, 170/255},
{0/255, 0/255, 0/255},
}
palStr = ""
-- Setup dither matrices matrix, each is a table of {xoff, yoff, proportion of error to give}
dMat = {}
dMat.FloydSteinberg = {
{ 1, 0, 7/16},
{-1, 1, 3/16},
{ 0, 1, 5/16},
{ 1, 1, 1/16},
}
dMat.Atkinson = {
{ 1, 0, 1/8},
{ 2, 0, 1/8},
{-1, 1, 1/8},
{ 0, 1, 1/8},
{ 1, 1, 1/8},
{ 0, 2, 1/8},
}
dMat.JarvisJudiceNinke = {
{ 1, 0, 7/48},
{ 2, 0, 5/48},
{-2, 1, 3/48},
{-1, 1, 5/48},
{ 0, 1, 7/48},
{ 1, 1, 5/48},
{ 2, 1, 3/48},
{-2, 2, 1/48},
{-1, 2, 3/48},
{ 0, 2, 5/48},
{ 1, 2, 3/48},
{ 2, 2, 1/48},
}
dMat.None = {}
local originalData = love.image.newImageData(imgPath)
originalImg = love.graphics.newImage(originalData)
--transformedImg = dither(originalData, quantPalette, pal.Pico8, false, dMat.FloydSteinberg)
transformedImg = dither(originalData, quantReduce, {4, 4, 2}, false, dMat.FloydSteinberg)
end
function quantReduce(r, g, b, valuesPerColour)
local lr, lg, lb
if type(valuesPerColour) == "table" then
-- we have a table, set levels seperately
lr, lg, lb = valuesPerColour[1], valuesPerColour[2], valuesPerColour[3]
else
-- we have a number, set all the same
lr, lg, lb = valuesPerColour, valuesPerColour, valuesPerColour
end
r = math.floor(r * (lr - 0.5)) / (lr - 1)
g = math.floor(g * (lg - 0.5)) / (lg - 1)
b = math.floor(b * (lb - 0.5)) / (lb - 1)
return r, g, b
end
function quantPalette(r, g, b, pal)
local dist, bestInd, bestDist
bestDist = 1000
for i = 1, #pal do
dist = math.abs(r - pal[i][1]) + math.abs(g - pal[i][2]) + math.abs(b - pal[i][3])
if dist < bestDist then
bestDist = dist
bestInd = i
end
end
-- uncomment to add the char to the palette string
-- palStr = palStr .. string.format("%x", bestInd % 16)
return pal[bestInd][1], pal[bestInd][2], pal[bestInd][3]
end
-- Dithers an imageData. 2nd argument is the quantization function (quantReduce to reduce colour bit depth and quantPalette to match to a palette).
-- quantArg is a number of values per colour for reduce (or a table of 3 like {8, 8, 4} for classic 8-bit reduction).
-- quantArg is a palette table of colours for the palette. Greyscale sets if the image should be turned greyscale first. ditherMatric sets the matric to use.
function dither(data, quantFunc, quantArg, greyscale, ditherMatrix)
local r, g, b, er, eg, eb, val, tx, ty
local height = data:getHeight() - 1
local width = data:getWidth() - 1
-- loop through all pixels, with x as inner loop
for y = 0, height do
for x = 0, width do
-- get pixel
r, g, b = data:getPixel(x, y)
if greyscale then
-- make greyscale
val = (0.299 * r) + (0.587 * g) + (0.114 * b)
r = val; g = val; b = val
end
-- backup r, g, b for working out the error later
er, eg, eb = r, g, b
-- quantize
r, g, b = quantFunc(r, g, b, quantArg)
data:setPixel(x, y, r, g, b, 1)
-- get difference from original pixel (error)
er = er - r
eg = eg - g
eb = eb - b
-- pass error on to neighbours
for i = 1, #ditherMatrix do
tx = x + ditherMatrix[i][1]
ty = y + ditherMatrix[i][2]
-- check it's in bounds, we don't need to check ty > 0 though, we never pass error up
if tx > 0 and tx < width and ty < height then
r, g, b = data:getPixel(tx, ty)
r = r + (er * ditherMatrix[i][3])
g = g + (eg * ditherMatrix[i][3])
b = b + (eb * ditherMatrix[i][3])
data:setPixel(tx, ty, r, g, b, 1)
end
end
end
end
-- uncomment to print the palette string to the console
-- print(palStr)
return love.graphics.newImage(data)
end
function love.draw()
-- draw original image
love.graphics.draw(originalImg, 0, 0, 0, scale, scale)
-- draw dithered image
love.graphics.draw(transformedImg, 768, 0, 0, scale, scale)
end