DONG Yuxuan @ Aug 21, 2019 Asia/Shanghai

An algorithm to remove both vertical and horizontal curves found by the seam carving algorithm from an image.

Seam carving is an algorithm for content-aware image resizing. It functions by establishing a number of seams (paths of least importance) in an image and automatically removes seams to reduce image size. The basic idea can be demonstrated with the following image from Wikipedia^{1}:

Seam carving will find seams which are indicated by red lines:

After removing seams, we get the resulting image:

Most papers about seam carving consider only finding seams, not removing them, and consider only one direction (usually finding vertical seams). To fastly reduce the size of the image, we need to remove both vertical and horizontal seams simultaneously after finding them. This problem is not as easy as we intuitively think.

The original algorithm of seam carving^{2}^{3} can find only one seam each time but there is an improvement^{4} which can find multiple seams at one time and ensures two seams of the same direction doesn’t intersect. This article is based on the improved algorithm, discussing how to simultaneously remove these seams.

If there are only vertical or horizontal seams, removing them is simple. Taking the vertical case as an example, for any pixel `(i, j)`

in the input image, where `i`

is the row index, `j`

is the column index, it will be mapped to the pixel `(i, j - co(i, j))`

in the resulting image, where `co(i, j)`

is the number of vertical seams on the left of the pixel `(i, j)`

in the input image. Denoting the input image as `x`

, the resulting image as `y`

, and indexes starting from 0, we have:

```
y[i][j - co(i, j)] = x[i][j]
```

, for `i`

in `[0, x.height)`

, `j`

in `[0, x.width)`

, and `x[i][j]`

is not passed through by any seam.

Similarly, if there are only horizontal seams, we have:

```
y[i - ro(i, j)][j] = x[i][j]
```

, for `i`

in `[0, x.height)`

, `j`

in `[0, x.width)`

, and `x[i][j]`

is not passed through by any seam. Here `ro(i, j)`

is the number of horizontal seams above the pixel `(i, j)`

in `x`

.

Combining two cases, for both vertical and horizontal seams, we have:

```
y[i - ro(i, j)][j - co(i, j)] = x[i][j]
```

, for `i`

in `[0, x.height)`

, `j`

in `[0, x.width)`

, and `x[i][j]`

is not passed through by any seam.

However, this algorithm is not correct for two reasons. The first reason is that multiple pixels in `x`

may be mapped to the same pixel in `y`

. The second reason is that some positions in `y`

may not be mapped by any pixel in `x`

. In another word, the mapping from remained pixels in `x`

to pixels in `y`

, is neither injective nor surjective. These two reasons are not easy to see, we will show by examples below.

The example is a 4x4 image. To very microscopically observe it, we draw it as the following grids. Each grid represents a pixel of the image, and we draw seams on it, blue for the vertical, red for the horizontal. As the grids show, two pixels of the input image can be mapped to the same position. For pixel M(3, 0), there’re 2 horizontal seams above it and no vertical seams on the left of it, so we have `ro[3][0] = 2`

and `co[3][0] = 0`

, thus it will be mapped to (1, 0) in the resulting image. For pixel K(2, 2), we have `co[2][2] = 2`

and `ro[2][2] = 1`

, thus it will also be mapped to (1, 0).

Again, we take a 4x4 image presented in the following grids as an example. We label every pixel with letters and draw seams on it, blue for the vertical, red for the horizontal. Observe that every pixels right to or below F(1, 1) is removed by seams, so no pixel will be mapped to (1, 1).

Drawing the resulting image will make this more clear:

We can see that the result can not even form a rectangle.

As the mapping is neither injective nor surjective, we can modify it to make it be.

To make the mapping injective, we can randomly choose a pixel to map when we found multiple pixels are mapped to the same position. A simple strategy is to choose the most bottom-right pixel. To make this happen, we need to do nothing on the intuitive algorithm. Just run the intuitive algorithm on the input image from top to bottom, left to right.

To make the mapping surjective, we run a blank-fill algorithm after the intuitive algorithm. For position `(i, j)`

in `y`

, if it’s not mapped, we use `y[i - 1][j]`

or `y[i][j - 1]`

to fill it. Here we assume the position (0, 0) was mapped in the intuitive algorithm. The assumption is obviously true if seams don’t pass through all pixels in the input image.

This solution may not be the best one, but it’s simple, usable and works fine in my implementation Resize.fun. Resize.fun is an implementation of browser-side computing seam carving with very high performance. On my old machine (1.2 GHz Intel Core m5, macOS 10.14.6, Chrome 76), it can even do real-time resizing of a 1024x768 image with a slider UI.