-- 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