Seam Carving is an image resizing technique that was discovered in 2007 and it is a feature in Photoshop. We will look at the implementation of this technique using directed graph data structure.
Below are some wikipedia links to the underlying data structre:
https://en.wikipedia.org/wiki/Directed_acyclic_graph
https://en.wikipedia.org/wiki/Shortest_path_problem
The goal is to remove a seam (vertical or horizontal) from the image one by one. To calculate a seam, we need to calculate the
energies of the pixels. Let's look at the energy method of our SeamCarver class:
public class SeamCarver {
private Picture picture;
//...
public double energy(int x, int y) {
if (x < 0 || x >= width) {
throw new IllegalArgumentException();
}
if (y < 0 || y >= height) {
throw new IllegalArgumentException();
}
if (x == 0 || y == 0 || x == width - 1 || y == height - 1) {
return 1000.;
}
int rx = picture.get(x + 1, y).getRed() - picture.get(x - 1, y).getRed();
int gx = picture.get(x + 1, y).getGreen() - picture.get(x - 1, y).getGreen();
int bx = picture.get(x + 1, y).getBlue() - picture.get(x - 1, y).getBlue();
double deltaSqrx = Math.pow(rx, 2.) + Math.pow(gx, 2.) + Math.pow(bx, 2.);
int ry = picture.get(x, y + 1).getRed() - picture.get(x, y - 1).getRed();
int gy = picture.get(x, y + 1).getGreen() - picture.get(x, y - 1).getGreen();
int by = picture.get(x, y + 1).getBlue() - picture.get(x, y - 1).getBlue();
double deltaSqry = Math.pow(ry, 2.) + Math.pow(gy, 2.) + Math.pow(by, 2.);
return Math.sqrt(deltaSqrx + deltaSqry);
}
So the energy of a pixel is about the RGB values and this is used to find a seam to remove from the image. This is the calculation that allows the content-aware resizing.
We see above the original image and lets see what the energies look like:
The red pixels are the pixels of a vertical seam. That seam will be removed from the image to shrink its width.
Now lets see an example vertical seam and the energies that are calculated:
As we see, we need to find the
shortest distance (minimum total energy) from top pixels to the bottom pixels. To do this, we need to model the problem as
finding a shortest path in a directed acyclic graph. So what are the directed edges between the pixels? There will be an edge between a pixel on the top and 3 neighbouring pixels. The weight will be the energy of the target pixel. What we can do is using a directed graph library:
public class SeamCarver {
private Picture picture;
private EdgeWeightedDigraph digraphVertical;
public SeamCarver(
Picture picture) { // create a seam carver object based on the given picture
this.picture = new Picture(picture);
initVertical(picture);
}
private void initVertical(Picture picture) {
digraphVertical = new EdgeWeightedDigraph(picture.width() * picture.height() + 1);
for (int col = 0; col < picture.width(); ++col) {
for (int row = 0; row < picture.height(); ++row) {
if (row + 1 >= picture.height()) {
continue;
}
if (col - 1 >= 0) {
digraphVertical.addEdge(new DirectedEdge(vertex(col, row), vertex(col - 1, row + 1), energy(col - 1, row + 1)));
}
digraphVertical.addEdge(new DirectedEdge(vertex(col, row), vertex(col, row + 1), energy(col, row + 1)));
if (col + 1 < picture.width()) {
digraphVertical.addEdge(new DirectedEdge(vertex(col, row), vertex(col + 1, row + 1), energy(col + 1, row + 1)));
}
}
}
Here we created a graph object with vertices as pixels and created directed edges between pixels. Now we need to use the shortest path algorithm on this graph to find the vertical seams:
public int[] findVerticalSeam() {
double minDist = Double.MAX_VALUE;
Iterable result = null;
for (int col = 0; col < picture.width(); ++col) {
int row = 0; // top
AcyclicSP acyclicSP = new AcyclicSP(digraphVertical, vertex(col, row));
for (int botCol = 0; botCol < picture.width(); ++botCol) {
int botRow = picture.height() - 1;
double curDist = acyclicSP.distTo(vertex(botCol, botRow));
if (curDist < minDist) {
minDist = curDist;
result = acyclicSP.pathTo(vertex(botCol, botRow));
}
}
}
int[] seam = new int[picture.height()];
int i = 0;
for (DirectedEdge edge : result) {
// System.out.println(edge.from() + "->" + edge.to());
if (i == 0) {
seam[i++] = colFromVertex(edge.from());
seam[i++] = colFromVertex(edge.to());
} else {
seam[i++] = colFromVertex(edge.to());
}
}
return seam;
}
Removing this calculated seam from the image is not difficult:
public void removeVerticalSeam(int[] seam) {
validate(seam);
Picture picture2 = new Picture(width - 1, height);
for (int row = 0; row < picture2.height(); ++row) {
for (int col = 0; col < picture2.width(); ++col) {
int seamItem = seam[row];
if (col + 1 >= picture.width()) {
continue;
}
if (col >= seamItem) {
picture2.set(col, row, picture.get(col + 1, row));
}
else {
picture2.set(col, row, picture.get(col, row));
}
}
}
this.picture = picture2;
initialize(this.picture);
}
Now this solution that uses a graph library works correctly but the performance is not good. Image size can be huge and we need to write our own algorithm implementation on pixels. So we will not use the library classes AcyclicSP and EdgeWeightedDigraph.
public class SeamCarver {
private Picture picture;
private double[] vertexEnergy;
public SeamCarver(Picture picture) {
if (picture == null) {
throw new IllegalArgumentException();
}
this.picture = new Picture(picture);
initialize(picture);
}
private void initialize(Picture picture) {
this.width = picture.width();
this.height = picture.height();
vertexEnergy = new double[width * height + 1];
for (int col = 0; col < picture.width(); ++col) {
for (int row = 0; row < picture.height(); ++row) {
double energy = energy(col, row);
vertexEnergy[vertex(col, row)] = energy;
}
}
}
Instead of creating a graph with DirectedEdge objects and energies as weights, I am creating an array of pixel energies.
To implement the shortest path algorithm on the pixels, I need edgeTo[] and distTo[] arrays. Then while traversing from top to bottom, I will perform
relaxation. Relaxation is the update of the path: when we see a shorter path, we update the distTo[] and edgeTo[] arrays.
public int[] findVerticalSeam() {
double minDist = Double.MAX_VALUE;
double[] distTo = new double[V];
int[] edgeTo = new int[V];
for (int i = 0; i < edgeTo.length; ++i) {
edgeTo[i] = -1;
}
for (int v = 0; v < V; v++)
distTo[v] = Double.POSITIVE_INFINITY;
for (int col = 0; col < this.width; ++col) {
int row = 0;
int vertex = vertex(col, row);
distTo[vertex] = vertexEnergy[vertex];
}
for (int row = 0; row < this.height; ++row) {
for (int col = 0; col < this.width; ++col) {
if (row == this.height - 1) {
// bottom row is already calculated
continue;
}
// a: col - 1, row + 1
// b: col, row + 1
//c: col + 1, row + 1
int a, b, c;
int s = vertex(col, row);
if (col - 1 >= 0) {
a = vertex(col - 1, row + 1);
relax(s, a, distTo, edgeTo);
}
b = vertex(col, row + 1);
relax(s, b, distTo, edgeTo);
if (col + 1 < this.width) {
c = vertex(col + 1, row + 1);
relax(s, c, distTo, edgeTo);
}
}
}
Stack result = null;
for (int botCol = 0; botCol < this.width; ++botCol) {
int botRow = this.height - 1;
double curDist = distTo[vertex(botCol, botRow)];
if (curDist < minDist) {
minDist = curDist;
result = pathTo((vertex(botCol, botRow)), edgeTo);
}
}
int[] seam = new int[this.height];
int i = 0;
for (Integer vertex : result) {
// System.out.println(edge.from() + "->" + edge.to());
seam[i++] = colFromVertex(vertex);
}
return seam;
}
The relax method is very important and it is a concept of shortest path algorithm for weighted directed acyclic graphs.
private void relax(int s, int v, double[] distTo, int[] edgeTo) {
if (distTo[v] > distTo[s] + vertexEnergy[v]) {
distTo[v] = distTo[s] + vertexEnergy[v];
edgeTo[v] = s;
}
}
So if the distance between source vertex "s" and the target vertex "v" is smaller than the existing distance to the target, then we update the edgeTo[] and distTo[] arrays. This means we have found a new shortest path.
NOTES:
- One of the applications of graph data structure is image processing.
- Sometimes using library classes is not enough and we need to understand in detail the underlying algorithm and how the library works.
- Then we can perform optimizations on the working solution, depending on our special needs.
- We started by using EdgeWeightedDigraph and AcyclicSP from Princeton University, then we wrote our own algorithm on pixels.
Comments