[Week 2]Princeton Algorithm PartII SeamCarving

回顾

第二周主要内容仍然是关于图的算法,主要内容为:

编程作业是SeamCarver:原题地址

题目

SeamCarving是一种调整图像尺寸的算法:从一幅图像中选出最不重要的像素并删去,在尽可能保留图像内容的情况下改变图像的尺寸。

original image
resized image

上图就是SeamCarving算法的应用,原图尺寸为505-by-287,变换后的图片尺寸为355-by-287。虽然尺寸变了,但是没有发生拉伸扭曲,保留了原图的特征。

算法步骤:

  1. 计算每个像素点的权重
  2. 找到水平(垂直)方向上的权重最小的像素序列,称为seam
  3. 移除seam
vertical seam

备注:

  • 像素点的权重计算使用dual-gradient energy function,具体计算方法在原题中有。
  • 从像素点(i,j)出发(假设垂直方向)只能连接到下一行的相邻三个像素(i-1,j+1),(i,j+1),(i+1,j+1)
  • 坐标系与默认的不同:(i,j)表示第j行,第i
a 3-by-4 image
  (0, 0)      (1, 0)      (2, 0)  
  (0, 1)      (1, 1)      (2, 1)  
  (0, 2)      (1, 2)      (2, 2)  
  (0, 3)      (1, 3)      (2, 3)  

API:

public class SeamCarver {
   public SeamCarver(Picture picture)                // create a seam carver object based on the given picture
   public Picture picture()                          // current picture
   public     int width()                            // width of current picture
   public     int height()                           // height of current picture
   public  double energy(int x, int y)               // energy of pixel at column x and row y
   public   int[] findHorizontalSeam()               // sequence of indices for horizontal seam
   public   int[] findVerticalSeam()                 // sequence of indices for vertical seam
   public    void removeHorizontalSeam(int[] seam)   // remove horizontal seam from current picture
   public    void removeVerticalSeam(int[] seam)     // remove vertical seam from current picture
}

解析

  • 图像的表示使用alg4.jar中的Picture类。但是构造这个类的开销很大,在内部用一个二维数组作为类成员变量来表示图像中每个点,数组中存的值为此像素点int类型的rbg值。在实现算法的时候使用此二维数组,只在public Picture picture()方法中生成Picture对象并返回。

  • 核心算法是找到某一方向上的seam,下面一步步思考:

    1. 首先可以把图像抽象成一个有向图,顶点是每一个像素点,每个顶点有三条边指向下一行(列)的相邻顶点。
    2. seam即是一条最短路径,权值就是最短路径上所有像素点的权重。
    3. 这是一个无环有向图(DAG),从时间复杂度考虑应该使用无环有向图的最短路径算法,而不是Dijkstra算法。
    4. 因此需要得到拓扑顺序,在这里不需要使用深度优先搜索来计算。以垂直方向为例,自上而下,每一行的拓扑顺序先于下一行,而每一行中各个顶点的顺序无关紧要
    5. 因此在最短路径算法中,可以直接for循环自上而下遍历所有顶点,进行松弛(relax)操作。
  • 水平和垂直方向:两个不同方向的算法实质是一样的,只要先对表示图像的二维数组进行转置,就能够复用代码。
    java中二维数组实际是由一维数组的每个元素表示其他各个一维数组,根据题意,垂直方向的像素作为第二层数组,方便用System.arraycopy()来整体移动,因此我们实现removeHorizontalSeam(int[] seam)方法:即水平方向上每行移除一个像素点,再整体移动剩余像素点;而对于removeVerticalSeam(int[] seam)方法,只要转置二维数组、调用removeHorizontalSeam(int[] seam)、再转置二维数组。

代码

成员变量:

private int[][] colors;

构造方法:

public Picture picture() {
        Picture picture = new Picture(colors.length, colors[0].length);
        for (int i = 0; i < colors.length; i++) {
            for (int j = 0; j < colors[0].length; j++) {
                Color color = new Color(this.colors[i][j]);
                picture.set(i, j, color);
            }
        }
        return picture;
    }

width()height()

public int width() {
    return this.colors.length;
}

public int height() {
    return this.colors[0].length;
}

energy():使用dual-gradient energy function来计算

public double energy(int x, int y) {
        if (x < 0 || x > this.width() - 1 || y < 0 || y > this.height() - 1) {
            throw new IndexOutOfBoundsException();
        }
        if (x == 0 || x == this.width() - 1 || y == 0 || y == this.height() - 1) {
            return 1000.0;
        } else {
            int deltaXRed = red(colors[x - 1][y]) -
                    red(colors[x + 1][y]);
            int deltaXGreen = green(colors[x - 1][y]) -
                    green(colors[x + 1][y]);
            int deltaXBlue = blue(colors[x - 1][y]) -
                    blue(colors[x + 1][y]);

            int deltaYRed = red(colors[x][y - 1]) - red(colors[x][y + 1]);
            int deltaYGreen = green(colors[x][y - 1]) - green(colors[x][y + 1]);
            int deltaYBlue = blue(colors[x][y - 1]) - blue(colors[x][y + 1]);

            return Math.sqrt(Math.pow(deltaXRed, 2) + Math.pow(deltaXBlue, 2) + Math.pow(deltaXGreen, 2) + Math.pow(deltaYRed, 2) + Math.pow(deltaYBlue, 2) + Math.pow(deltaYGreen, 2));
        }

    }

