语义分割中的小算法——Color2Label

一、问题描述

  今天在学习语义分割的代码时, 遇到一个有趣的算法小问题, 本身实现并不复杂, 但不同方法得到的时间复杂度相去甚远, 故通过本文将探索过程详细记录下来。希望对日后遇到的相似问题有所启发。

  简要阐述一下问题: 众所周知, 语义分割是一个像素级的分类任务, 需要将输入图像的每一个像素划分到指定的类别。由于是逐像素分类, 故给定的标签常常是和输入图像尺寸一致的Colormap, 其中每一种颜色代表一个特定类别, 比如下面的例子:

Fig. 1. Pascal Voc数据集中的样例

  如上所述, 每个类别对应一个颜色, 其映射关系是在数据标注阶段设定的。比如上面右图中, 飞机上的每一个像素其像素值均为[128, 0, 0], 行人则用[192, 128, 128]表示。

VOC_COLORMAP = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],
                [0, 0, 128], [128, 0, 128], [0, 128, 128], [128, 128, 128],
                [64, 0, 0], [192, 0, 0], [64, 128, 0], [192, 128, 0],
                [64, 0, 128], [192, 0, 128], [64, 128, 128], [192, 128, 128],
                [0, 64, 0], [128, 64, 0], [0, 192, 0], [128, 192, 0],
                [0, 64, 128]]

VOC_CLASSES = ['background', 'aeroplane', 'bicycle', 'bird', 'boat',
               'bottle', 'bus', 'car', 'cat', 'chair', 'cow',
               'diningtable', 'dog', 'horse', 'motorbike', 'person',
               'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor']

  有了这个映射关系, 我们就能将每个像素对应的物体标号找出来。为了方便起见我们可以将类别标签VOC_CLASSES转换成[0, 1, 2, 3, 4,..., 19]表示的数值形式, 现在还有一个问题:

  Colormap形式的标签是一个RGB图像, 而根据我们的任务, 一个更理想的标签形式是一张与原图尺寸一致的2-D图像, 其每个像素值等于该像素的类别标签, 即[0, 1, 2,..., 19]中的一个数字。

  所以问题就是: 将RGB形式的COLORMAP图像, 转换成2-D形式的LabelMap。

二、解决方法

  这部分记录几个解决方法, 及其在单位图像上的耗时。

  我在这个小练习中用的数据是自己的数据, 不是VOC数据集; 刚开始实验是用MXNet的NDArray写的, 在函数中加了.asnumpy(), 就发现有的方法非常慢...后来才意识到, 因为nd转np本身就很慢。后来改成numpy的代码, 写博客也方便一些。(顺便吐槽下NDArray对于list的支持太差, 不支持nd.array(list)和ndarray.tolist()等常用方法...)

Fig. 2. 实验用的数据

2.1 暴力解法

  首先, 一种不动脑子的做法是遍历输入图像的每一个像素, 返回颜色对应的索引...

  首先定义一个映射函数:

def color2index(color):
    """
    color: List of [R, G, B]
    """
    if color in VerteColorMap:
        return VerteColorMap.index(color)
    else:
        return 0

  然后对输入图像的每一个像素进行处理...

def proc_one_img(img):
    h, w = img.shape[:2]
    idx = np.zeros((h, w))
    for i in range(h):
        for j in range(w):
            idx[i, j]=color2index(img[i, j].tolist())
    return idx

  运行时间记录:


Fig. 3. 方法一速度

  注意, 上面的color2index函数中第一行的in和第二行的.index可能做了重复的工作, 最近刚开始刷LeetCode, 也是经常这么写。一种可能的优化方法是用try-except代替if, 代码和结果如下:

Fig. 4. 方法一中用try-except代替if, 提升了一点点速度

