> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pixeltable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Transform images with PIL operations

> Resize, crop, rotate, and transform images at scale in Pixeltable using PIL operations exposed as built-in UDFs for computed columns.

<a href="https://kaggle.com/kernels/welcome?src=https://github.com/pixeltable/pixeltable/blob/release/docs/release/howto/cookbooks/images/img-pil-transforms.ipynb" id="openKaggle" target="_blank" rel="noopener noreferrer"><img src="https://kaggle.com/static/images/open-in-kaggle.svg" alt="Open in Kaggle" style={{ display: 'inline', margin: '0px' }} noZoom /></a>  <a href="https://colab.research.google.com/github/pixeltable/pixeltable/blob/release/docs/release/howto/cookbooks/images/img-pil-transforms.ipynb" id="openColab" target="_blank" rel="noopener noreferrer"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open in Colab" style={{ display: 'inline', margin: '0px' }} noZoom /></a>  <a href="https://raw.githubusercontent.com/pixeltable/pixeltable/refs/tags/release/docs/release/howto/cookbooks/images/img-pil-transforms.ipynb" id="downloadNotebook" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/%E2%AC%87-Download%20Notebook-blue" alt="Download Notebook" style={{ display: 'inline', margin: '0px' }} noZoom /></a>

<Tip>This documentation page is also available as an interactive notebook. You can launch the notebook in
Kaggle or Colab, or download it for use with an IDE or local Jupyter installation, by clicking one of the
above links.</Tip>

## Problem

You need to resize, rotate, crop, or convert hundreds of images—and keep
track of all the transformed versions.

## Solution

**What’s in this recipe:**

* Basic image operations (resize, rotate, flip, crop)
* Track image properties
* Iterate on transformations before adding to your table

You apply PIL transformations (resize, rotate, flip, crop) to images in
your table using Pixeltable’s built-in image functions—common operations
that work directly on image columns.

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 transformation to
all images in your table.

For more on this workflow, see [Get fast feedback on
transformations](/howto/cookbooks/core/dev-iterative-workflow).

### Setup

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
%pip install -qU pixeltable
```

### Load images

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
import pixeltable as pxt

# Create a fresh directory (drop existing if present)
pxt.drop_dir('image_demo', force=True)
pxt.create_dir('image_demo')
```

<pre style={{ 'margin': '-20px 20px 0px 20px', 'padding': '0px', 'background-color': 'transparent', 'color': 'black' }}>
  Connected to Pixeltable database at: postgresql+psycopg://postgres:@/pixeltable?host=/Users/pjlb/.pixeltable/pgdata
  Created directory 'image\_demo'.
  \<pixeltable.catalog.dir.Dir at 0x13984d780>
</pre>

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
t = pxt.create_table('image_demo/images', {'image': pxt.Image})
```

<pre style={{ 'margin': '-20px 20px 0px 20px', 'padding': '0px', 'background-color': 'transparent', 'color': 'black' }}>
  Created table 'images'.
</pre>

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
t.insert(
    [
        {
            'image': 'https://raw.githubusercontent.com/pixeltable/pixeltable/main/docs/resources/images/000000000285.jpg'
        },
        {
            'image': 'https://raw.githubusercontent.com/pixeltable/pixeltable/main/docs/resources/images/000000000776.jpg'
        },
        {
            'image': 'https://raw.githubusercontent.com/pixeltable/pixeltable/main/docs/resources/images/000000000885.jpg'
        },
    ]
)
```

<pre style={{ 'margin': '-20px 20px 0px 20px', 'padding': '0px', 'background-color': 'transparent', 'color': 'black' }}>
  Inserting rows into \`images\`: 0 rows \[00:00, ? rows/s]
  Inserting rows into \`images\`: 3 rows \[00:00, 708.38 rows/s]
  Inserted 3 rows with 0 errors.
  3 rows inserted, 6 values computed.
</pre>

### Iterate: check image properties for a few images first

Use `.select()` to define the transformation, then `.collect()` to
execute and return results. If you want to collect only the first few
rows, use `.head(n)` instead of `.collect()`. Nothing is stored in your
table.

Pixeltable includes these built-in functions for image properties:

* `.height` - Get image height in pixels
* `.width` - Get image width in pixels
* `.mode` - Get color mode (RGB, RGBA, L for grayscale, etc.)

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Preview the properties
t.select(t.image, t.image.height, t.image.width, t.image.mode).collect()
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[0] }} />

### Add: check image properties for all images in your table

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Save as computed columns
t.add_computed_column(height=t.image.height)
t.add_computed_column(width=t.image.width)
t.add_computed_column(mode=t.image.mode)  # RGB, RGBA, L (grayscale), etc.
```

