{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "from PIL import Image\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "from bitarray import bitarray\n",
    "\n",
    "img = Image.open('../peacock.jpg')\n",
    "img = img.resize((128, 128))\n",
    "img = img.convert('P', palette=Image.ADAPTIVE, colors=16)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Playing around: median downscaling + bayer tiebreaker\n",
    "pixels = np.array(img)\n",
    "smaller = np.zeros((64, 64), dtype=np.uint8)\n",
    "bayer2x2 = np.array([[0, 3], [2, 1]])\n",
    "for y in range(64):\n",
    "    for x in range(64):\n",
    "        vs = pixels[y*2:y*2+2, x*2:x*2+2].flatten()\n",
    "        vs.sort()\n",
    "        if vs[0] == vs[2]:\n",
    "            v = vs[0]\n",
    "        elif vs[1] == vs[3]:\n",
    "            v = vs[1]\n",
    "        else:\n",
    "            bv = bayer2x2[y & 1][x & 1]\n",
    "            v = vs[bv]\n",
    "        smaller[y][x] = v\n",
    "halfimg = Image.fromarray(smaller, mode='P')\n",
    "halfimg.putpalette(img.getpalette())\n",
    "\n",
    "#plt.imshow(halfimg, interpolation='nearest')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<matplotlib.image.AxesImage at 0x7ce89033fec0>"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 33472 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "#x = bitarray()\n",
    "\n",
    "predictions = {}\n",
    "frontier = [0]*(img.width + img.height + 1 + 2) # 1 px corner, 2 px to cover the ends\n",
    "\n",
    "def get_diag_index(x, y):\n",
    "    return x - y + img.height + 1\n",
    "\n",
    "def get_neighborhood(x, y):\n",
    "    diag = get_diag_index(x, y)\n",
    "    return tuple(frontier[diag-1:diag+4])\n",
    "\n",
    "def get_prediction(x, y):\n",
    "    key = get_neighborhood(x, y)\n",
    "    if key in predictions:\n",
    "        return predictions[key]\n",
    "    else:\n",
    "        return 0\n",
    "\n",
    "pixels = np.array(img)\n",
    "for y in range(img.height):\n",
    "    for x in range(img.width):\n",
    "        value = (pixels[y, x] + 16 - get_prediction(x, y)) % 16\n",
    "        predictions[get_neighborhood(x, y)] = pixels[y, x]\n",
    "        frontier[get_diag_index(x, y) - 1] = pixels[y, x]\n",
    "        pixels[y, x] = value\n",
    "\n",
    "encoded = bitarray()\n",
    "run_value = pixels[0, 0]\n",
    "run_length = 0\n",
    "run_data = []\n",
    "\n",
    "def flush():\n",
    "    global run_length, run_value\n",
    "    run_data.append((run_value, run_length))\n",
    "    value = bitarray()\n",
    "    x = run_value\n",
    "    for _ in range(4):\n",
    "        value.append((x>>3) & 1)\n",
    "        x <<= 1\n",
    "    if run_length < 3:\n",
    "        for _ in range(run_length):\n",
    "            encoded.extend(value)\n",
    "    else:\n",
    "        for _ in range(3):\n",
    "            encoded.extend(value)\n",
    "        run = run_length - 3 + 1\n",
    "        encoded.extend(0 for _ in range(run.bit_length() - 1))\n",
    "        for i in range(run.bit_length() - 1, -1, -1):\n",
    "            encoded.append(run >> i & 1)\n",
    "    run_length = 0\n",
    "\n",
    "for y in range(img.height):\n",
    "    for x in range(img.width):\n",
    "        val = pixels[y, x]\n",
    "        if val == run_value:\n",
    "            run_length += 1\n",
    "        else:\n",
    "            flush()\n",
    "            run_value = val\n",
    "            run_length = 1\n",
    "flush()\n",
    "\n",
    "transformed = Image.fromarray(pixels, mode='P')\n",
    "transformed.putpalette(img.getpalette())\n",
    "plt.imshow(transformed, interpolation='nearest')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "bitarray('[encoded data, 42865 characters]')"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "encoded"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[(np.uint8(2), 1),\n",
       " (np.uint8(0), 4),\n",
       " (np.uint8(2), 1),\n",
       " (np.uint8(0), 7),\n",
       " (np.uint8(2), 1),\n",
       " (np.uint8(0), 10),\n",
       " (np.uint8(14), 1),\n",
       " (np.uint8(0), 1),\n",
       " (np.uint8(2), 1),\n",
       " (np.uint8(0), 6)]"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "run_data[:10]"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