2.2 map大法

  任何时候涉及到将作用于单个元素的函数扩展到这类元素的iterable对象时, 我都会想到map大法...

  使用map的思路很简单, 就是上面已经定义了一个处理单个像素位置(h, w)的函数color2index, 现在想要将该函数同时对整个图像所有像素点的位置进行处理。使用map函数的一个难点在于找到扩展的方式:

  首先观察color2index, 其输入为List of [R, G, B], 而一张普通图像'img'的shape是[H, W, C], 也就是img[i, j]的结果才是一个[R, G, B]的list。

  这里给出自己的经验: 比如刚开始我们不知道要把img弄成怎样的形式才能用map, 那就先假设转换后得到一个iterable对象, 则最后调用的命令为: map(color2index, iterable), 其中iterable是img调整过之后的形式, 那么我们可以用下面的代码检查该iterable是否为正确形式:

for item in iterable:
    lbl_value = color2index(item)

  显然, 针对这个例子, iterable的形式为: [HxW, C]

def color2index(color):
    """
    color: List of [R, G, B]
    """
    try:
        return VerteColorMap.index(color.tolist())
    except ValueError:
        return 0
    
def proc_one_img_v2(img):
    """
    img: Np.array with shape HxWxC
    
    """
    h, w = img.shape[:2]
    mask = np.array(list(map(color2index, img.reshape(-1, 3)))).reshape(h, w)
    return mask
Fig. 5. 方法二结果: map大法又提升了一些速度

2.3 最优解: 矩阵索引

   假如我们能够定义一个矩阵, 用其下标位置代表Uint8空间所有可能的颜色值(256x256x256种颜色), 每一个可能颜色值对应一个标签值(0, 1, 2...), 这样要找出某一个颜色值对应的标签, 只需要用上述矩阵对该颜色索引即可; 同样, 要找出一张图像中所有颜色对应的标签, 直接用该矩阵对该图像(对应的颜色值矩阵)进行索引即可。 掌握矩阵对子下标矩阵的索引是一个远比看起来要困难的任务, 下面举一个简单的例子作为示意。

Fig. 6. 矩阵a对下标子矩阵ind的索引示意

   为了使该映射矩阵形式更简单, 我们可以将传统的[R, G, B]颜色表示方法调整下, 改为用(Rx256x256 + Gx256 + B)一个整数表达的形式。注意这里不需要担心多个不同的[R,G,B]对应同一个整数, 也就是说这种映射关系是一一对应的。

### 现在我们来定义上述映射矩阵: 注意由于256**3数值较大, 需使用32位整数
mapMatrix = np.zeros(256*256*256, dtype=np.int32)
for i, cm in enumerate(VerteColorMap):
    mapMatrix[cm[0]*65536 + cm[1]*256 + cm[2]] = i

def SegColor2Label(img, mapMatrix):
    """
    img: Shape [h, w, 3]
    mapMatrix: color-> label mapping matrix, 
               覆盖了Uint8 RGB空间所有256x256x256种颜色对应的label
               
    return: labelMatrix: Shape [h, w], 像素值即类别标签
    """
    data = img.astype('int32')
    indices = data[:,:,0]*65536 + data[:,:,1]*256 + data[:,:,2]
    return mapMatrix[indices]

   运行时间记录:


Fig. 7. 方法三运行时间

   我担心这个时间测的不准, 特地把循环次数调成10000次, 还把小数点往后挪了点才找到有效数字...可以看到, 最优方法的速度是方法一的110倍....
   当然, 我也验证了下, 三种方法求出的矩阵mask1和mask2, mask3是一样的, 截图就不放了。

三、一点感想

  首先就是一个直观感受: 程序员了解的知识哪怕多一点点, 运用到程序中都可以带来明显的优化效果。如果啥都不知道, 那就只能用暴力法遍历...而在图像领域, 逐像素遍历很浮夸的行为, 一定要尽量避免。

  另外就是, 矩阵索引确实是非常强大的武器, 这个能力还需要长期的积累修炼才行。map在可迭代对象上也是一如既往的好用, 但是map感觉只是对循环过程提出一点优化, 真正质的提高还是需要更好的算法, 而更好的算法来源于知识的不断积累。

  TODO: 分析几种方法的时间复杂度。

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

推荐阅读更多精彩内容