<pre style={{ 'margin': '-20px 20px 0px 20px', 'padding': '0px', 'background-color': 'transparent', 'color': 'black' }}>
  Added 3 column values with 0 errors.
  Added 3 column values with 0 errors.
  Added 3 column values with 0 errors.
  3 rows updated, 6 values computed.
</pre>

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# View images with computed height, width, and mode columns
t.collect()
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[1] }} />

### Iterate: resize a few images first

Use `.select()` to define the transformation, then `.collect()` to
execute and return results. If you want to collect only the first few
rows, use `.head(n)` instead of `.collect()`. Nothing is stored in your
table.

Pixeltable includes a built-in function for resizing image files with
PIL:

* `.resize(width, height)` - Change image dimensions

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Preview the resize operation
t.select(t.image, t.image.resize((224, 224))).head(1)
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[2] }} />

### Add: resize all images in your table

Once you’re satisfied with the results, use `.add_computed_column()`
with the same expression. This processes all rows and stores the results
permanently in your table.

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Save as computed column
t.add_computed_column(resized=t.image.resize((224, 224)))
```

<pre style={{ 'margin': '-20px 20px 0px 20px', 'padding': '0px', 'background-color': 'transparent', 'color': 'black' }}>
  Added 3 column values with 0 errors.
  3 rows updated, 3 values computed.
</pre>

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# View images with resized column
t.collect()
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[3] }} />

### Iterate: rotate a few images first

Use `.select()` to define the transformation, then `.collect()` to
execute and return results. If you want to collect only the first few
rows, use `.head(n)` instead of `.collect()`. Nothing is stored in your
table.

Pixeltable includes a built-in function for rotating image files with
PIL:

* `.rotate(degrees)` - Rotate image by specified degrees

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Preview the rotation
t.select(t.image, t.image.rotate(90)).head(1)
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[4] }} />

### Add: rotate all images in your table

Once you’re satisfied with the results, use `.add_computed_column()`
with the same expression. This processes all rows and stores the results
permanently in your table.

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Save as computed column
t.add_computed_column(rotated=t.image.rotate(90))
```

<pre style={{ 'margin': '-20px 20px 0px 20px', 'padding': '0px', 'background-color': 'transparent', 'color': 'black' }}>
  Added 3 column values with 0 errors.
  3 rows updated, 3 values computed.
</pre>

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# View images with rotated column
t.collect()
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[5] }} />

### Iterate: flip a few images first

Use `.select()` to define the transformation, then `.collect()` to
execute and return results. If you want to collect only the first few
rows, use `.head(n)` instead of `.collect()`. Nothing is stored in your
table.

Pixeltable includes a built-in function for transposing image files with
PIL (note that for this transform you will need import PIL to access the
`FLIP_*` constants):

* `.transpose(Image.FLIP_TOP_BOTTOM)` - Flip image vertically
* `.transpose(Image.FLIP_LEFT_RIGHT)` - Mirror image horizontally

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Import PIL Image to access flip constants
from PIL import Image

# Preview both flip operations
t.select(
    t.image,
    t.image.transpose(Image.FLIP_TOP_BOTTOM),
    t.image.transpose(Image.FLIP_LEFT_RIGHT),
).head(1)
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[6] }} />

### Add: flip all images in your table

Once you’re satisfied with the results, use `.add_computed_column()`
with the same expression. This processes all rows and stores the results
permanently in your table.

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Flip vertically (top to bottom)
t.add_computed_column(flip_v=t.image.transpose(Image.FLIP_TOP_BOTTOM))

