{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "2b892544-c4ac-4559-b5c0-1e08159b295a",
   "metadata": {},
   "source": [
    "# Преобразование Фурье в изображениях\n",
    "С помощью этого ноутбука вы на практике познакомитесь с преобразованием Фурье для изображений, а также с его различными способами применения, например:\n",
    "* Для ускорения применения сверток («Теорема о свертке»)\n",
    "* Для склейки изображений\n",
    "* Для поиска фрагмента картинки на полном изображении\n",
    "\n",
    "**Перед началом попытайтесь ответить на два вопроса:**\n",
    "* Для чего применяются преобразования Фурье в математике?\n",
    "* Зачем нужны преобразования Фурье в изображениях?\n",
    "\n",
    "_Подробнее про дискретное преобразование Фурье можно прочитать на [Википедии](https://en.wikipedia.org/wiki/Discrete_Fourier_transform)._"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "823ded06-2bbb-4670-927d-aa79b85b56b8",
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "from scipy import signal\n",
    "from skimage import filters, morphology\n",
    "from skimage.color import gray2rgb, rgb2gray\n",
    "from skimage.io import imread\n",
    "\n",
    "# Increase these if figures appear small\n",
    "plt.rcParams[\"figure.figsize\"] = fx, fy = (14.08, 10.35)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c263b4bb-6ce0-44cb-95ee-c6ce135f349b",
   "metadata": {},
   "source": [
    "## Визуализация преобразования Фурье\n",
    "\n",
    "В данном разделе вам будет предложено реализовать преобразование Фурье для изображений с помощью готовых функций в NumPy, а также проанализировать полученные визуализации."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "648ec61a",
   "metadata": {},
   "outputs": [],
   "source": [
    "!curl -O 'https://courses.cv-gml.ru/storage/seminars/fourier-transform/images.zip'\n",
    "!unzip -o images.zip"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1269c619-b53c-4ced-b075-a941752f9b4b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Загрузим изображения\n",
    "imgs = {\n",
    "    \"tiger\": imread(\"images/tiger.jpg\"),\n",
    "    \"msu\": imread(\"images/msu.jpg\"),\n",
    "    \"cloth\": imread(\"images/cloth.jpg\"),\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aaeef774-9072-49b6-b9f9-6f532e1fb9b2",
   "metadata": {},
   "outputs": [],
   "source": [
    "def show_imgs_wrapper(*imgs):\n",
    "    for img_names, imgset in imgs:\n",
    "        if not isinstance(imgset, (list, tuple)):\n",
    "            img_names = (img_names,)\n",
    "            imgset = (imgset,)\n",
    "\n",
    "        num = len(imgset)\n",
    "        cbars = [img.ndim == 2 or img.shape[-1] == 1 for img in imgset]\n",
    "        num += sum(cbars)\n",
    "        cbar_width = 0.05\n",
    "        widths = [r for ax in cbars for r in ([1, cbar_width] if ax else [1])]\n",
    "        fig, axs = plt.subplots(\n",
    "            nrows=1,\n",
    "            ncols=num,\n",
    "            gridspec_kw={\"wspace\": 0.01, \"hspace\": 0.01},\n",
    "            width_ratios=widths,\n",
    "            squeeze=True,\n",
    "            layout=\"constrained\",\n",
    "            figsize=(fx, fy / len(imgset)),\n",
    "        )\n",
    "        axs = [axs] if num <= 1 else axs\n",
    "\n",
    "        i, j = 0, 0\n",
    "        while i < num:\n",
    "            ax = axs[i]\n",
    "            i += 1\n",
    "\n",
    "            img = imgset[j]\n",
    "            name = img_names[j]\n",
    "            j += 1\n",
    "\n",
    "            ax.set_title(name)\n",
    "            cbar = ax.imshow(img)\n",
    "            ax.axis(\"off\")\n",
    "\n",
    "            if img.ndim == 2 or img.shape[-1] == 1:\n",
    "                ax = axs[i]\n",
    "                i += 1\n",
    "                fig.colorbar(cbar, cax=ax)\n",
    "\n",
    "        plt.show(fig)\n",
    "        plt.close(fig)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8cf1212e-29e5-4775-a652-48632eb903d8",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Визуализируем изображения\n",
    "show_imgs_wrapper(*imgs.items())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "de15a7e7",
   "metadata": {},
   "source": [
    "### Задание: реализуйте функцию визуализации преобразования Фурье для изображения"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0399a3fe-8518-41aa-9491-5ed5178d7085",
   "metadata": {},
   "outputs": [],
   "source": [
    "def discrete_fourier_transform(img, vis=True):\n",
    "    # RGB -> Gray\n",
    "    if img.ndim == 3:\n",
    "        if img.shape[-1] == 3:\n",
    "            img = rgb2gray(img)\n",
    "        else:\n",
    "            img = img.squeeze(-1)\n",
    "\n",
    "    # Применим преобразование Фурье из NumPy\n",
    "    fft = np.fft.fft2(img)\n",
    "\n",
    "    if vis:\n",
    "        # Отцентруем\n",
    "        fft_centered = np.fft.fftshift(fft)\n",
    "\n",
    "        # Применим логарифмическое преобразование для наглядности визуализации\n",
    "        fft_normalized = np.log1p(abs(fft_centered))\n",
    "\n",
    "        return fft_normalized\n",
    "    else:\n",
    "        return fft"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7146488c-10a0-471c-b760-080217862a16",
   "metadata": {},
   "outputs": [],
   "source": [
    "imgs_fft = {\n",
    "    (name, f\"{name}_fft\"): (gray2rgb(rgb2gray(image)), discrete_fourier_transform(image))\n",
    "    for name, image in imgs.items()\n",
    "}\n",
    "show_imgs_wrapper(*imgs_fft.items())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "051dfb43-be3b-45ab-bda5-dc1e23d56469",
   "metadata": {},
   "source": [
    "### Вопросы:\n",
    "* Чем отличаются друг от друга полученные визуализации преобразования Фурье?\n",
    "* Какие особенности вы заметили в визуализациях и с чем они связаны?\n",
    "\n",
    "<br/>\n",
    "Задание: Найдите основную частоту, которая присутствует на этом изображении.\n",
    "<img src=\"https://courses.cv-gml.ru/storage/seminars/fourier-transform/AliasingSines_empty.svg\">\n",
    "\n",
    "<br/>\n",
    "<details>\n",
    "    Вы все конечно нашли именно эту частоту, да?\n",
    "    <img src=\"https://courses.cv-gml.ru/storage/seminars/fourier-transform/AliasingSines_red.svg\">\n",
    "</details>\n",
    "\n",
    "<br/>\n",
    "<details>\n",
    "<ul>\n",
    "    <li>\n",
    "        <a href=\"https://en.wikipedia.org/wiki/Nyquist%E2%80%93Shannon_sampling_theorem\">Nyquist–Shannon sampling theorem</a>\n",
    "        (она же - Теорема Котельникова)\n",
    "    </li>\n",
    "    <br/>\n",
    "    <li>\n",
    "        <a href=\"https://en.wikipedia.org/wiki/Aliasing\">Aliasing</a>\n",
    "        <img src=\"https://courses.cv-gml.ru/storage/seminars/fourier-transform/AliasingSines.svg\">\n",
    "    </li>\n",
    "    <br/>\n",
    "    <li>\n",
    "        <a href=\"https://en.wikipedia.org/wiki/Wagon-wheel_effect\">Wagon-wheel effect</a>\n",
    "    </li>\n",
    "</ul>\n",
    "\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8ec6a709",
   "metadata": {},
   "source": [
    "## Теорема о свертке\n",
    "\n",
    "Применение свертки с ядром $K$ к изображению $I$ эквивалентно обратному преобразованию Фурье к поэлементному умножению изображения $I_{Fourier}$ на $K_{Fourier}$, где $I_{Fourier}$ — результат преобразования Фурье для изображения $I$, а $K_{Fourier}$ — результат преобразования Фурье для ядра $K$. _Это и есть «Теорема о свертке»._\n",
    "\n",
    "**Но в чем же достоинство данной теоремы?**\n",
    "\n",
    "Пусть изображение $I$ имеет размер $N \\times N$, а ядро K — $K \\times K$. Тогда, вычисление сверток для всего изображения будет иметь сложность $O(N^2 K^2)$. Если же применить «Теорему о свертке», то сложность составит $O(N^2 log N)$, так как для прямого и обратного преобразований Фурье можно использовать алгоритм [Быстрого_преобразования_Фурье](https://ru.wikipedia.org/wiki/Быстрое_преобразование_Фурье). Таким образом, при большом размере ядра фильтра будем иметь существенный прирост производительности."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "12aed78f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Загрузим изображение\n",
    "img = imread(\"images/cat.jpg\", as_gray=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "af0b6c4a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Визуализируем изображение\n",
    "show_imgs_wrapper([\"cat\", gray2rgb(img)])\n",
    "# (это тоже тигр, просто маленький)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b574a915",
   "metadata": {},
   "source": [
    "### Задание:\n",
    "\n",
    "Напишите функцию применения фильтра Гаусса к изображению с использованием «Теоремы о свертке», для этого:\n",
    "\n",
    "1. Примените преобразование Фурье к изображению\n",
    "2. Расширьте нулями и приведите к нужному формату ядро фильтра Гаусса\n",
    "3. Примените к полученному «раширенному» ядру преобразование Фурье\n",
    "4. Перемножьте результат применения преобразования Фурье к изображению и к «расширенному» ядру\n",
    "5. Примените обратное преобразование Фурье"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "86b69e84",
   "metadata": {},
   "outputs": [],
   "source": [
    "def pad_kernel(kernel, target):\n",
    "    th, tw = target\n",
    "    kh, kw = kernel.shape[:2]\n",
    "    ph, pw = th - kh, tw - kw\n",
    "\n",
    "    padding = [\n",
    "        ((ph + 1) // 2, ph // 2),\n",
    "        ((pw + 1) // 2, pw // 2),\n",
    "    ]\n",
    "    kernel = np.pad(kernel, padding)\n",
    "\n",
    "    assert kernel.shape[:2] == target\n",
    "    return kernel\n",
    "\n",
    "\n",
    "def conv_theorem(img, kernel):\n",
    "    \"\"\"conv(img, kernel) == F^-1 (F(img) * F(kernel))\"\"\"\n",
    "\n",
    "    # Используя discrete_fourier_transform() с vis=False, примените преобразование Фурье к изображению\n",
    "    img_fourier = discrete_fourier_transform(img, vis=False)\n",
    "\n",
    "    # Расширьте полученное ядро до размера изображения\n",
    "    kernel_padded = pad_kernel(kernel, img.shape[:2])\n",
    "\n",
    "    # Приведите ядро к правильному формату\n",
    "    kernel_unshifted = np.fft.ifftshift(kernel_padded)\n",
    "\n",
    "    # Используя discrete_fourier_transform() с vis=False, примените преобразование Фурье к «раширенному» ядру\n",
    "    kernel_fourier = discrete_fourier_transform(kernel_unshifted, vis=False)\n",
    "\n",
    "    # Перемножьте результат применения преобразования Фурье к изображению и к «расширенному» ядру\n",
    "    result = img_fourier * kernel_fourier\n",
    "\n",
    "    # Примените обратное преобразование Фурье\n",
    "    result = np.fft.ifft2(result).real\n",
    "\n",
    "    return result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6550908a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Example kernels:\n",
    "size = 63\n",
    "identity_kernel = np.zeros((size, size))\n",
    "identity_kernel[size // 2, size // 2] = 1\n",
    "# or\n",
    "unsharp_kernel = np.array(\n",
    "    [\n",
    "        [0, -1, 0],\n",
    "        [-1, 5, -1],\n",
    "        [0, -1, 0],\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2b7f48dc",
   "metadata": {},
   "outputs": [],
   "source": [
    "kernel = unsharp_kernel\n",
    "blurred_theo = conv_theorem(img, kernel)\n",
    "blurred_conv = signal.convolve2d(img, kernel, mode=\"same\", boundary=\"wrap\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a876c0b6",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Визуализируем результат\n",
    "F = r\"\\mathcal{F\\,}\"\n",
    "show_imgs_wrapper(\n",
    "    ([\"I\", \"K\"], [gray2rgb(img), kernel]),\n",
    "    (\n",
    "        [r\"I $\\ast$ K\", rf\"${F}^{{-1}}({F}(I) \\cdot {F}(K))$\"],\n",
    "        [gray2rgb(blurred_conv.clip(0, 1)), gray2rgb(blurred_theo.clip(0, 1))],\n",
    "    ),\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d0e34c8d",
   "metadata": {},
   "outputs": [],
   "source": [
    "help(signal.convolve2d)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "683d9d6a",
   "metadata": {},
   "outputs": [],
   "source": [
    "abs(blurred_conv - blurred_theo).max()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3df8614e",
   "metadata": {},
   "source": [
    "### Задание:\n",
    "Проанализируйте зависимость скорости работы «стандартного» (с помощью сверток) применения фильтра Гаусса от размера ядра фильтра, аналогичный анализ проведите для случая применения фильтра Гаусса к изображению с использованием «Теоремы о свертке». Также сделайте сравнение графиков для «стандартного» метода и метода, основанного на теореме, о чем можно по ним судить?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a954293b",
   "metadata": {},
   "outputs": [],
   "source": [
    "import collections\n",
    "import time\n",
    "\n",
    "kernels = []\n",
    "kernel_sizes = list(range(5, 50, 5))\n",
    "for kernel_size in kernel_sizes:\n",
    "    kernels.append(np.random.randn(kernel_size, kernel_size))\n",
    "\n",
    "results = collections.defaultdict(list)\n",
    "experiments = {\n",
    "    \"Standard Convolution\": signal.convolve2d,\n",
    "    \"Convolution Theorem\": conv_theorem,\n",
    "}\n",
    "for experiment_name, function in experiments.items():\n",
    "    for kernel in kernels:\n",
    "        measurements = []\n",
    "        for _ in range(10):\n",
    "            start_time = time.time()\n",
    "            function(img, kernel)\n",
    "            end_time = time.time()\n",
    "            measurements.append(end_time - start_time)\n",
    "\n",
    "        results[experiment_name].append(sum(measurements) / len(measurements))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c1e9d9ae",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots()\n",
    "ax.set_xlabel(\"Kernel Size\")\n",
    "ax.set_ylabel(\"Time (mean)\")\n",
    "\n",
    "for experiment_name, function in experiments.items():\n",
    "    ax.plot(kernel_sizes, results[experiment_name], label=experiment_name)\n",
    "ax.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a6186d9e",
   "metadata": {},
   "source": [
    "## Фильтр Гаусса и box-фильтр\n",
    "\n",
    "Данные фильтры применяются для размытия изображений и, как следствие, для уменьшения шума на картинке. Более того, фильтр Гаусса часто используется для предварительной обработки в алгоритмах компьютерного зрения с целью улучшения структуры изображения в различных масштабах.\n",
    "\n",
    "В данном разделе предлагается визуализировать ядра данных фильтров, а также результаты применения сверток с этими фильтрами для изображений. Но самым важным моментом, который будет затронут в текущем пункте, является визуализация Фурье-образов фильтра Гаусса и box-фильтра. Это поможет вам понять принципы работы рассматриваемых фильтров.\n",
    "\n",
    "_Подробнее про фильтр Гаусса можно прочитать [здесь](https://ru.wikipedia.org/wiki/Размытие_по_Гауссу), про box-фильтр — [здесь](https://en.wikipedia.org/wiki/Box_blur)._"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eff80227",
   "metadata": {},
   "source": [
    "### Задание: реализуйте функцию, возвращающую ядро фильтра Гаусса"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "43963c65",
   "metadata": {},
   "outputs": [],
   "source": [
    "def gauss_filter_kernel(sigma, kernel_size=63):\n",
    "    # Используйте signal.windows.gaussian\n",
    "    if True:\n",
    "        gkern1d = signal.windows.gaussian(kernel_size, std=sigma)\n",
    "    else:\n",
    "        # или то же самое \"руками\" по формуле\n",
    "        limit = (kernel_size - 1) / (2 * sigma)\n",
    "        spread = np.linspace(-limit, limit, kernel_size)\n",
    "        gkern1d = np.exp(-0.5 * spread * spread)\n",
    "\n",
    "    # 1) Separable filters (general case)\n",
    "    # If there exists such a pair of smaller kernels F1 and F2,\n",
    "    # such that F_LARGE == conv(F1, F2), then due to associativity\n",
    "    #     conv(I, F_LARGE) == conv(I, conv(F1, F2)) == conv(conv(I, F1), F2)\n",
    "    #\n",
    "    # For the 2d gaussian kernel the following decomposition exists:\n",
    "    #\n",
    "    # gkern2d = signal.convolve2d(\n",
    "    #     gkern1d[None, :], gkern1d[:, None],\n",
    "    #     mode=\"full\", boundary=\"fill\",\n",
    "    # )\n",
    "\n",
    "    # 2) In addition, for any pair of kernels F1, F2\n",
    "    # of shapes (N, 1) and (1, M) the following is true:\n",
    "    #     conv(F1, F2) = F1 * F2\n",
    "    # So, in this case, the 2d gaussian kernel can also be expressed as:\n",
    "    gkern2d = gkern1d[None, :] * gkern1d[:, None]\n",
    "\n",
    "    gkern2d /= gkern2d.sum()\n",
    "    return gkern2d"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2c3f50d7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Визуализация ядра фильтра Гаусса\n",
    "sigma_vals = (2, 5, 11)\n",
    "gauss_kernels = [gauss_filter_kernel(sigma) for sigma in sigma_vals]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e58c9ede",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_imgs_wrapper(\n",
    "    ([f\"gauss_filter_kernel({sigma})\" for sigma in sigma_vals], gauss_kernels),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dba941f2",
   "metadata": {},
   "source": [
    "### Задание: реализуйте функцию, возвращающую ядро box-фильтра"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eb85c90e",
   "metadata": {},
   "outputs": [],
   "source": [
    "def box_filter_kernel(kernel_size, pad_until=63):\n",
    "    kernel = np.ones((kernel_size, kernel_size))\n",
    "    kernel /= kernel.sum()\n",
    "\n",
    "    if pad_until is not None:\n",
    "        kernel = pad_kernel(kernel, (pad_until, pad_until))\n",
    "\n",
    "    return kernel"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "502f23a9",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Визуализация ядра box-фильтра\n",
    "box_vals = (3, 15, 29)\n",
    "box_kernels = [box_filter_kernel(size) for size in box_vals]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a6357bed",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_imgs_wrapper(\n",
    "    ([f\"box_filter_kernel({size})\" for size in box_vals], box_kernels),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "45e3acc5",
   "metadata": {},
   "source": [
    "### Задание: релизуйте фильтр Гаусса и рассмотрите результаты его применения"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b2860bcb",
   "metadata": {},
   "outputs": [],
   "source": [
    "def gauss_blur(img, sigma, kernel_size=63):\n",
    "    # Используйте conv_theorem\n",
    "    kernel = gauss_filter_kernel(sigma, kernel_size)\n",
    "    return conv_theorem(img, kernel)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "552542c7",
   "metadata": {},
   "outputs": [],
   "source": [
    "gauss_blurs = [img] + [gauss_blur(img, sigma) for sigma in sigma_vals]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "243398ed",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Визуализируем размытие с помощью фильтра Гаусса\n",
    "show_imgs_wrapper(\n",
    "    (\n",
    "        [\"original\"] + [f\"gauss_blur({sigma})\" for sigma in sigma_vals],\n",
    "        [gray2rgb(I) for I in gauss_blurs],\n",
    "    ),\n",
    "    (\n",
    "        [\"original_fft\"] + [f\"gauss_blur_fft({sigma})\" for sigma in sigma_vals],\n",
    "        [discrete_fourier_transform(I) for I in gauss_blurs],\n",
    "    ),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "80b4659b",
   "metadata": {},
   "source": [
    "### Задание: релизуйте box-фильтр и рассмотрите результаты его применения"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ff62aa1c",
   "metadata": {},
   "outputs": [],
   "source": [
    "def box_blur(img, kernel_size):\n",
    "    # Используйте conv_theorem\n",
    "    kernel = box_filter_kernel(kernel_size)\n",
    "    return conv_theorem(img, kernel)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "34c2a2fc",
   "metadata": {},
   "outputs": [],
   "source": [
    "box_blurs = [img] + [box_blur(img, size) for size in box_vals]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "96480213",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Визуализируем размытие с помощью box-фильтра\n",
    "show_imgs_wrapper(\n",
    "    (\n",
    "        [\"original\"] + [f\"box_blur({sigma})\" for sigma in sigma_vals],\n",
    "        [gray2rgb(I) for I in box_blurs],\n",
    "    ),\n",
    "    (\n",
    "        [\"original_fft\"] + [f\"box_blur_fft({sigma})\" for sigma in sigma_vals],\n",
    "        [discrete_fourier_transform(I) for I in box_blurs],\n",
    "    ),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "049cf696",
   "metadata": {},
   "source": [
    "### Вопросы:\n",
    "* Какой артефакт возникает при использовании box-фильтра?\n",
    "* Как этот артефакт можно объяснить с помощью преобразования Фурье?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a59c442a",
   "metadata": {},
   "source": [
    "### Задание: визуализируйте преобразование Фурье для ядер фильтра Гаусса и box-фильтра"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1be62ad8",
   "metadata": {},
   "outputs": [],
   "source": [
    "kernels = [gauss_filter_kernel(sigma) for sigma in sigma_vals]\n",
    "kernel_dfts = [discrete_fourier_transform(kernel, vis=True) for kernel in kernels]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9f1fd64b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Визуализируем преобразование Фурье для фильтра Гаусса\n",
    "show_imgs_wrapper(\n",
    "    ([f\"gauss_kernel({sigma})\" for sigma in sigma_vals], kernels),\n",
    "    ([f\"gauss_kernel({sigma}) -> DFT\" for sigma in sigma_vals], kernel_dfts),\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7908711e",
   "metadata": {},
   "outputs": [],
   "source": [
    "kernels = [box_filter_kernel(size) for size in box_vals]\n",
    "kernel_dfts = [discrete_fourier_transform(kernel, vis=True) for kernel in kernels]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c7e6e289",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Визуализируем преобразование Фурье для box-фильтра\n",
    "show_imgs_wrapper(\n",
    "    ([f\"box_kernel({size})\" for size in box_vals], kernels),\n",
    "    ([f\"box_kernel({size}) -> DFT\" for size in box_vals], kernel_dfts),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a64ae4d0",
   "metadata": {},
   "source": [
    "### Задание: релизуйте медианный фильтр и рассмотрите результаты его применения"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "34c0477f",
   "metadata": {},
   "outputs": [],
   "source": [
    "def med_blur(img, size):\n",
    "    # Можно ли тут использовать conv_theorem?\n",
    "    disk = morphology.disk(size // 2)\n",
    "    return filters.median(img, disk, mode=\"wrap\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9d8f83c7",
   "metadata": {},
   "outputs": [],
   "source": [
    "med_vals = (5, 11, 21)\n",
    "med_blurs = [img] + [med_blur(img, size) for size in med_vals]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0abc97b8",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Визуализируем размытие с помощью медианного фильтра\n",
    "show_imgs_wrapper(\n",
    "    (\n",
    "        [\"original\"] + [f\"med_blur({size})\" for size in med_vals],\n",
    "        [gray2rgb(I) for I in med_blurs],\n",
    "    ),\n",
    "    (\n",
    "        [\"original_fft\"] + [f\"med_blur_fft({size})\" for size in med_vals],\n",
    "        [discrete_fourier_transform(I) for I in med_blurs],\n",
    "    ),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6cf4c29a",
   "metadata": {},
   "source": [
    "### Вопросы:\n",
    "* Что происходит с частотной визуализацией изображения после применения медианного фильтра?\n",
    "* Почему не получается реализовать медианный фильтр через свертку?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "60319f5f",
   "metadata": {},
   "source": [
    "## Склейка изображений\n",
    "Интересным примером использования фильтра Гаусса для изображений является задача склейки картинок. Ее суть заключается в том, чтобы по маске совместить два изображения и при этом сделать переход от одной картинки к другой максимально плавным. Для этого используются пирамиды Гаусса и Лапласа.\n",
    "\n",
    "**План действий:**\n",
    "\n",
    "_Вход_: изображения $A$ и $B$, маска склейки $M$\n",
    "\n",
    "_Алгоритм_:\n",
    "1. Строим гауссовские пирамиды $GA$ и $GB$ из изображений $A$ и $B$\n",
    "2. Строим лапласовские пирамиды $LA$ и $LB$ из пирамид $GA$ и $GB$\n",
    "3. Строим гауссовскую пирамиду $GM$ из маски $M$\n",
    "4. «Смешиваем» пирамиды: $LS = GM * LA+ (1−GM) * LB$\n",
    "5. Строим результат из пирамиды LS\n",
    "\n",
    "_Более подробно и с поясняющими схемами можно прочитать [здесь](https://courses.cv-gml.ru/storage/seminars/fourier-transform/idar_dyrdal_laplace_blending.pdf) (см. слайды 6-11)._"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ae2cc648",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Загрузим изображения\n",
    "imgs = {\n",
    "    \"a\": imread(\"images/a.png\")[..., :3].astype(np.float64) / 255,\n",
    "    \"b\": imread(\"images/b.png\")[..., :3].astype(np.float64) / 255,\n",
    "    \"mask\": imread(\"images/mask.jpg\", as_gray=True)[..., None],\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fbb35ee2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Визуализируем изображения\n",
    "show_imgs_wrapper([list(imgs.keys()), list(imgs.values())])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fb5eebbf",
   "metadata": {},
   "source": [
    "### Задание: напрямую смешайте два изображения по маске"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b6bcce38",
   "metadata": {},
   "outputs": [],
   "source": [
    "mask = imgs[\"mask\"]\n",
    "a = imgs[\"a\"]\n",
    "b = imgs[\"b\"]\n",
    "masked_a = (1 - mask) * a\n",
    "masked_b = mask * b\n",
    "naive_blend = masked_a + masked_b"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1b797338",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Визуализируем результат\n",
    "show_imgs_wrapper(\n",
    "    (\n",
    "        [\"M\", \"(1-M) * a\", \"M * b\", \"naive_blend\"],\n",
    "        [mask, masked_a, masked_b, naive_blend],\n",
    "    ),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37800dc0",
   "metadata": {},
   "source": [
    "### Задание: смешайте два изображения по сглаженной маске"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a250161e",
   "metadata": {},
   "outputs": [],
   "source": [
    "from skimage.exposure import adjust_gamma\n",
    "from skimage.filters import gaussian\n",
    "\n",
    "gamma = 2.2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "20c60b56",
   "metadata": {},
   "outputs": [],
   "source": [
    "smooth_mask = gaussian(imgs[\"mask\"], sigma=64)\n",
    "a = adjust_gamma(imgs[\"a\"], gamma)\n",
    "b = adjust_gamma(imgs[\"b\"], gamma)\n",
    "masked_a = (1 - smooth_mask) * a\n",
    "masked_b = smooth_mask * b\n",
    "smooth_blend = adjust_gamma(masked_a + masked_b, 1 / gamma)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7bf5f4a2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Визуализируем результат\n",
    "show_imgs_wrapper(\n",
    "    (\n",
    "        [\"M\", \"(1-M) * a\", \"M * b\", \"smooth_blend\"],\n",
    "        [smooth_mask, masked_a, masked_b, smooth_blend],\n",
    "    ),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "718291a8",
   "metadata": {},
   "source": [
    "Charles Poyton strikes again: [Frequently Asked Questions about Gamma](https://www.poynton.ca/pdf/GammaFAQ.pdf)!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2db25b6c",
   "metadata": {},
   "source": [
    "### Задание: реализовать пирамиды Гаусса и Лапласа, а также функции «смешивания» пирамид и склейки изображений"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dccafcb4",
   "metadata": {},
   "outputs": [],
   "source": [
    "from skimage.transform import resize"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "64a8aa4a",
   "metadata": {},
   "outputs": [],
   "source": [
    "n_layers = 4\n",
    "base_mask = gaussian(imgs[\"mask\"], 8)\n",
    "a = adjust_gamma(imgs[\"a\"], gamma)\n",
    "b = adjust_gamma(imgs[\"b\"], gamma)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cff1e340",
   "metadata": {},
   "outputs": [],
   "source": [
    "def gaussian_pyramid(img, n_layers, sigma):\n",
    "    \"\"\"Построение пирамиды Гаусса\n",
    "\n",
    "    Вход: изображение, количество уровней, sigma\n",
    "    Выход: уровни пирамиды\n",
    "\n",
    "    \"\"\"\n",
    "    pyramid = [img]\n",
    "    for _ in range(n_layers - 1):\n",
    "        nxt = pyramid[-1]\n",
    "        nxt = gaussian(nxt, sigma, channel_axis=-1)\n",
    "        h, w = nxt.shape[:2]\n",
    "        nxt = resize(nxt, output_shape=(h // 2, w // 2))\n",
    "        pyramid.append(nxt)\n",
    "    return pyramid"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fccd5ca0",
   "metadata": {},
   "outputs": [],
   "source": [
    "gaussian_mask = gaussian_pyramid(base_mask, n_layers, 32)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "da9e3b1e",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_imgs_wrapper(\n",
    "    ([f\"gaussian_mask[{i}]\" for i in range(n_layers)], gaussian_mask),\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "88e56af5",
   "metadata": {},
   "outputs": [],
   "source": [
    "gaussian_a = gaussian_pyramid(a, n_layers, 16)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2e753c67",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_imgs_wrapper(\n",
    "    (\n",
    "        [f\"gaussian_a[{i}]\" for i in range(n_layers)],\n",
    "        [adjust_gamma(i, 1 / gamma) for i in gaussian_a],\n",
    "    ),\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2e1a50f4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def laplacian_pyramid(img, n_layers, sigma):\n",
    "    \"\"\"Построение пирамиды Лапласа\n",
    "\n",
    "    Вход: изображение, количество уровней, sigma\n",
    "    Выход: уровни пирамиды\n",
    "\n",
    "    \"\"\"\n",
    "    pyramid_gauss = gaussian_pyramid(img, n_layers, sigma)\n",
    "    pyramid = []\n",
    "    for i in range(n_layers - 1):\n",
    "        ths = pyramid_gauss[i]\n",
    "        nxt = pyramid_gauss[i + 1]\n",
    "        nxt = resize(nxt, output_shape=ths.shape)\n",
    "        pyramid.append(ths - nxt)\n",
    "    pyramid.append(pyramid_gauss[-1])\n",
    "    return pyramid"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8d9ecefd",
   "metadata": {},
   "outputs": [],
   "source": [
    "laplacian_a = laplacian_pyramid(a, n_layers, 16)\n",
    "laplacian_b = laplacian_pyramid(b, n_layers, 16)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8718a68f",
   "metadata": {},
   "outputs": [],
   "source": [
    "def vis_img_delta(delta):\n",
    "    return (delta + 0.5).clip(0, 1)\n",
    "\n",
    "laplacian_a_vis = [vis_img_delta(img) for img in laplacian_a[:-1]] + [laplacian_a[-1]]\n",
    "laplacian_b_vis = [vis_img_delta(img) for img in laplacian_b[:-1]] + [laplacian_b[-1]]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "82839dc1",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_imgs_wrapper(\n",
    "    ([f\"laplacian_a[{i}]\" for i in range(n_layers)], laplacian_a_vis),\n",
    "    ([f\"laplacian_b[{i}]\" for i in range(n_layers)], laplacian_b_vis),\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "06f6e551",
   "metadata": {},
   "outputs": [],
   "source": [
    "def blend_pyramids(pyramid_a, pyramid_b, pyramid_mask):\n",
    "    \"\"\"Смешивание пирамид\n",
    "\n",
    "    Вход: пирамида Лапласа для первого иззображения,\n",
    "          пирамида Лапласа для второго изображения,\n",
    "          пирамида Гаусса для маски\n",
    "    Выход: «смешанная» пирамида\n",
    "\n",
    "    \"\"\"\n",
    "    blend_pyramid = []\n",
    "    for i in range(len(pyramid_a)):\n",
    "        mask = pyramid_mask[i]\n",
    "        a = pyramid_a[i]\n",
    "        b = pyramid_b[i]\n",
    "\n",
    "        masked_a = (1 - mask) * a\n",
    "        masked_b = mask * b\n",
    "        blend = masked_a + masked_b\n",
    "        blend_pyramid.append(blend)\n",
    "    return blend_pyramid"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "78b2913a",
   "metadata": {},
   "outputs": [],
   "source": [
    "result_pyramid = blend_pyramids(laplacian_a, laplacian_b, gaussian_mask)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3f742988",
   "metadata": {},
   "outputs": [],
   "source": [
    "result_pyramid_vis = [vis_img_delta(img) for img in result_pyramid[:-1]] + result_pyramid[-1:]\n",
    "show_imgs_wrapper(\n",
    "    ([f\"gaussian_mask[{i}]\" for i in range(n_layers)], gaussian_mask),\n",
    "    ([f\"result_pyramid[{i}]\" for i in range(n_layers)], result_pyramid_vis),\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "148f29f8",
   "metadata": {},
   "outputs": [],
   "source": [
    "def blend_image(blend_pyramid):\n",
    "    \"\"\"Склейка изображений\n",
    "\n",
    "    Вход: «смешанная» пирамида\n",
    "    Выход: склеенное изображение\n",
    "\n",
    "    \"\"\"\n",
    "    img = blend_pyramid[-1]\n",
    "    for i in range(len(blend_pyramid) - 1)[::-1]:\n",
    "        level = blend_pyramid[i]\n",
    "        img = resize(img, output_shape=level.shape)\n",
    "        img = img + level\n",
    "    return img"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "83a16b1b",
   "metadata": {},
   "outputs": [],
   "source": [
    "pyramid_blend = blend_image(result_pyramid).clip(0, 1)\n",
    "pyramid_blend = adjust_gamma(pyramid_blend, 1 / gamma)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6fb72083",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_imgs_wrapper(\n",
    "    (\n",
    "        [\"naive\", \"smooth\", \"pyramid\"],\n",
    "        [naive_blend, smooth_blend, pyramid_blend],\n",
    "    ),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "344eb79d",
   "metadata": {},
   "source": [
    "---"
   ]
  }
 ],
 "metadata": {},
 "nbformat": 4,
 "nbformat_minor": 5
}