语义分割之MIoU原理与实现

一、MIoU简介

MIoU全称为Mean Intersection over Union,平均交并比。可作为语义分割系统性能的评价指标。

P:Prediction预测值
G:Ground Truth真实值

MIoU

假设有3个类别,各类别iou值如下:

iou_1, iou_2, iou_3 = 0.6, 0.7, 0.75
num_class = 3
miou = sum([iou_1, iou_2, iou_3]) / num_class

二、IoU简介

交并比是预测值和真实值的交集和并集之比。


图取自网络

要预测的图像中有人、树、草等...
我们针对“人”这个类别来直观看下,什么是交并比
左上图和右上图,分别是标注好的真实值,和模型输出的预测值

交集:真实值和预测值的交集就是下面这张图:
蓝线圈的是真实值,红线圈的是预测值,黄色区域就是交集

并集:真实值和预测值的并集就是下面这张图:
蓝线圈的是真实值,红线圈的是预测值,黄色区域就是并集

混淆矩阵(Confusion Matrix)

混淆矩阵可以用于直观展示每个类别的预测情况。并能从中计算精确值(Accuracy)、精确率(Precision)、召回率(Recall)、交并比(IoU)。


混淆矩阵(图取自网络)

混淆矩阵是n*n的矩阵(n是类别),对角线上的是正确预测的数量。

  • 假设求dog的IoU
    请对照着附图和代码看


    dog_iou
true_dog = (7+2+28+111+18+801+13+17+0+3) # 上图绿框
predict_dog = (1+0+8+48+13+801+4+17+1+0) # 上图黄框
# 因为分母的801加了两次,因此要减一次
iou_dog = 801 / true_dog + predict_dog - 801

按照dog求IoU的方法,对每个类别进行求值,再求平均,就是语义分割模型的MIoU值。
理论上说,MIoU值越大(越接近1),模型效果越好。

P:Prediction预测值
G:Ground Truth真实值

MIoU

代码实现

因为numpy能基于数组计算,因此MIoU的求解非常简洁。

  • 生成混淆矩阵
import numpy as np
def fast_hist(a, b, n):
    """
    生成混淆矩阵
    a 是形状为(HxW,)的预测值
    b 是形状为(HxW,)的真实值
    n 是类别数    
    """
    # 确保a和b在0~n-1的范围内,k是(HxW,)的True和False数列
    k = (a >= 0) & (a < n)
    # 这句会返回混淆矩阵,具体在下面解释
    return np.bincount(n * a[k].astype(int) + b[k], minlength=n ** 2).reshape(n, n)

np.bincount(array)会返回array当中每个元素的个数

例如下面例子的[1, 3, 5]当中,0个0,2个1,0个2,1个3...以此类推

np.bincount([1, 3, 5, 1])
# 返回array([0, 2, 0, 1, 0, 1], dtype=int64)

从上例可以看出,返回值的长度是输入array中的最大值加1
最大值是5,返回的列表长度为6(5+1)

np.bincount(array, minlength)中,minlength就是限制返回列表的最小长度

np.bincount([1, 3, 5, 1], minlength=10)
# 返回array([0, 2, 0, 1, 0, 1, 0, 0, 0, 0], dtype=int64)

返回长度为10,末尾填充了0,0,0...

看回源代码,如果没有越界情况,a[k]可以看成a,b[k]可以看成b

    return np.bincount(n * a[k].astype(int) + b[k], minlength=n ** 2).reshape(n, n)
  • 举个例子:

前面8, 9, 4, 7, 6都预测正确
对于预测正确的像素来说,n * a + b就是对角线的值
假设n=10,有10类
n * a + b就是88, 99, 44, 77, 66

紧接着6预测成了5
因此n * a + b就是65

minlength=n**2是为了确保混淆矩阵一定是n*n的大小
因为最后reshape(n, n)
88, 99, 44, 77, 66就是对角线上的值(如下图红框),65就是预测错误,并且能真实反映把6预测成了5(如下图蓝框)


  • 计算IoU
def per_class_iou(hist):
    """
    hist传入混淆矩阵(n, n)
    """
    # 因为下面有除法,防止分母为0的情况报错
    np.seterr(divide="ignore", invalid="ignore")
    # 交集:np.diag取hist的对角线元素
    # 并集:hist.sum(1)和hist.sum(0)分别按两个维度相加,而对角线元素加了两次,因此减一次
    iou = np.diag(hist) / (hist.sum(1) + hist.sum(0) - np.diag(hist))
    # 把报错设回来
    np.seterr(divide="warn", invalid="warn")
    # 如果分母为0,结果是nan,会影响后续处理,因此把nan都置为0
    iou[np.isnan(res)] = 0.
    return iou

per_class_iou返回每个类别的iou,再求平均就能得到MIoU了。
下面是我进行封装后的代码块,欢迎讨论优化

def fast_hist(a, b, n):
    """
    Return a histogram that's the confusion matrix of a and b
    :param a: np.ndarray with shape (HxW,)
    :param b: np.ndarray with shape (HxW,)
    :param n: num of classes
    :return: np.ndarray with shape (n, n)
    """
    k = (a >= 0) & (a < n)
    return np.bincount(n * a[k].astype(int) + b[k], minlength=n ** 2).reshape(n, n)


def per_class_iu(hist):
    """
    Calculate the IoU(Intersection over Union) for each class
    :param hist: np.ndarray with shape (n, n)
    :return: np.ndarray with shape (n,)
    """
    np.seterr(divide="ignore", invalid="ignore")
    res = np.diag(hist) / (hist.sum(1) + hist.sum(0) - np.diag(hist))
    np.seterr(divide="warn", invalid="warn")
    res[np.isnan(res)] = 0.
    return res


class ComputeIoU(object):
    """
    IoU: Intersection over Union
    """

    def __init__(self):
        self.cfsmatrix = np.zeros((CONFIG.NUM_CLASSES, CONFIG.NUM_CLASSES), dtype="uint64")  # confusion matrix
        self.ious = dict()

    def get_cfsmatrix(self):
        return self.cfsmatrix

    def get_ious(self):
        self.ious = dict(zip(range(CONFIG.NUM_CLASSES), per_class_iu(self.cfsmatrix)))  # {0: iou, 1: iou, ...}
        return self.ious

    def get_miou(self, ignore=None):
        self.get_ious()
        total_iou = 0
        count = 0
        for key, value in self.ious.items():
            if isinstance(ignore, list) and key in ignore or \
                    isinstance(ignore, int) and key == ignore:
                continue
            total_iou += value
            count += 1
        return total_iou / count

    def __call__(self, pred, label):
        """
        :param pred: [N, H, W]
        :param label:  [N, H, W}
        Channel == 1
        """

        pred = pred.cpu().numpy()
        label = label.cpu().numpy()

        assert pred.shape == label.shape

        self.cfsmatrix += fast_hist(pred.reshape(-1), label.reshape(-1), CONFIG.NUM_CLASSES).astype("uint64")

调用方式:

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