-- ostromoukhov.lua : Color dithering using variable
-- coefficients.
--
-- https://liris.cnrs.fr/victor.ostromoukhov/publications/pdf/SIGGRAPH01_varcoeffED.pdf
--
-- Version: 02-jan-2017
--
-- Copyright 2016-2017 by Samuel Devulder
--
-- This program is free software; you can redistribute
-- it and/or modify it under the terms of the GNU
-- General Public License as published by the Free
-- Software Foundation; version 2 of the License.
-- See
run('color.lua')
run('thomson.lua')
if not OstroDither then
OstroDither = {}
local function default_levels()
return {r={0,Color.ONE},g={0,Color.ONE},b={0,Color.ONE}}
end
function OstroDither:new(palette,attenuation,levels)
local o = {
attenuation = attenuation or .9, -- works better than 1
palette = palette or thomson.default_palette,
levels = levels or default_levels(),
clash_size = 8 -- for color clash
}
setmetatable(o, self)
self.__index = self
return o
end
function OstroDither:setLevelsFromPalette()
local rLevels = {[1]=true,[16]=true}
local gLevels = {[1]=true,[16]=true}
local bLevels = {[1]=true,[16]=true}
local default_palette = true
for i,pal in ipairs(self.palette) do
local r,g,b=pal%16,math.floor(pal/16)%16,math.floor(pal/256)
rLevels[1+r] = true
gLevels[1+g] = true
bLevels[1+b] = true
if pal~=thomson.default_palette[i] then
default_palette = false
end
end
local levels = {r={},g={},b={}}
for i,v in ipairs(thomson.levels.linear) do
if false then
if rLevels[i] and gLevels[i] and bLevels[i] then
table.insert(levels.r, v)
table.insert(levels.g, v)
table.insert(levels.b, v)
end
else
if rLevels[i] then table.insert(levels.r, v) end
if gLevels[i] then table.insert(levels.g, v) end
if bLevels[i] then table.insert(levels.b, v) end
end
end
self.levels = levels
if default_palette then
self.attenuation = .98
self.levels = default_levels()
else
self.attenuation = .9
self.levels = levels
end
end
function OstroDither:_coefs(linearLevel,rgb)
if self._ostro==nil then
-- original coefs, about to be adapted to the levels
local t={
13, 0, 5,
13, 0, 5,
21, 0, 10,
7, 0, 4,
8, 0, 5,
47, 3, 28,
23, 3, 13,
15, 3, 8,
22, 6, 11,
43, 15, 20,
7, 3, 3,
501, 224, 211,
249, 116, 103,
165, 80, 67,
123, 62, 49,
489, 256, 191,
81, 44, 31,
483, 272, 181,
60, 35, 22,
53, 32, 19,
237, 148, 83,
471, 304, 161,
3, 2, 1,
459, 304, 161,
38, 25, 14,
453, 296, 175,
225, 146, 91,
149, 96, 63,
111, 71, 49,
63, 40, 29,
73, 46, 35,
435, 272, 217,
108, 67, 56,
13, 8, 7,
213, 130, 119,
423, 256, 245,
5, 3, 3,
281, 173, 162,
141, 89, 78,
283, 183, 150,
71, 47, 36,
285, 193, 138,
13, 9, 6,
41, 29, 18,
36, 26, 15,
289, 213, 114,
145, 109, 54,
291, 223, 102,
73, 57, 24,
293, 233, 90,
21, 17, 6,
295, 243, 78,
37, 31, 9,
27, 23, 6,
149, 129, 30,
299, 263, 54,
75, 67, 12,
43, 39, 6,
151, 139, 18,
303, 283, 30,
38, 36, 3,
305, 293, 18,
153, 149, 6,
307, 303, 6,
1, 1, 0,
101, 105, 2,
49, 53, 2,
95, 107, 6,
23, 27, 2,
89, 109, 10,
43, 55, 6,
83, 111, 14,
5, 7, 1,
172, 181, 37,
97, 76, 22,
72, 41, 17,
119, 47, 29,
4, 1, 1,
4, 1, 1,
4, 1, 1,
4, 1, 1,
4, 1, 1,
4, 1, 1,
4, 1, 1,
4, 1, 1,
4, 1, 1,
65, 18, 17,
95, 29, 26,
185, 62, 53,
30, 11, 9,
35, 14, 11,
85, 37, 28,
55, 26, 19,
80, 41, 29,
155, 86, 59,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
305, 176, 119,
155, 86, 59,
105, 56, 39,
80, 41, 29,
65, 32, 23,
55, 26, 19,
335, 152, 113,
85, 37, 28,
115, 48, 37,
35, 14, 11,
355, 136, 109,
30, 11, 9,
365, 128, 107,
185, 62, 53,
25, 8, 7,
95, 29, 26,
385, 112, 103,
65, 18, 17,
395, 104, 101,
4, 1, 1,
4, 1, 1,
395, 104, 101,
65, 18, 17,
385, 112, 103,
95, 29, 26,
25, 8, 7,
185, 62, 53,
365, 128, 107,
30, 11, 9,
355, 136, 109,
35, 14, 11,
115, 48, 37,
85, 37, 28,
335, 152, 113,
55, 26, 19,
65, 32, 23,
80, 41, 29,
105, 56, 39,
155, 86, 59,
305, 176, 119,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
5, 3, 2,
155, 86, 59,
80, 41, 29,
55, 26, 19,
85, 37, 28,
35, 14, 11,
30, 11, 9,
185, 62, 53,
95, 29, 26,
65, 18, 17,
4, 1, 1,
4, 1, 1,
4, 1, 1,
4, 1, 1,
4, 1, 1,
4, 1, 1,
4, 1, 1,
4, 1, 1,
4, 1, 1,
119, 47, 29,
72, 41, 17,
97, 76, 22,
172, 181, 37,
5, 7, 1,
83, 111, 14,
43, 55, 6,
89, 109, 10,
23, 27, 2,
95, 107, 6,
49, 53, 2,
101, 105, 2,
1, 1, 0,
307, 303, 6,
153, 149, 6,
305, 293, 18,
38, 36, 3,
303, 283, 30,
151, 139, 18,
43, 39, 6,
75, 67, 12,
299, 263, 54,
149, 129, 30,
27, 23, 6,
37, 31, 9,
295, 243, 78,
21, 17, 6,
293, 233, 90,
73, 57, 24,
291, 223, 102,
145, 109, 54,
289, 213, 114,
36, 26, 15,
41, 29, 18,
13, 9, 6,
285, 193, 138,
71, 47, 36,
283, 183, 150,
141, 89, 78,
281, 173, 162,
5, 3, 3,
423, 256, 245,
213, 130, 119,
13, 8, 7,
108, 67, 56,
435, 272, 217,
73, 46, 35,
63, 40, 29,
111, 71, 49,
149, 96, 63,
225, 146, 91,
453, 296, 175,
38, 25, 14,
459, 304, 161,
3, 2, 1,
471, 304, 161,
237, 148, 83,
53, 32, 19,
60, 35, 22,
483, 272, 181,
81, 44, 31,
489, 256, 191,
123, 62, 49,
165, 80, 67,
249, 116, 103,
501, 224, 211,
7, 3, 3,
43, 15, 20,
22, 6, 11,
15, 3, 8,
23, 3, 13,
47, 3, 28,
8, 0, 5,
7, 0, 4,
21, 0, 10,
13, 0, 5,
13, 0, 5}
local function process(tab)
local tab2={}
local function add(i)
i=3*math.floor(i+.5)
local c0,c1,c2=t[i+1],t[i+2],t[i+3]
local norm=self.attenuation/(c0+c1+c2)
table.insert(tab2,c0*norm)
table.insert(tab2,c1*norm)
table.insert(tab2,c2*norm)
end
local function level(i)
return tab[i]*255/Color.ONE
end
local a,b,j=level(1),level(2),3
for i=0,255 do
if i>b then a,b,j=b,level(j),j+1; end
add(255*(i-a)/(b-a))
end
return tab2
end
self._ostro = {r=process(self.levels.r),
g=process(self.levels.g),
b=process(self.levels.b)}
end
local i = math.floor(linearLevel[rgb]*255/Color.ONE+.5)
i = 3*(i<0 and 0 or i>255 and 255 or i)
return self._ostro[rgb][i+1],self._ostro[rgb][i+2],self._ostro[rgb][i+3]
end
function OstroDither:_linearPalette(colorIndex)
if self._linear==nil then
self._linear = {}
local t=thomson.levels.linear
for i,pal in ipairs(self.palette) do
local r,g,b=pal%16,math.floor(pal/16)%16,math.floor(pal/256)
self._linear[i] = Color:new(t[1+r],t[1+g],t[1+b])
end
end
return self._linear[colorIndex]
end
function OstroDither:getColorIndex(linearPixel)
local k=linearPixel:hash(64)
local c=self[k]
if c==nil then
local dm=1e30
for i=1,#self.palette do
local d = self:_linearPalette(i):dist2(linearPixel)
if d M and M or a
end
local c0,c1,c2=self:_coefs(linearColor,rgb)
if err0 and c0>0 then err0[rgb] = f(err0[rgb],c0) end
if err1 and c1>0 then err1[rgb] = f(err1[rgb],c1) end
if err2 and c2>0 then err2[rgb] = f(err2[rgb],c2) end
end
d("r"); d("g"); d("b")
return c
end
function OstroDither:dither(screen_w,screen_h,getLinearPixel,pset,serpentine,info)
if not info then info = function(y) thomson.info() end end
if not serpentine then serpentine = true end
local err1,err2 = {},{}
for x=-1,screen_w do
err1[x] = Color:new(0,0,0)
err2[x] = Color:new(0,0,0)
end
for y=0,screen_h-1 do
-- permute error buffers
err1,err2 = err2,err1
-- clear current-row's buffer
for i=-1,screen_w do err2[i]:mul(0) end
local x0,x1,xs=0,screen_w-1,1
if serpentine and y%2==1 then x0,x1,xs=x1,x0,-xs end
for x=x0,x1,xs do
local p = getLinearPixel(x,y,xs,err1)
local c = self:_diffuse(p,err1[x],err1[x+xs],
err2[x-xs],err2[x])
pset(x,y,c-1)
end
info(y)
end
end
function OstroDither:ccAcceptCouple(c1,c2)
return c1~=c2
end
function OstroDither:ccDither(screen_w,screen_h,getLinearPixel,pset,serpentine,info) -- dither with color clash
local c1,c2
self.getColorIndex = function(self,p)
return p:dist2(self:_linearPalette(c1))b.n or a.n==b.n and a.cdm then break end
end
return d
else
return dm
end
end
dm=eval()
if histo:num(1)>=self.clash_size/2+1 then
local z=c2
for i=1,#self.palette do c2=i
local d=eval()
if d0 and 0 or self.clash_size-1) then
findC1C2(x,y,xs,err1)
end
return getLinearPixel(x,y)
end
self:dither(screen_w,screen_h,_getLinearPixel,_pset,serpentine,info)
end
function OstroDither:dither40cols(getpalette,serpentine)
-- get screen size
local screen_w, screen_h = getpicturesize()
-- Converts thomson coordinates (0-159,0-199) into screen coordinates
local function thom2screen(x,y)
local i,j;
if screen_w/screen_h < 1.6 then
i = x*screen_h/200
j = y*screen_h/200
else
i = x*screen_w/320
j = y*screen_w/320
end
return math.floor(i), math.floor(j)
end
-- return the Color @(x,y) in linear space (0-255)
-- corresonding to the thomson screen (x in 0-319,
-- y in 0-199)
local function getLinearPixel(x,y)
local with_cache = true
if not self._getLinearPixel then self._getLinearPixel = {} end
local k=x+y*thomson.w
local p = self._getLinearPixel[k]
if not p then
local x1,y1 = thom2screen(x,y)
local x2,y2 = thom2screen(x+1,y+1)
if x2==x1 then x2=x1+1 end
if y2==y1 then y2=y1+1 end
p = Color:new(0,0,0);
for j=y1,y2-1 do
for i=x1,x2-1 do
p:add(getLinearPictureColor(i,j))
end
end
p:div((y2-y1)*(x2-x1)) --:floor()
if with_cache then self._getLinearPixel[k]=p end
end
return with_cache and p:clone() or p
end
-- MO5 mode
thomson.setMO5()
self.palette = getpalette(thomson.w,thomson.h,getLinearPixel)
-- compute levels from palette
self:setLevelsFromPalette()
-- convert picture
self:ccDither(thomson.w,thomson.h,
getLinearPixel, thomson.pset,
serpentine or true, function(y)
thomson.info("Converting...",
math.floor(y*100/thomson.h),"%")
end,true)
-- refresh screen
setpicturesize(thomson.w,thomson.h)
thomson.updatescreen()
thomson.savep()
finalizepicture()
end
end -- OstroDither