Adrien Destugues f4bfaeca89 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!
2017-03-21 21:48:39 +01:00

300 lines
7.1 KiB
Lua

-- bayer4_to8.lua : converts an image into BM16
-- mode for thomson machines (MO6,TO8,TO9,TO9+)
-- using bayer matrix and a special palette.
--
-- 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/>
-- This is my first code in lua, so excuse any bad
-- coding practice.
-- use a zig zag. If false (recommended value), this gives
-- a raster look and feel
local with_zig_zag = with_zig_zag or false
-- debug: displays histograms
local debug = false
-- enhance luminosity since our mode divide it by two
local enhance_lum = enhance_lum or true
-- use fixed levels (default=false, give better result)
local fixed_levels = fixed_levels or false
-- use void-and-cluster 8x8 matrix (default=false)
local use_vac = use_vac or false
-- get screen size
local screen_w, screen_h = getpicturesize()
run("lib/thomson.lua")
run("lib/color.lua")
run("lib/bayer.lua")
-- Converts thomson coordinates (0-159,0-99) into screen coordinates
local function thom2screen(x,y)
local i,j;
if screen_w/screen_h < 1.6 then
i = x*screen_h/100
j = y*screen_h/100
else
i = x*screen_w/160
j = y*screen_w/160
end
return math.floor(i), math.floor(j)
end
-- return the pixel @(x,y) in linear space corresonding to the thomson screen (x in 0-159, y in 0-99)
local function getLinearPixel(x,y)
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
local p,i,j = Color:new(0,0,0);
for i=x1,x2-1 do
for j=y1,y2-1 do
p:add(getLinearPictureColor(i,j))
end
end
return p:div((y2-y1)*(x2-x1)):floor()
end
--[[ make a bayer matrix
function bayer(matrix)
local m,n=#matrix,#matrix[1]
local r,i,j = {}
for j=1,m*2 do
local t = {}
for i=1,n*2 do t[i]=0; end
r[j] = t;
end
-- 0 3
-- 2 1
for j=1,m do
for i=1,n do
local v = 4*matrix[j][i]
r[m*0+j][n*0+i] = v-3
r[m*1+j][n*1+i] = v-2
r[m*1+j][n*0+i] = v-1
r[m*0+j][n*1+i] = v-0
end
end
return r;
end
--]]
-- dither matrix
local dither = bayer.make(4)
if use_vac then
-- vac8: looks like FS
dither = bayer.norm{
{35,57,19,55,7,51,4,21},
{29,6,41,27,37,17,59,45},
{61,15,53,12,62,25,33,9},
{23,39,31,49,2,47,13,43},
{3,52,8,22,36,58,20,56},
{38,18,60,46,30,5,42,28},
{63,26,34,11,64,16,54,10},
{14,48,1,44,24,40,32,50}
}
end
-- get color statistics
local stat = {};
function stat:clear()
self.r = {}
self.g = {}
self.b = {}
for i=1,16 do self.r[i] = 0; self.g[i] = 0; self.b[i] = 0; end
end
function stat:update(px)
local pc2to = thomson.levels.pc2to
local r,g,b=pc2to[px.r], pc2to[px.g], pc2to[px.b];
self.r[r] = self.r[r] + 1;
self.g[g] = self.g[g] + 1;
self.b[b] = self.b[b] + 1;
end
function stat:coversThr(perc)
local function f(stat)
local t=-stat[1]
for i,n in ipairs(stat) do t=t+n end
local thr = t*perc; t=-stat[1]
for i,n in ipairs(stat) do
t=t+n
if t>=thr then return i end
end
return 0
end
return f(self.r),f(self.g),f(self.b)
end
stat:clear();
for y = 0,99 do
for x = 0,159 do
stat:update(getLinearPixel(x,y))
end
thomson.info("Collecting stats...",y,"%")
end
-- enhance luminosity since our mode divide it by two
local gain = 1
if enhance_lum then
-- findout level that covers 98% of all non-black pixels
local max = math.max(stat:coversThr(.98))
gain = math.min(2,255/thomson.levels.linear[max])
if gain>1 then
-- redo stat with enhanced levels
-- messagebox('gain '..gain..' '..table.concat({stat:coversThr(.98)},','))
stat:clear();
for y = 0,99 do
for x = 0,159 do
stat:update(getLinearPixel(x,y):mul(gain):floor())
end
thomson.info("Enhancing levels..",y,"%")
end
end
end
-- find regularly spaced levels in thomson space
local levels = {}
function levels.compute(name, stat, num)
local tot, max = -stat[1],0;
for _,t in ipairs(stat) do
max = math.max(t,max)
tot = tot + t
end
local acc,full=-stat[1],0
for i,t in ipairs(stat) do
acc = acc + t
if acc>tot*.98 then
full=thomson.levels.linear[i]
break
end
end
-- sanity
if fixed_levels or full==0 then full=255 end
local res = {1}; num = num-1
for i=1,num do
local p = math.floor(full*i/num)
local q = thomson.levels.linear2to[p]
if q==res[i] and q<16 then q=q+1 end
if not fixed_levels and i<num then
if q>res[i]+1 and stat[q-1]>stat[q] then q=q-1 end
if q>res[i]+1 and stat[q-1]>stat[q] then q=q-1 end
-- 3 corrections? no need...
-- if q>res[i]+1 and stat[q-1]>stat[q] then q=q-1 end
end
res[1+i] = q
end
-- debug
if debug then
local txt = ""
for _,i in ipairs(res) do
txt = txt .. i .. " "
end
for i,t in ipairs(stat) do
txt = txt .. "\n" .. string.format("%s%2d:%3d%% ", name, i, math.floor(100*t/(tot+stat[1]))) .. string.rep('X', math.floor(23*t/max))
end
messagebox(txt)
end
return res
end
function levels.computeAll(stat)
levels.grn = levels.compute("GRN",stat.g,5)
levels.red = levels.compute("RED",stat.r,4)
levels.blu = levels.compute("BLU",stat.b,3)
end
levels.computeAll(stat)
-- put a pixel at (x,y) with dithering
local function pset(x,y,px)
local thr = dither[1+(y % #dither)][1+(x % #dither[1])]
local function dither(val,thr,lvls)
local i=#lvls
local a,b = thomson.levels.linear[lvls[i]],1e30
while i>1 and val<a do
i=i-1;
a,b=thomson.levels.linear[lvls[i]],a;
end
return i + ((val-a)>=thr*(b-a) and 0 or -1)
end
local r = dither(px.r, thr, levels.red);
local g = dither(px.g, thr, levels.grn);
local b = dither(px.b, thr, levels.blu);
local i = r + b*4
local j = g==0 and 0 or (11 + g)
if with_zig_zag and x%2==1 then
thomson.pset(x,y*2+0,j)
thomson.pset(x,y*2+1,i)
else
thomson.pset(x,y*2+0,i)
thomson.pset(x,y*2+1,j)
end
end
-- BM16 mode
thomson.setBM16()
-- define palette
for i=0,15 do
local r,g,b=0,0,0
if i<12 then
-- r = bit32.band(i,3)
-- b = bit32.rshift(i,2)
b = math.floor(i/4)
r = i-4*b
else
g = i-11
end
r,g,b=levels.red[r+1],levels.grn[g+1],levels.blu[b+1]
thomson.palette(i,b*256+g*16+r-273)
end
-- convert picture
for y = 0,99 do
for x = 0,159 do
pset(x,y, getLinearPixel(x,y):mul(gain):floor())
end
thomson.info("Converting...",y,"%")
end
-- refresh screen
setpicturesize(320,200)
thomson.updatescreen()
finalizepicture()
-- save picture
do
local function exist(file)
local f=io.open(file,'rb')
if not f then return false else io.close(f); return true; end
end
local name,path = getfilename()
local mapname = string.gsub(name,"%.%w*$","") .. ".map"
local fullname = path .. '/' .. mapname
-- fullname = 'D:/tmp/toto.map'
local ok = not exist(fullname)
if not ok then
selectbox("Ovr " .. mapname .. "?", "Yes", function() ok = true; end, "No", function() ok = false; end)
end
if ok then thomson.savep(fullname) end
end