Image to gameboy tile conversion
Mix . install ( [
{ :ex_png , "~> 1.0" } ,
{ :kino , "~> 0.11.2" } ,
{ :nx , "~> 0.6.3" }
] )
width = 160
height = 140
tile_size = 8
tiles_per_row = div ( width , tile_size )
Convert image to 2 bit grayscale
file_input = Kino.Input . image ( "Image" , format: :rgb , size: { height , width } , fit: :crop )
image = Kino.Input . read ( file_input )
image_path = Kino.Input . file_path ( image . file_ref )
image_data = File . read! ( image_path )
rgb =
image_data
|> :binary . bin_to_list ( )
|> Enum . chunk_every ( 3 )
grayscale =
rgb
|> Enum . map ( fn [ r , g , b ] -> div ( r + g + b , 3 ) end )
{ min_value , max_value } = Enum . min_max ( grayscale )
quantize = fn value ->
trunc ( value / 255.0 * 3 )
end
clamp = fn value , lower , upper ->
max ( min ( value , upper ) , lower )
end
update_row = fn rows , y , x , new_value ->
List . update_at ( rows , y , fn row -> List . update_at ( row , x , fn _ -> new_value end ) end )
end
diffuse_error = fn rows , error , x , y ->
factors = [ { 1 , 0 , 7 / 16 } , { 0 , 1 , 5 / 16 } , { - 1 , 1 , 3 / 16 } , { 1 , 1 , 1 / 16 } ]
Enum . reduce ( factors , rows , fn { dx , dy , factor } , acc_rows ->
new_x = x + dx
new_y = y + dy
if new_y < length ( acc_rows ) and new_x >= 0 and new_x < length ( Enum . at ( acc_rows , new_y ) ) do
old_value = Enum . at ( Enum . at ( acc_rows , new_y ) , new_x )
new_value = clamp . ( old_value + round ( error * factor ) , 0 , 255 )
update_row . ( acc_rows , new_y , new_x , new_value )
else
acc_rows
end
end )
end
diffuse_pixel = fn value , rows , x , y ->
new_pixel = quantize . ( value )
error = value - new_pixel * 255 / 3.0
updated_rows = diffuse_error . ( rows , error , x , y )
{ new_pixel , updated_rows }
end
apply_dithering = fn rows ->
Enum . reduce ( 0 .. ( length ( rows ) - 1 ) , rows , fn y , acc_rows ->
Enum . reduce ( 0 .. ( length ( Enum . at ( rows , y ) ) - 1 ) , acc_rows , fn x , acc_inner_rows ->
{ new_pixel , updated_rows } =
diffuse_pixel . ( Enum . at ( Enum . at ( acc_inner_rows , y ) , x ) , acc_inner_rows , x , y )
update_row . ( updated_rows , y , x , new_pixel )
end )
end )
end
offsets = Nx . tensor ( [ [ 1 , 0 ] , [ - 1 , 1 ] , [ 0 , 1 ] , [ 1 , 1 ] ] )
factors = Nx . tensor ( [ 7 , 3 , 5 , 1 ] ) |> Nx . divide ( 16 )
# offsets = Nx.tensor([[1, 0], [-1, 1], [0, 1], [1, 1]])
offsets = [ { 1 , 0 } , { - 1 , 1 } , { 0 , 1 } , { 1 , 1 } ]
# factors = Nx.tensor([7, 3, 5, 1]) |> Nx.divide(16)
# |> Enum.map(&(&1 / 16))
factors = [ 7.0 / 16.0 , 3.0 / 16.0 , 5.0 / 16.0 , 1.0 / 16.0 ]
dither = fn data , w , h ->
data =
for y <- 0 .. ( h - 1 ) , x <- 0 .. ( w - 1 ) , into: % { } do
{ { x , y } , Enum . at ( data , y * w + x ) }
end
data =
for y <- 0 .. ( h - 1 ) , x <- 0 .. ( w - 1 ) , reduce: data do
data ->
old = Map . get ( data , { x , y } )
new = ( old / 255.0 ) |> round ( )
err = ( old - new ) |> round ( )
data = Map . put ( data , { x , y } , new )
offsets
|> Enum . map ( fn { dx , dy } -> { x + dx , y + dy } end )
|> Enum . zip ( factors )
|> Enum . filter ( fn { { x , y } , _factor } -> x >= 0 && x < w && y >= 0 && y < h end )
|> Enum . reduce ( data , fn { key , factor } , acc ->
Map . update! ( acc , key , & ( & 1 + err * factor ) )
end )
end
for y <- 0 .. ( h - 1 ) , x <- 0 .. ( w - 1 ) , into: [ ] do
Map . get ( data , { x , y } )
end
end
new_dithered =
grayscale
# |> Nx.tensor()
# |> Nx.reshape({height, width})
|> dither . ( width , height )
new_dithered |> Enum . uniq ( )
dither_nx = fn tensor ->
# offsets = Nx.tensor([[0, 1], [1, -1], [1, 0], [1, 1]])
right = [ 0 , 1 ]
bottom_left = [ 1 , - 1 ]
bottom = [ 1 , 0 ]
bottom_right = [ 1 , 1 ]
offsets = Nx . tensor ( [ right , bottom_left , bottom , bottom_right ] )
factors = Nx . tensor ( [ 7 , 3 , 5 , 1 ] ) |> Nx . divide ( 16 )
{ h , w } = Nx . shape ( tensor )
# |> Nx.as_type(:u8)
quantized = tensor |> Nx . divide ( 255 ) |> Nx . round ( )
# |> Nx.round()
error = tensor |> Nx . subtract ( quantized )
dbg ( quantized )
for y <- 1 .. ( h - 2 ) , x <- 1 .. ( w - 2 ) , reduce: tensor do
pixels ->
idx = Nx . tensor ( [ y , x ] )
indices =
offsets
|> Nx . add ( Nx . tensor ( [ y , x ] ) )
|> Nx . to_list ( )
err =
error
|> Nx . gather ( Nx . tensor ( [ y , x ] ) )
|> Nx . multiply ( factors )
|> Nx . to_list ( )
new =
quantized
|> Nx . gather ( idx )
pixels =
pixels
|> Nx . indexed_put ( Nx . tensor ( [ y , x ] ) , new )
indices
|> Enum . zip ( err )
|> Enum . reduce ( pixels , fn { idx , e } , acc ->
Nx . indexed_add ( acc , Nx . tensor ( idx ) , e )
end )
pixels
# |> Nx.indexed_add(indices, err)
end
# |> Nx.as_type(:u8)
end
nx_dithered =
grayscale
|> Nx . tensor ( )
|> Nx . reshape ( { height , width } )
|> dither_nx . ( )
|> Nx . slice ( [ 1 , 1 ] , [ 138 , 158 ] )
# new_dithered |> Enum.uniq()
dithered =
grayscale
|> Enum . chunk_every ( 160 )
|> apply_dithering . ( )
|> List . flatten ( )
rgb_img =
rgb
|> IO . iodata_to_binary ( )
|> Nx . from_binary ( :u8 )
|> Nx . reshape ( { height , width , 3 } )
|> Kino.Image . new ( )
grayscale_img =
grayscale
|> IO . iodata_to_binary ( )
|> Nx . from_binary ( :u8 )
|> Nx . reshape ( { height , width , 1 } )
|> Kino.Image . new ( )
dithered_img =
dithered
|> IO . iodata_to_binary ( )
|> Nx . from_binary ( :u8 )
|> Nx . reshape ( { height , width , 1 } )
|> Nx . multiply ( div ( 255 , 3 ) )
|> Kino.Image . new ( )
new_dithered_img =
new_dithered
|> Enum . map ( & trunc / 1 )
|> IO . iodata_to_binary ( )
|> Nx . from_binary ( :u8 )
|> Nx . reshape ( { height , width , 1 } )
|> Nx . multiply ( div ( 255 , 3 ) )
|> Kino.Image . new ( )
# nx_dithered_img =
# nx_dithered
# |> Nx.round()
# |> Nx.as_type(:u8)
# |> Nx.reshape({140-2, 160-2, 1})
# |> Nx.multiply(div(255, 1))
# |> Kino.Image.new()
Kino.Layout . grid (
[
Kino.Shorts . text ( "RGB" ) ,
# Kino.Shorts.text("Grayscale"),
Kino.Shorts . text ( "Dithered" ) ,
Kino.Shorts . text ( "New Dithered" ) ,
rgb_img ,
# grayscale_img,
dithered_img ,
new_dithered_img
# nx_dithered_img
] ,
columns: 3
)
Convert dithered image to tile data
rows = dithered |> Enum . chunk_every ( tiles_per_row * tile_size * tile_size )
tiles =
rows
|> Enum . flat_map ( fn row ->
tile_rows = row |> Enum . chunk_every ( 8 )
Enum . map ( 0 .. ( tiles_per_row - 1 ) , fn offset ->
tile_rows |> Enum . drop ( offset ) |> Enum . take_every ( tiles_per_row )
end )
end )
Convert tile data to 2bpp
import Bitwise
tile_bytes =
tiles
|> Enum . map ( fn rows ->
Enum . map ( rows , fn row ->
row
|> Enum . with_index ( )
|> Enum . reduce ( [ 0 , 0 ] , fn { pixel , i } , [ lo , hi ] ->
case pixel do
0 -> [ lo , hi ]
1 -> [ lo ||| 1 <<< ( 7 - i ) , hi ]
2 -> [ lo , hi ||| 1 <<< ( 7 - i ) ]
3 -> [ lo ||| 1 <<< ( 7 - i ) , hi ||| 1 <<< ( 7 - i ) ]
end
end )
end )
end )
IO . iodata_length ( tile_bytes )
tiles_binary = tile_bytes |> IO . iodata_to_binary ( )
IO . inspect ( tiles_binary , limit: :infinity , width: :infinity )
Kino.Download . new ( fn -> tile_bytes |> IO . iodata_to_binary ( ) end ,
label: "Image Data" ,
filename: "image.bin"
)