Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Image to gameboy tile conversion

livebooks/image-encoding.livemd

Image to gameboy tile conversion

Mix.install([
  {:ex_png, "~> 1.0"},
  {:kino, "~> 0.9.4"},
  {:nx, "~> 0.5.3"}
])

Convert image to 2 bit grayscale

# data = img.pixels |> List.flatten() |> Stream.map(fn <> -> trunc((r+g+b) / 3) end) |> Stream.map(fn value -> trunc(value / 255 * 4) end) |> Stream.chunk_every(4) |> Enum.reduce(<<>>, fn [a,b,c,d], acc -> acc <> <> end)
width = 160
height = 144
file_input = Kino.Input.image("Image", format: :rgb, size: {height, width}, fit: :crop)
image = Kino.Input.read(file_input)
grayscale =
  image.data
  |> :binary.bin_to_list()
  |> Enum.chunk_every(3)
  |> Enum.map(fn [r, g, b] -> 3 - trunc((r + g + b) / 3 / 200 * 3) end)
Enum.uniq(grayscale)
grayscale |> Nx.tensor() |> Nx.reshape({height, width}) |> Nx.to_heatmap()

Convert grayscale image to tile data

tile_size = 8
tiles_per_row = div(width, tile_size)
rows = grayscale |> Enum.chunk_every(tiles_per_row * tile_size * tile_size)
length(rows)
length(Enum.at(rows, 0))
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)
length(tiles)

Convert tile data to 2bpp

import Bitwise

tile_bytes =
  tiles
  # |> Enum.take(1)
  |> 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)
IO.puts(inspect(tile_bytes |> IO.iodata_to_binary(), limit: :infinity))