# Flip horizontally (left to right, mirror effect)
t.add_computed_column(flip_h=t.image.transpose(Image.FLIP_LEFT_RIGHT))
```

<pre style={{ 'margin': '-20px 20px 0px 20px', 'padding': '0px', 'background-color': 'transparent', 'color': 'black' }}>
  Added 3 column values with 0 errors.
  Added 3 column values with 0 errors.
  3 rows updated, 3 values computed.
</pre>

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# View original and flipped versions side by side
t.select(t.image, t.flip_v, t.flip_h).collect()
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[7] }} />

### Iterate: crop a few images first

Use `.select()` to define the transformation, then `.collect()` to
execute and return results. If you want to collect only the first few
rows, use `.head(n)` instead of `.collect()`. Nothing is stored in your
table.

Pixeltable includes a built-in function for cropping image files with
PIL:

* `.crop(box)` - Extract a rectangular region from the image (box
  format: `(left, top, right, bottom)`)

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Preview the center crop
# Box format: (left, top, right, bottom)
t.select(
    t.image,
    t.image.crop(
        (
            t.image.width // 4,
            t.image.height // 4,
            3 * t.image.width // 4,
            3 * t.image.height // 4,
        )
    ),
).head(1)
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[8] }} />

### Add: crop all images in your table

Once you’re satisfied with the results, use `.add_computed_column()`
with the same expression. This processes all rows and stores the results
permanently in your table.

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Save as computed column
t.add_computed_column(
    center_crop=t.image.crop(
        (
            t.image.width // 4,
            t.image.height // 4,
            3 * t.image.width // 4,
            3 * t.image.height // 4,
        )
    )
)
```

<pre style={{ 'margin': '-20px 20px 0px 20px', 'padding': '0px', 'background-color': 'transparent', 'color': 'black' }}>
  Added 3 column values with 0 errors.
  3 rows updated, 3 values computed.
</pre>

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# View center-cropped images
t.select(t.center_crop).collect()
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[9] }} />

## Explanation

**How PIL transformations work in Pixeltable:**

Pixeltable provides built-in functions that wrap PIL (Pillow) operations
for image manipulation. These functions work directly on image columns
in your table—no need to write loops or manage file I/O. When you call
`.resize()`, `.rotate()`, or other methods on an image column,
Pixeltable handles applying the transformation to each image
automatically.

All these transformations use standard PIL operations under the hood.
For more details on PIL functionality, see the [Pillow
documentation](https://pillow.readthedocs.io/).

**To customize transformations:**

* **Resize**: Change dimensions with `.resize((width, height))` -
  specify target size in pixels
* **Rotate**: Rotate counterclockwise with `.rotate(degrees)` - use
  negative values for clockwise rotation
* **Flip**: Use `.transpose(Image.FLIP_LEFT_RIGHT)` for horizontal
  mirror or `.transpose(Image.FLIP_TOP_BOTTOM)` for vertical flip
* **Crop**: Extract regions with `.crop((left, top, right, bottom))` -
  coordinates are in pixels from top-left origin
* **Properties**: Access `.width`, `.height`, and `.mode` to get image
  dimensions and color mode

**The Pixeltable workflow:**

In traditional databases, `.select()` just picks which columns to view.
In Pixeltable, `.select()` also lets you compute new transformations on
the fly—define new columns without storing them. This makes `.select()`
perfect for testing transformations before you commit them.

When you use `.select()`, you’re creating a query that doesn’t execute
until you call `.collect()`. You must use `.collect()` to execute the
query and return results—nothing is stored in your table. If you want to
collect only the first few rows, use `.head(n)` instead of `.collect()`
to test on a subset before processing your full dataset. Once satisfied,
use `.add_computed_column()` with the same expression to persist results
permanently.

For more on this workflow, see [Get fast feedback on
transformations](/howto/cookbooks/core/dev-iterative-workflow).

## See also

* [Convert RGB images to
  grayscale](/howto/cookbooks/images/img-rgb-to-grayscale)
* [Apply filters to
  images](/howto/cookbooks/images/img-apply-filters)
* [Test transformations with fast feedback
  loops](/howto/cookbooks/core/dev-iterative-workflow)
