{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "b8e61c5c-4f3e-4980-be22-4010a247babd",
   "metadata": {},
   "source": [
    "## Streaming Video: Live video streams made easy\n",
    "\n",
    "In this example we will demonstrate how to develop a general tool to transform live image streams from the users web cam. We will be using Panels\n",
    "[`VideoStream`](https://panel.holoviz.org/reference/widgets/VideoStream.html) widget to record and stream the images.\n",
    "\n",
    "We will also show how to apply *blur*, *grayscale*, *sobel* and *face recognition* models to the video stream."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "5a3760b6-f8a7-46ad-8831-620a131c1512",
   "metadata": {},
   "source": [
    "## Imports and Settings\n",
    "\n",
    "Among other things we will be using [numpy](https://numpy.org/), [PIL](https://pillow.readthedocs.io/en/stable/) and [scikit-image](https://scikit-image.org/) to work with the images."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "02b862bc-f710-456b-9a34-f1b74a76df2c",
   "metadata": {},
   "outputs": [],
   "source": [
    "import base64\n",
    "import io\n",
    "import time\n",
    "\n",
    "import numpy as np\n",
    "import param\n",
    "import PIL\n",
    "import skimage\n",
    "\n",
    "from PIL import Image, ImageFilter\n",
    "from skimage import data, filters\n",
    "from skimage.color.adapt_rgb import adapt_rgb, each_channel\n",
    "from skimage.draw import rectangle\n",
    "from skimage.exposure import rescale_intensity\n",
    "from skimage.feature import Cascade\n",
    "\n",
    "import panel as pn\n",
    "\n",
    "pn.extension(design='material', sizing_mode=\"stretch_width\")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "43e68289-6c33-435f-b279-cf89327266ac",
   "metadata": {},
   "source": [
    "We define the *height* and *width* of the images to transform. Smaller is faster.\n",
    "We also define the *timeout*, i.e. how often the videostream takes and streams a new image."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7b46542f-fa1a-4dde-a8c6-f023f3358a6c",
   "metadata": {},
   "outputs": [],
   "source": [
    "HEIGHT = 500 # pixels\n",
    "WIDTH = 500 # pixels\n",
    "TIMEOUT = 500 # milliseconds"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "bb8b1066-c799-4d6a-94d0-361dd3cc6007",
   "metadata": {},
   "source": [
    "## Base Image Models\n",
    "\n",
    "We will need to define some *base image models* components. The base models are custom Panel components that inherit from Panels [`Viewer`](https://panel.holoviz.org/how_to/custom_components/custom_viewer.html) class.\n",
    "\n",
    "The *base models* makes it easy to later turn *image to image* algorithms into interactive UIs like the `FaceDetectionModel` shown in the image just below.\n",
    "\n",
    "![VideoStreamInterfaceTimer](../../assets/VideoStreamInterfaceFaceDetectionViewer.jpg)\n",
    "\n",
    "Please note we restrict our selves to working with `.jpg` images. The `VideoStream` widget also support `.png` images. But `.png` images are much bigger and slower to work with."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a507eb66-b242-441c-8d89-dba504ec79a3",
   "metadata": {},
   "outputs": [],
   "source": [
    "class ImageModel(pn.viewable.Viewer):\n",
    "    \"\"\"Base class for image models.\"\"\"\n",
    "\n",
    "    def __init__(self, **params):\n",
    "        super().__init__(**params)\n",
    "\n",
    "        with param.edit_constant(self):\n",
    "            self.name = self.__class__.name.replace(\"Model\", \"\")\n",
    "        self.view = self.create_view()\n",
    "\n",
    "    def __panel__(self):\n",
    "        return self.view\n",
    "\n",
    "    def apply(self, image: str, height: int = HEIGHT, width: int = WIDTH) -> str:\n",
    "        \"\"\"Transforms a base64 encoded jpg image to a base64 encoded jpg BytesIO object\"\"\"\n",
    "        raise NotImplementedError()\n",
    "\n",
    "    def create_view(self):\n",
    "        \"\"\"Creates a view of the parameters of the transform to enable the user to configure them\"\"\"\n",
    "        return pn.Param(self, name=self.name)\n",
    "\n",
    "    def transform(self, image):\n",
    "        \"\"\"Transforms the image\"\"\"\n",
    "        raise NotImplementedError()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "be8256eb-f2bb-4f24-84dc-8b6b47d64958",
   "metadata": {},
   "source": [
    "Lets define a base model for working with **`PIL`** images"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5ebf5560-29f6-4b3f-815e-a70010998ba1",
   "metadata": {},
   "outputs": [],
   "source": [
    "class PILImageModel(ImageModel):\n",
    "    \"\"\"Base class for PIL image models\"\"\"\n",
    "\n",
    "    @staticmethod\n",
    "    def to_pil_img(value: str, height=HEIGHT, width=WIDTH):\n",
    "        \"\"\"Converts a base64 jpeg image string to a PIL.Image\"\"\"\n",
    "        encoded_data = value.split(\",\")[1]\n",
    "        base64_decoded = base64.b64decode(encoded_data)\n",
    "        image = Image.open(io.BytesIO(base64_decoded))\n",
    "        image.draft(\"RGB\", (height, width))\n",
    "        return image\n",
    "\n",
    "    @staticmethod\n",
    "    def from_pil_img(image: Image):\n",
    "        \"\"\"Converts a PIL.Image to a base64 encoded JPG BytesIO object\"\"\"\n",
    "        buff = io.BytesIO()\n",
    "        image.save(buff, format=\"JPEG\")\n",
    "        return buff\n",
    "\n",
    "    def apply(self, image: str, height: int = HEIGHT, width: int = WIDTH) -> io.BytesIO:\n",
    "        pil_img = self.to_pil_img(image, height=height, width=width)\n",
    "\n",
    "        transformed_image = self.transform(pil_img)\n",
    "\n",
    "        return self.from_pil_img(transformed_image)\n",
    "\n",
    "    def transform(self, image: PIL.Image) -> PIL.Image:\n",
    "        \"\"\"Transforms the PIL.Image image\"\"\"\n",
    "        raise NotImplementedError()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "63cff6f1-f652-4413-b230-b4602e115560",
   "metadata": {},
   "source": [
    "Lets define a base model for working with **`Numpy`** images."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "128c8e1e-8695-4710-a7c1-40476ced300a",
   "metadata": {},
   "outputs": [],
   "source": [
    "class NumpyImageModel(ImageModel):\n",
    "    \"\"\"Base class for np.ndarray image models\"\"\"\n",
    "\n",
    "    @staticmethod\n",
    "    def to_np_ndarray(image: str, height=HEIGHT, width=WIDTH) -> np.ndarray:\n",
    "        \"\"\"Converts a base64 encoded jpeg string to a np.ndarray\"\"\"\n",
    "        pil_img = PILImageModel.to_pil_img(image, height=height, width=width)\n",
    "        return np.array(pil_img)\n",
    "\n",
    "    @staticmethod\n",
    "    def from_np_ndarray(image: np.ndarray) -> io.BytesIO:\n",
    "        \"\"\"Converts np.ndarray jpeg image to a jpeg BytesIO instance\"\"\"\n",
    "        if image.dtype == np.dtype(\"float64\"):\n",
    "            image = (image * 255).astype(np.uint8)\n",
    "        pil_img = PIL.Image.fromarray(image)\n",
    "        return PILImageModel.from_pil_img(pil_img)\n",
    "\n",
    "    def apply(self, image: str, height: int = HEIGHT, width: int = WIDTH) -> io.BytesIO:\n",
    "        np_array = self.to_np_ndarray(image, height=height, width=width)\n",
    "\n",
    "        transformed_image = self.transform(np_array)\n",
    "\n",
    "        return self.from_np_ndarray(transformed_image)\n",
    "\n",
    "    def transform(self, image: np.ndarray) -> np.ndarray:\n",
    "        \"\"\"Transforms the np.array image\"\"\"\n",
    "        raise NotImplementedError()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "77bf1e99-701e-4dad-bd8a-faba4bdd446a",
   "metadata": {},
   "source": [
    "## Timer\n",
    "\n",
    "Lets define a timer component to visualize the stats of the live videostream and the image transformations\n",
    "\n",
    "![VideoStreamInterfaceTimer](../../assets/VideoStreamInterfaceTimer.jpg)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "92a4ec91-3a6d-43be-8337-690614bb10cb",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Timer(pn.viewable.Viewer):\n",
    "    \"\"\"Helper Component used to show duration trends\"\"\"\n",
    "\n",
    "    _trends = param.Dict()\n",
    "\n",
    "    def __init__(self, **params):\n",
    "        super().__init__()\n",
    "\n",
    "        self.last_updates = {}\n",
    "        self._trends = {}\n",
    "\n",
    "        self._layout = pn.Row(**params)\n",
    "\n",
    "    def time_it(self, name, func, *args, **kwargs):\n",
    "        \"\"\"Measures the duration of the execution of the func function and reports it under the\n",
    "        name specified\"\"\"\n",
    "        start = time.time()\n",
    "        result = func(*args, **kwargs)\n",
    "        end = time.time()\n",
    "        duration = round(end - start, 2)\n",
    "        self._report(name=name, duration=duration)\n",
    "        return result\n",
    "\n",
    "    def inc_it(self, name):\n",
    "        \"\"\"Measures the duration since the last time `inc_it` was called and reports it under the\n",
    "        specified name\"\"\"\n",
    "        start = self.last_updates.get(name, time.time())\n",
    "        end = time.time()\n",
    "        duration = round(end - start, 2)\n",
    "        self._report(name=name, duration=duration)\n",
    "        self.last_updates[name] = end\n",
    "\n",
    "    def _report(self, name, duration):\n",
    "        if not name in self._trends:\n",
    "            self._trends[name] = pn.indicators.Trend(\n",
    "                title=name,\n",
    "                data={\"x\": [1], \"y\": [duration]},\n",
    "                height=100,\n",
    "                width=150,\n",
    "                sizing_mode=\"fixed\",\n",
    "            )\n",
    "            self.param.trigger(\"_trends\")\n",
    "        else:\n",
    "            trend = self._trends[name]\n",
    "            next_x = max(trend.data[\"x\"]) + 1\n",
    "            trend.stream({\"x\": [next_x], \"y\": [duration]}, rollover=10)\n",
    "\n",
    "    @param.depends(\"_trends\")\n",
    "    def _panel(self):\n",
    "        self._layout[:] = list(self._trends.values())\n",
    "        return self._layout\n",
    "\n",
    "    def __panel__(self):\n",
    "        return pn.panel(self._panel)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "44851482-f580-49a7-ac53-2132d3f29bc0",
   "metadata": {},
   "source": [
    "## VideoStreamInterface\n",
    "\n",
    "The `VideoStreamInterface` will be putting things together in a nice UI.\n",
    "\n",
    "![VideoStreamInterfaceSobel](../../assets/VideoStreamInterfaceSobel.jpg)\n",
    "\n",
    "Lets define a helper function first"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b4de82da-81e6-4799-a1d3-a54bf8e017b1",
   "metadata": {},
   "outputs": [],
   "source": [
    "def to_instance(value, **params):\n",
    "    \"\"\"Converts the value to an instance\n",
    "\n",
    "    Args:\n",
    "        value: A param.Parameterized class or instance\n",
    "\n",
    "    Returns:\n",
    "        An instance of the param.Parameterized class\n",
    "    \"\"\"\n",
    "    if isinstance(value, param.Parameterized):\n",
    "        value.param.update(**params)\n",
    "        return value\n",
    "    return value(**params)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "d74ba396-7972-4711-8ff5-b4d27b31120c",
   "metadata": {},
   "source": [
    "The `VideoStreamInterface` will take a list of `ImageModel`s. The user can the select and apply the models to the images from the `VideoStream`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c6dc71b7-34b2-4171-8a72-46ab9edeba91",
   "metadata": {},
   "outputs": [],
   "source": [
    "class VideoStreamInterface(pn.viewable.Viewer):\n",
    "    \"\"\"An easy to use interface for a VideoStream and a set of transforms\"\"\"\n",
    "\n",
    "    video_stream = param.ClassSelector(\n",
    "        class_=pn.widgets.VideoStream, constant=True, doc=\"The source VideoStream\"\n",
    "    )\n",
    "\n",
    "    height = param.Integer(\n",
    "        default=HEIGHT,\n",
    "        bounds=(10, 2000),\n",
    "        step=10,\n",
    "        doc=\"\"\"The height of the image converted and shown\"\"\",\n",
    "    )\n",
    "    width = param.Integer(\n",
    "        default=WIDTH,\n",
    "        bounds=(10, 2000),\n",
    "        step=10,\n",
    "        doc=\"\"\"The width of the image converted and shown\"\"\",\n",
    "    )\n",
    "\n",
    "    model = param.Selector(doc=\"The currently selected model\")\n",
    "\n",
    "    def __init__(\n",
    "        self,\n",
    "        models,\n",
    "        timeout=TIMEOUT,\n",
    "        paused=False,\n",
    "        **params,\n",
    "    ):\n",
    "        super().__init__(\n",
    "            video_stream=pn.widgets.VideoStream(\n",
    "                name=\"Video Stream\",\n",
    "                timeout=timeout,\n",
    "                paused=paused,\n",
    "                height=0,\n",
    "                width=0,\n",
    "                visible=False,\n",
    "                format=\"jpeg\",\n",
    "            ),\n",
    "            **params,\n",
    "        )\n",
    "        self.image = pn.pane.JPG(\n",
    "            height=self.height, width=self.width, sizing_mode=\"fixed\"\n",
    "        )\n",
    "        self._updating = False\n",
    "        models = [to_instance(model) for model in models]\n",
    "        self.param.model.objects = models\n",
    "        self.model = models[0]\n",
    "        self.timer = Timer(sizing_mode=\"stretch_width\")\n",
    "        self.settings = self._create_settings()\n",
    "        self._panel = self._create_panel()\n",
    "\n",
    "    def _create_settings(self):\n",
    "        return pn.Column(\n",
    "            pn.Param(\n",
    "                self.video_stream,\n",
    "                parameters=[\"timeout\", \"paused\"],\n",
    "                widgets={\n",
    "                    \"timeout\": {\n",
    "                        \"widget_type\": pn.widgets.IntSlider,\n",
    "                        \"start\": 10,\n",
    "                        \"end\": 2000,\n",
    "                        \"step\": 10,\n",
    "                    }\n",
    "                },\n",
    "            ),\n",
    "            self.timer,\n",
    "            pn.Param(self, parameters=[\"height\", \"width\"], name=\"Image\"),\n",
    "            pn.Param(\n",
    "                self,\n",
    "                parameters=[\"model\"],\n",
    "                expand_button=False,\n",
    "                expand=False,\n",
    "                widgets={\n",
    "                    \"model\": {\n",
    "                        \"widget_type\": pn.widgets.RadioButtonGroup,\n",
    "                        \"orientation\": \"vertical\",\n",
    "                        \"button_type\": \"primary\",\n",
    "                        \"button_style\": \"outline\"\n",
    "                    }\n",
    "                },\n",
    "                name=\"Model\",\n",
    "            ),\n",
    "            self._get_transform,\n",
    "        )\n",
    "\n",
    "    def _create_panel(self):\n",
    "        return pn.Row(\n",
    "            self.video_stream,\n",
    "            pn.layout.HSpacer(),\n",
    "            self.image,\n",
    "            pn.layout.HSpacer(),\n",
    "            sizing_mode=\"stretch_width\",\n",
    "            align=\"center\",\n",
    "        )\n",
    "\n",
    "    @param.depends(\"height\", \"width\", watch=True)\n",
    "    def _update_height_width(self):\n",
    "        self.image.height = self.height\n",
    "        self.image.width = self.width\n",
    "\n",
    "    @param.depends(\"model\")\n",
    "    def _get_transform(self):\n",
    "        # Hack: returning self.transform stops working after browsing the transforms for a while\n",
    "        return self.model.view\n",
    "\n",
    "    def __panel__(self):\n",
    "        return self._panel\n",
    "\n",
    "    @param.depends(\"video_stream.value\", watch=True)\n",
    "    def _handle_stream(self):\n",
    "        if self._updating:\n",
    "            return\n",
    "\n",
    "        self._updating = True\n",
    "        if self.model and self.video_stream.value:\n",
    "            value = self.video_stream.value\n",
    "            try:\n",
    "                image = self.timer.time_it(\n",
    "                    name=\"Model\",\n",
    "                    func=self.model.apply,\n",
    "                    image=value,\n",
    "                    height=self.height,\n",
    "                    width=self.width,\n",
    "                )\n",
    "                self.image.object = image\n",
    "            except PIL.UnidentifiedImageError:\n",
    "                print(\"unidentified image\")\n",
    "\n",
    "            self.timer.inc_it(\"Last Update\")\n",
    "        self._updating = False"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "9afa4c9b-a541-4a77-bb38-68d85f0a82d4",
   "metadata": {},
   "source": [
    "## Custom Image Models\n",
    "\n",
    "We will now make specific image to image algorithms interactive.\n",
    "\n",
    "Let us start with the [Gaussian Blur](https://pillow.readthedocs.io/en/stable/reference/ImageFilter.html#PIL.ImageFilter.GaussianBlur) algorithm."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f389b022-28b0-4ce2-a629-0df728a235bb",
   "metadata": {},
   "outputs": [],
   "source": [
    "class GaussianBlurModel(PILImageModel):\n",
    "    \"\"\"Gaussian Blur Model\n",
    "\n",
    "    https://pillow.readthedocs.io/en/stable/reference/ImageFilter.html#PIL.ImageFilter.GaussianBlur\n",
    "    \"\"\"\n",
    "\n",
    "    radius = param.Integer(default=2, bounds=(0, 10))\n",
    "\n",
    "    def transform(self, image: Image):\n",
    "        return image.filter(ImageFilter.GaussianBlur(radius=self.radius))"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "42d93547-bfce-4deb-a7e5-387cab9fac8e",
   "metadata": {},
   "source": [
    "Lets implement a [Grayscale](https://scikit-image.org/docs/0.15.x/auto_examples/color_exposure/plot_rgb_to_gray.html) algorithm."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "13f7f769-af74-4027-be8d-a22208e31800",
   "metadata": {},
   "outputs": [],
   "source": [
    "class GrayscaleModel(NumpyImageModel):\n",
    "    \"\"\"GrayScale Model\n",
    "\n",
    "    https://scikit-image.org/docs/0.15.x/auto_examples/color_exposure/plot_rgb_to_gray.html\n",
    "    \"\"\"\n",
    "\n",
    "    def transform(self, image: np.ndarray):\n",
    "        grayscale = skimage.color.rgb2gray(image[:, :, :3])\n",
    "        return skimage.color.gray2rgb(grayscale)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "b3461593-e63d-424a-b402-cbd6fcc75ffe",
   "metadata": {},
   "source": [
    "Lets implement the [Sobel](https://scikit-image.org/docs/0.15.x/auto_examples/color_exposure/plot_adapt_rgb.html) algorithm."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0991dd1c-0557-4dfa-b940-7918b0363d6c",
   "metadata": {},
   "outputs": [],
   "source": [
    "class SobelModel(NumpyImageModel):\n",
    "    \"\"\"Sobel Model\n",
    "\n",
    "    https://scikit-image.org/docs/0.15.x/auto_examples/color_exposure/plot_adapt_rgb.html\n",
    "    \"\"\"\n",
    "    def transform(self, image):\n",
    "\n",
    "\n",
    "        @adapt_rgb(each_channel)\n",
    "        def sobel_each(image):\n",
    "            return filters.sobel(image)\n",
    "\n",
    "        return rescale_intensity(1 - sobel_each(image))"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "cadae0c9-0191-4ca4-a770-c3be0b0987b7",
   "metadata": {},
   "source": [
    "Lets implement the [face detection model](https://scikit-image.org/docs/0.15.x/auto_examples/applications/plot_face_detection.html) of scikit-image."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "57423cdc-e23e-4236-ace8-452e8068970c",
   "metadata": {},
   "outputs": [],
   "source": [
    "@pn.cache()\n",
    "def get_detector():\n",
    "    \"\"\"Returns the Cascade detector\"\"\"\n",
    "    trained_file = data.lbp_frontal_face_cascade_filename()\n",
    "    return Cascade(trained_file)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5baa7f38-6368-4ed1-883c-7a0247d1c08a",
   "metadata": {},
   "outputs": [],
   "source": [
    "class FaceDetectionModel(NumpyImageModel):\n",
    "    \"\"\"Face detection using a cascade classifier.\n",
    "\n",
    "    https://scikit-image.org/docs/0.15.x/auto_examples/applications/plot_face_detection.html\n",
    "    \"\"\"\n",
    "\n",
    "    scale_factor = param.Number(default=1.4, bounds=(1.0, 2.0), step=0.1)\n",
    "    step_ratio = param.Integer(default=1, bounds=(1, 10))\n",
    "    size_x = param.Range(default=(60, 322), bounds=(10, 500))\n",
    "    size_y = param.Range(default=(60, 322), bounds=(10, 500))\n",
    "\n",
    "    def transform(self, image):\n",
    "        detector = get_detector()\n",
    "        detected = detector.detect_multi_scale(\n",
    "            img=image,\n",
    "            scale_factor=self.scale_factor,\n",
    "            step_ratio=self.step_ratio,\n",
    "            min_size=(self.size_x[0], self.size_y[0]),\n",
    "            max_size=(self.size_x[1], self.size_y[1]),\n",
    "        )\n",
    "\n",
    "        for patch in detected:\n",
    "            rrr, ccc = rectangle(\n",
    "                start=(patch[\"r\"], patch[\"c\"]),\n",
    "                extent=(patch[\"height\"], patch[\"width\"]),\n",
    "                shape=image.shape[:2],\n",
    "            )\n",
    "            image[rrr, ccc, 0] = 200\n",
    "\n",
    "        return image"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "253e15c1-0097-456f-b979-fbf42de8d270",
   "metadata": {},
   "source": [
    "Please note that these models are just examples. You can also implement your own models using Scikit-Image, Pytorch, Tensorflow etc and use the `VideoStreamInterface` to work interactively with them."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "1e6ffe9c-7885-4cb8-9793-97c00862e8ba",
   "metadata": {},
   "source": [
    "## Its alive!\n",
    "\n",
    "Lets define an instance of the `VideoStreamInterface`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "47d8118f-77b0-4edf-a733-2b9ba877e25e",
   "metadata": {},
   "outputs": [],
   "source": [
    "component = VideoStreamInterface(\n",
    "    models=[\n",
    "        GaussianBlurModel,\n",
    "        GrayscaleModel,\n",
    "        SobelModel,\n",
    "        FaceDetectionModel,\n",
    "    ]\n",
    ")\n",
    "pn.Row(pn.Row(component.settings, max_width=400), component)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "651ea40a-4d91-4a17-8761-c8b5cc446197",
   "metadata": {},
   "source": [
    "## Wrap it in a template\n",
    "\n",
    "What makes Panel unique is that our components work very well in both the notebook and as standalone data apps.\n",
    "\n",
    "We can wrap the component in the nicely styled [`FastListTemplate`](https://panel.holoviz.org/reference/templates/FastListTemplate.html) to make it *ready for production*."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dab6006d-001d-4217-ade7-ff2d0d77752f",
   "metadata": {},
   "outputs": [],
   "source": [
    "pn.template.FastListTemplate(\n",
    "    site=\"Panel\",\n",
    "    title=\"VideoStream Interface\",\n",
    "    sidebar=[component.settings],\n",
    "    main=[component],\n",
    ").servable(); # We add ; to not show the template in the notebook as it does not display well."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "9b00da3e-87fd-4e3e-bc81-e8bf827e2818",
   "metadata": {},
   "source": [
    "## Serve it as a server side app"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "7191262b-f610-494f-a797-9186a496c633",
   "metadata": {},
   "source": [
    "It is now possible to serve the live app via the command `panel serve streaming_videostream.ipynb`. The app is the available at http://localhost:5006/streaming_videostream.\n",
    "\n",
    "<img style=\"max-height:500px\" src=\"../../assets/VideoStreamInterface.jpg\"></img>"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "01fb5b28-6e49-4aaa-a0ca-2eaa301106c2",
   "metadata": {},
   "source": [
    "## Serve it as a client side app\n",
    "\n",
    "You can also [`panel convert`](https://panel.holoviz.org/how_to/wasm/convert.html) this app to web assembly for even better performance.\n",
    "\n",
    "First you will need to create a `requirements.txt` file with the following content\n",
    "\n",
    "```bash\n",
    "panel\n",
    "numpy\n",
    "scikit-image\n",
    "```\n",
    "\n",
    "Then you can\n",
    "\n",
    "- Run `panel convert streaming_videostream.ipynb --to pyodide-worker --out pyodide --requirements requirements.txt`\n",
    "- Run `python3 -m http.server` to start a web server locally\n",
    "- Open http://localhost:8000/pyodide/streaming_videostream.html to try out the app."
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
