Add scripts to color-reduce pictures with constraints
Scripts for various Thomson machines, Oric and ZX. All the hard work of Samuel Devulder, with extensive research on color palette reduction algorithms, lots of testing, research and debugging. Thanks a lot!
This commit is contained in:
@@ -0,0 +1,629 @@
|
||||
-- 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
|
||||
Reference in New Issue
Block a user