-
Notifications
You must be signed in to change notification settings - Fork 0
Advanced
A S H edited this page Mar 23, 2026
·
2 revisions
- Check available backends:
ashwal --list-backends - Choose backend:
ashwal --img image.jpg --backend libimagequant - Post-process:
ashwal --img image.jpg --script ~/.local/bin/update-theme.sh - Batch processing:
for img in ~/Pictures/wallpapers/*.{jpg,png,jpeg}; do
ashwal --img "$img" --quiet
doneashwal now supports custom backends using Lua scripts. This allows you to implement your own color quantization algorithms or image processing techniques.
To create a custom backend:
-
Create a Lua script with a
Main(image_path)function that returns a table of 16 colors, each as{r, g, b}wherer,g,bare integers 0-255. -
Place the script in
~/.config/ashwal/backends/(the directory will be created if it doesn't exist). -
Use the backend by its name (script filename without
.lua) with--backend <name>.
The script receives the image path and should process it to generate the palette.
Example
local ffi = require("ffi")
local function raw_to_pixels(data, size)
local expected = size * size * 3
if #data < expected then
return nil, string.format("expected >= %d bytes, got %d", expected, #data)
end
local buf = ffi.cast("const unsigned char*", data)
local pixels = {}
pixels[#pixels + size * size] = false -- preallocate
local idx = 1
for i = 0, size * size - 1 do
local base = i * 3
pixels[idx] = { buf[base], buf[base + 1], buf[base + 2] }
idx = idx + 1
end
return pixels
end
local function try_read_pixels_with(path, size)
local quoted = string.format('"%s"', path)
local conv = string.format("magick %s -resize %dx%d! -colorspace sRGB -depth 8 rgb:-", quoted, size, size)
local f = io.popen(conv, "r")
if not f then
return nil, "popen failed"
end
local data = f:read("*all")
f:close()
if not data or #data == 0 then
return nil, "no data"
end
return raw_to_pixels(data, size)
end
local function read_pixels(path, size)
local px, err = try_read_pixels_with(path, size)
if px then
return px
end
error("Could not read pixels via ImageMagick: " .. tostring(err))
end
local function dist2(a, b)
local dr = a[1] - b[1]
local dg = a[2] - b[2]
local db = a[3] - b[3]
return dr * dr + dg * dg + db * db
end
local function init_centroids_kpp(pixels, k)
local n = #pixels
if k > n then
k = n
end
local centroids = {}
local i1 = math.random(n)
centroids[1] = { pixels[i1][1], pixels[i1][2], pixels[i1][3] }
local function nearest_d2(p)
local best = math.huge
for i = 1, #centroids do
local d = dist2(p, centroids[i])
if d < best then
best = d
end
end
return best
end
while #centroids < k do
local dsum, d2s = 0.0, {}
for i = 1, n do
local d = nearest_d2(pixels[i])
d2s[i] = d
dsum = dsum + d
end
local r, acc = math.random() * dsum, 0.0
for i = 1, n do
acc = acc + d2s[i]
if acc >= r then
local p = pixels[i]
centroids[#centroids + 1] = { p[1], p[2], p[3] }
break
end
end
if #centroids < 2 then
break
end
end
return centroids
end
local function kmeans(pixels, k, max_iter)
max_iter = max_iter or 25
local n = #pixels
if n == 0 then
return {}
end
if k < 1 then
k = 1
elseif k > n then
k = n
end
math.randomseed(tonumber(tostring(os.clock()):gsub("%D", "")))
local centroids = init_centroids_kpp(pixels, k)
local assign = ffi.new("int[?]", n)
local changed, iter = true, 0
while changed and iter < max_iter do
iter, changed = iter + 1, false
-- assign step
for i = 1, n do
local p = pixels[i]
local best_k, best_d = 1, dist2(p, centroids[1])
for c = 2, k do
local d = dist2(p, centroids[c])
if d < best_d then
best_d, best_k = d, c
end
end
if assign[i - 1] ~= best_k then
assign[i - 1] = best_k
changed = true
end
end
if not changed then
break
end
-- update step
local sumR, sumG, sumB, count = {}, {}, {}, {}
for c = 1, k do
sumR[c], sumG[c], sumB[c], count[c] = 0, 0, 0, 0
end
for i = 1, n do
local c = assign[i - 1]
local p = pixels[i]
sumR[c] = sumR[c] + p[1]
sumG[c] = sumG[c] + p[2]
sumB[c] = sumB[c] + p[3]
count[c] = count[c] + 1
end
for c = 1, k do
if count[c] > 0 then
centroids[c][1] = sumR[c] / count[c]
centroids[c][2] = sumG[c] / count[c]
centroids[c][3] = sumB[c] / count[c]
else
local rp = pixels[math.random(n)]
centroids[c][1], centroids[c][2], centroids[c][3] = rp[1], rp[2], rp[3]
changed = true
end
end
end
-- build palette
local palette = {}
for c = 1, k do
local r = centroids[c][1] + 0.5
if r < 0 then
r = 0
elseif r > 255 then
r = 255
end
local g = centroids[c][2] + 0.5
if g < 0 then
g = 0
elseif g > 255 then
g = 255
end
local b = centroids[c][3] + 0.5
if b < 0 then
b = 0
elseif b > 255 then
b = 255
end
palette[#palette + 1] = {
math.floor(r),
math.floor(g),
math.floor(b),
}
end
return palette
end
local function sort_by_population(pixels, palette)
local k, counts = #palette, {}
for c = 1, k do
counts[c] = 0
end
for i = 1, #pixels do
local p, best_c, best_d = pixels[i], 1, dist2(p, palette[1])
for c = 2, k do
local d = dist2(p, palette[c])
if d < best_d then
best_d, best_c = d, c
end
end
counts[best_c] = counts[best_c] + 1
end
local idx = {}
for c = 1, k do
idx[c] = c
end
table.sort(idx, function(a, b)
return counts[a] > counts[b]
end)
local sorted = {}
for i = 1, k do
sorted[i] = palette[idx[i]]
end
return sorted
end
function Main(image_path)
local k, sample_size, max_iter = 16, 128, 25
local pixels = read_pixels(image_path, sample_size)
local palette = kmeans(pixels, k, max_iter)
palette = sort_by_population(pixels, palette)
while #palette > k do
table.remove(palette)
end
while #palette < k do
local last = palette[#palette]
palette[#palette + 1] = { last[1], last[2], last[3] }
end
return palette
end