{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Convert color images to grayscale" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Problem\n", "\n", "You need to convert color images to grayscale for analysis, preprocessing, or model inputs that require single-channel images.\n", "\n", "Different conversion methods produce different results—you need to choose the right approach for your use case." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Solution\n", "\n", "**What's in this recipe:**\n", "\n", "- Simple conversion with PIL\n", "- Perceptually accurate grayscale (weighted RGB channels)\n", "- Custom UDF for advanced conversion\n", "\n", "You convert RGB images to grayscale in your table using either Pixeltable's built-in `.convert()` method for standard conversion, or a custom UDF (relies on NumPy and PIL/Pillow) for gamma-corrected conversion when scientific accuracy matters.\n", "\n", "You can iterate on transformations before adding them to your table. Use `.select()` with `.collect()` to preview results on sample images—nothing is stored in your table. If you want to collect only the first few rows, use `.head(n)` instead of `.collect()`. Once you're satisfied, use `.add_computed_column()` to apply the conversion to all images in your table.\n", "\n", "For more on this workflow, see [Get fast feedback on transformations](https://docs.pixeltable.com/howto/cookbooks/core/dev-iterative-workflow)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Conversion methods:**\n", "\n", "| Method | Speed | Accuracy | When to use |\n", "|--------|-------|----------|-------------|\n", "| **Simple (PIL `.convert('L')`)** | Fast | Good | Model preprocessing, general analysis |\n", "| **Gamma-corrected (custom UDF)** | Slow | Best | Scientific imaging, professional photography |\n", "\n", "The simple method uses PIL's built-in conversion. The gamma-corrected method requires a custom UDF (not built into PIL) that applies perceptual weighting in linear color space.\n", "\n", "*For technical details on gamma correction and grayscale conversion, see [Wikipedia: Grayscale](https://en.wikipedia.org/wiki/Grayscale).*\n", "\n", "### Setup" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "execution": { "iopub.execute_input": "2025-12-12T02:39:57.066884Z", "iopub.status.busy": "2025-12-12T02:39:57.066605Z", "iopub.status.idle": "2025-12-12T02:39:59.904424Z", "shell.execute_reply": "2025-12-12T02:39:59.903882Z" } }, "outputs": [], "source": [ "%pip install -qU pixeltable numpy" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "execution": { "iopub.execute_input": "2025-12-12T02:39:59.923155Z", "iopub.status.busy": "2025-12-12T02:39:59.922962Z", "iopub.status.idle": "2025-12-12T02:40:01.207961Z", "shell.execute_reply": "2025-12-12T02:40:01.207548Z" } }, "outputs": [], "source": [ "import numpy as np\n", "import pixeltable as pxt\n", "from PIL import Image" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Load images" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "execution": { "iopub.execute_input": "2025-12-12T02:40:01.210395Z", "iopub.status.busy": "2025-12-12T02:40:01.210176Z", "iopub.status.idle": "2025-12-12T02:40:01.414333Z", "shell.execute_reply": "2025-12-12T02:40:01.413978Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Connected to Pixeltable database at: postgresql+psycopg://postgres:@/pixeltable?host=/Users/pjlb/.pixeltable/pgdata\n", "Created directory 'image_demo'.\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Create a fresh directory (drop existing if present)\n", "pxt.drop_dir('image_demo', force=True)\n", "pxt.create_dir('image_demo')" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "execution": { "iopub.execute_input": "2025-12-12T02:40:01.416084Z", "iopub.status.busy": "2025-12-12T02:40:01.415941Z", "iopub.status.idle": "2025-12-12T02:40:01.481598Z", "shell.execute_reply": "2025-12-12T02:40:01.481218Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Created table 'gray'.\n" ] } ], "source": [ "t = pxt.create_table('image_demo/gray', {'image': pxt.Image})" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "execution": { "iopub.execute_input": "2025-12-12T02:40:01.483607Z", "iopub.status.busy": "2025-12-12T02:40:01.483488Z", "iopub.status.idle": "2025-12-12T02:40:02.420224Z", "shell.execute_reply": "2025-12-12T02:40:02.419533Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\r", "Inserting rows into `gray`: 0 rows [00:00, ? rows/s]" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\r", "Inserting rows into `gray`: 3 rows [00:00, 617.66 rows/s]" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "Inserted 3 rows with 0 errors.\n" ] }, { "data": { "text/plain": [ "3 rows inserted, 6 values computed." ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "t.insert(\n", " [\n", " {\n", " 'image': 'https://raw.githubusercontent.com/pixeltable/pixeltable/main/docs/resources/images/000000000036.jpg'\n", " },\n", " {\n", " 'image': 'https://raw.githubusercontent.com/pixeltable/pixeltable/main/docs/resources/images/000000000090.jpg'\n", " },\n", " {\n", " 'image': 'https://raw.githubusercontent.com/pixeltable/pixeltable/main/docs/resources/images/000000000106.jpg'\n", " },\n", " ]\n", ")" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "execution": { "iopub.execute_input": "2025-12-12T02:40:02.422609Z", "iopub.status.busy": "2025-12-12T02:40:02.422172Z", "iopub.status.idle": "2025-12-12T02:40:02.527786Z", "shell.execute_reply": "2025-12-12T02:40:02.527116Z" } }, "outputs": [ { "data": { "text/html": [ "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
image
\n", " \n", "
\n", " \n", "
\n", " \n", "
" ], "text/plain": [ " image\n", "0 \n", " \n", " \n", " image\n", " convert\n", " \n", " \n", " \n", " \n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", " \n", " \n", "" ], "text/plain": [ " image \\\n", "0 \n", " \n", " \n", " image\n", " grayscale\n", " \n", " \n", " \n", " \n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", " \n", " \n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", " \n", " \n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", " \n", " \n", "" ], "text/plain": [ " image \\\n", "0 Image.Image:\n", " \"\"\"Convert RGB to grayscale with full gamma correction.\n", "\n", " Most accurate but slower. Gamma-decompresses, applies perceptual weights\n", " in linear space, then re-compresses for display.\n", " \"\"\"\n", " rgb = np.array(img).astype(np.float32) / 255.0\n", "\n", " # Gamma decompress: make pixel values perceptually linear\n", " rgb_lin = ((rgb + 0.055) / 1.055) ** 2.4\n", " rgb_lin = np.where(rgb <= 0.04045, rgb / 12.92, rgb_lin)\n", "\n", " # Apply perceptual weights in linear space\n", " gray_lin = (\n", " 0.2126 * rgb_lin[:, :, 0]\n", " + 0.7152 * rgb_lin[:, :, 1]\n", " + 0.0722 * rgb_lin[:, :, 2]\n", " )\n", "\n", " # Gamma compress: make values display-ready\n", " gray = 1.055 * gray_lin ** (1 / 2.4) - 0.055\n", " gray = np.where(gray_lin <= 0.0031308, 12.92 * gray_lin, gray)\n", "\n", " gray = (gray * 255).astype(np.uint8)\n", " return Image.fromarray(gray)" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "execution": { "iopub.execute_input": "2025-12-12T02:40:02.801632Z", "iopub.status.busy": "2025-12-12T02:40:02.801523Z", "iopub.status.idle": "2025-12-12T02:40:02.907920Z", "shell.execute_reply": "2025-12-12T02:40:02.907325Z" } }, "outputs": [ { "data": { "text/html": [ "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
imagegrayscalergb_to_gray_accurate
\n", " \n", "
\n", " \n", "
\n", " \n", "
" ], "text/plain": [ " image \\\n", "0 \n", " \n", " \n", " image\n", " grayscale\n", " accurate\n", " \n", " \n", " \n", " \n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", " \n", " \n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", " \n", " \n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", "
\n", " \n", "
\n", " \n", " \n", "" ], "text/plain": [ " image \\\n", "0