findVerticalSeam()

  1. 先计算所有顶点的distTo值,即从第一行到此顶点的最短路径上所有顶点权值之和
  2. 找出最后一行中distTo值最小的顶点,此顶点属于seam
  3. 根据nodeTo,逐行逆向找到每个属于seam的顶点
public int[] findVerticalSeam() {
        int n = this.width() * this.height();
        int[] seam = new int[this.height()];
        int[] nodeTo = new int[n];
        double[] distTo = new double[n];
        for (int i = 0; i < n; i++) {
            if (i < width())
                distTo[i] = 0;
            else
                distTo[i] = Double.POSITIVE_INFINITY;
        }
        for (int i = 0; i < height(); i++) {
            for (int j = 0; j < width(); j++) {
                for (int k = -1; k <= 1; k++) {
                    if (j + k < 0 || j + k > this.width() - 1 || i + 1 < 0 || i + 1 > this.height() - 1) {
                        continue;
                    } else {
                        if (distTo[index(j + k, i + 1)] > distTo[index(j, i)] + energy(j, i)) {
                            distTo[index(j + k, i + 1)] = distTo[index(j, i)] + energy(j, i);
                            nodeTo[index(j + k, i + 1)] = index(j, i);
                        }
                    }
                }
            }
        }

        // find min dist in the last row
        double min = Double.POSITIVE_INFINITY;
        int index = -1;
        for (int j = 0; j < width(); j++) {
            if (distTo[j + width() * (height() - 1)] < min) {
                index = j + width() * (height() - 1);
                min = distTo[j + width() * (height() - 1)];
            }
        }

        // find seam one by one
        for (int j = 0; j < height(); j++) {
            int y = height() - j - 1;
            int x = index - y * width();
            seam[height() - 1 - j] = x;
            index = nodeTo[index];
        }

        return seam;
    }
    
    private int index(int x, int y) {
        return width() * y + x;
    }

findHorizontalSeam()

public int[] findHorizontalSeam() {
        this.colors = transpose(this.colors);
        int[] seam = findVerticalSeam();
        this.colors = transpose(this.colors);
        return seam;
    }
    

removeHorizontalSeam

public void removeHorizontalSeam(int[] seam) {
        if (height() <= 1) throw new IllegalArgumentException();
        if (seam == null) throw new NullPointerException();
        if (seam.length != width()) throw new IllegalArgumentException();

        for (int i = 0; i < seam.length; i++) {
            if (seam[i] < 0 || seam[i] > height() - 1)
                throw new IllegalArgumentException();
            if (i < width() - 1 && Math.pow(seam[i] - seam[i + 1], 2) > 1)
                throw new IllegalArgumentException();
        }

        int[][] updatedColor = new int[width()][height() - 1];
        for (int i = 0; i < seam.length; i++) {
            if (seam[i] == 0) {
                System.arraycopy(this.colors[i], seam[i] + 1, updatedColor[i], 0, height() - 1);
            } else if (seam[i] == height() - 1) {
                System.arraycopy(this.colors[i], 0, updatedColor[i], 0, height() - 1);
            } else {
                System.arraycopy(this.colors[i], 0, updatedColor[i], 0, seam[i]);
                System.arraycopy(this.colors[i], seam[i] + 1, updatedColor[i], seam[i], height() - seam[i] - 1);
            }

        }
        this.colors = updatedColor;
    }
    

removeVerticalSeam:转置后复用removeHorizontalSeam(int[] seam)

public void removeVerticalSeam(int[] seam) {
        this.colors = transpose(this.colors);
        removeHorizontalSeam(seam);
        this.colors = transpose(this.colors);
    }

最后是转置方法:

private int[][] transpose(int[][] origin) {
        if (origin == null) throw new NullPointerException();
        if (origin.length < 1) throw new IllegalArgumentException();
        int[][] result = new int[origin[0].length][origin.length];
        for (int i = 0; i < origin[0].length; i++) {
            for (int j = 0; j < origin.length; j++) {
                result[i][j] = origin[j][i];
            }
        }
        return result;
    }

成绩

ASSESSMENT SUMMARY

Compilation:  PASSED
API:          PASSED

Findbugs:     PASSED
Checkstyle:   FAILED (3 warnings)

Correctness:  31/31 tests passed
Memory:       7/7 tests passed
Timing:       6/6 tests passed

Aggregate score: 100.00%
[Compilation: 5%, API: 5%, Findbugs: 0%, Checkstyle: 0%, Correctness: 60%, Memory: 10%, Timing: 20%]

完整代码和测试用例在GitHub上,欢迎讨论
https://github.com/michael0905/SeamCarver

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容

  • 第一章 绪论 什么是数据结构? 数据结构的定义:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。 第二章...
    SeanCheney阅读 5,760评论 0 19
  • 不同图像灰度不同,边界处一般会有明显的边缘,利用此特征可以分割图像。需要说明的是:边缘和物体间的边界并不等同,边缘...
    大川无敌阅读 13,836评论 0 29
  • https://zh.visualgo.net/graphds 浅谈图形结构https://zh.visualgo...
    狼之独步阅读 4,139评论 0 0
  • 自由书写第十七篇 人生最大的痛苦是你抗拒无常,生老病死,乃人之常情 忘了读那一本书上说的,研究癌症病人有一个共同的...
    Wendy3阅读 198评论 0 0
  • 由于历史的原因,我们的国家贫困落后,而且被西方社会孤立了,我们不懂葡萄酒,不是因为我们愚昧无知,而是因为我们没有机...
    武破九荒阅读 719评论 3 1