630 lines
15 KiB
Lua
630 lines
15 KiB
Lua
-- 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 <http://www.gnu.org/licenses/>
|
|
|
|
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<dm then dm,c=d,i end
|
|
end
|
|
self[k] = c
|
|
end
|
|
return c
|
|
end
|
|
|
|
function OstroDither:_diffuse(linearColor,err, err0,err1,err2)
|
|
local c=self:getColorIndex(err:add(linearColor))
|
|
local M = Color.ONE
|
|
|
|
err:sub(self:_linearPalette(c))
|
|
local function d(rgb)
|
|
local e = err[rgb]
|
|
function f(a,c)
|
|
a=a+c*e
|
|
return a<-M and -M or
|
|
a> 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))<p:dist2(self:_linearPalette(c2)) and c1 or c2
|
|
end
|
|
|
|
local function _pset(x,y,c)
|
|
pset(x,y,c==c1-1 and c or -c2)
|
|
end
|
|
|
|
local findC1C2 = function(x,y,xs,err1)
|
|
-- collect the data we are working on
|
|
local gpl = {
|
|
clone = function(self)
|
|
local r={}
|
|
for i,v in ipairs(self) do
|
|
r[i] = {pix=v.pix:clone(),
|
|
err=v.err:clone()}
|
|
end
|
|
return r
|
|
end,
|
|
fill = function(self,dither)
|
|
for i=x,x+(dither.clash_size-1)*xs,xs do
|
|
table.insert(self,
|
|
{pix=getLinearPixel(i,y),
|
|
err=err1[i]})
|
|
end
|
|
table.insert(self, {pix=Color:new(),
|
|
err=Color:new()})
|
|
end
|
|
}
|
|
gpl:fill(self)
|
|
|
|
local histo = {
|
|
fill = function(self,dither)
|
|
local t=gpl:clone()
|
|
for i=1,#dither.palette do self[i] = {n=0,c=i} end
|
|
local back = dither.getColorIndex
|
|
dither.getColorIndex = OstroDither.getColorIndex
|
|
for i=1,dither.clash_size do
|
|
local c = dither:_diffuse(t[i].pix,t[i].err,
|
|
t[i+1].err)
|
|
self[c].n = self[c].n+1
|
|
end
|
|
dither.getColorIndex = back
|
|
table.sort(self, function(a,b)
|
|
return a.n>b.n or a.n==b.n and a.c<b.c end)
|
|
end,
|
|
get = function(self,i,...)
|
|
if i then
|
|
return self[i].c,self:get(...)
|
|
end
|
|
end,
|
|
num = function(self,i,...)
|
|
if i then
|
|
return self[i].n,self:num(...)
|
|
end
|
|
end,
|
|
sum = function(self,i,...)
|
|
return i and self[i].n+self:sum(...) or 0
|
|
end
|
|
}
|
|
histo:fill(self)
|
|
|
|
c1,c2=histo:get(1,2)
|
|
|
|
if not self:ccAcceptCouple(c1,c2) or histo:sum(1,2)<=self.clash_size-2 then
|
|
info(y)
|
|
local dm=1e30
|
|
local function eval()
|
|
if self:ccAcceptCouple(c1,c2) then
|
|
local d,t = 0,gpl:clone()
|
|
for i=1,self.clash_size do
|
|
local err=t[i].err
|
|
self:_diffuse(t[i].pix,err,t[i+1].err)
|
|
d = d + err.r^2 + err.g^2 + err.b^2
|
|
if d>dm 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 d<dm then dm,z=d,i end
|
|
end
|
|
c2=z
|
|
else
|
|
local a,b=c1,c2
|
|
for i=1,#self.palette-1 do c1=i
|
|
for j=1+i,#self.palette do c2=j
|
|
local d=eval()
|
|
if d<dm then dm,a,b=d,i,j end
|
|
end
|
|
end
|
|
c1,c2=a,b
|
|
end
|
|
end
|
|
end
|
|
|
|
local function _getLinearPixel(x,y,xs,err1)
|
|
if x%self.clash_size==(xs>0 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
|