score.py是FCN中用于测试测试集/验证集的,并输出相应的像素准确度、平均准确度、mean IU和频率加权交并比(frequency weighted IU)四个指标的python文件。
score.py的源码如下:
from __future__ import division
import caffe
import numpy as np
import os
import sys
from datetime import datetime
from PIL import Image
def fast_hist(a, b, n):
k = (a >= 0) & (a < n)
return np.bincount(n * a[k].astype(int) + b[k], minlength=n**2).reshape(n, n)
def compute_hist(net, save_dir, dataset, layer='score', gt='label'):
n_cl = net.blobs[layer].channels
if save_dir:
os.mkdir(save_dir)
hist = np.zeros((n_cl, n_cl))
loss = 0
for idx in dataset:
net.forward()
hist += fast_hist(net.blobs[gt].data[0, 0].flatten(),
net.blobs[layer].data[0].argmax(0).flatten(),
n_cl)
if save_dir:
im = Image.fromarray(net.blobs[layer].data[0].argmax(0).astype(np.uint8), mode='P')
im.save(os.path.join(save_dir, idx + '.png'))
# compute the loss as well
loss += net.blobs['loss'].data.flat[0]
return hist, loss / len(dataset)
def seg_tests(solver, save_format, dataset, layer='score', gt='label'):
print '>>>', datetime.now(), 'Begin seg tests'
solver.test_nets[0].share_with(solver.net)
do_seg_tests(solver.test_nets[0], solver.iter, save_format, dataset, layer, gt)
def do_seg_tests(net, iter, save_format, dataset, layer='score', gt='label'):
n_cl = net.blobs[layer].channels
if save_format:
save_format = save_format.format(iter)
hist, loss = compute_hist(net, save_format, dataset, layer, gt)
# mean loss
print '>>>', datetime.now(), 'Iteration', iter, 'loss', loss
# overall accuracy
acc = np.diag(hist).sum() / hist.sum()
print '>>>', datetime.now(), 'Iteration', iter, 'overall accuracy', acc
# per-class accuracy
acc = np.diag(hist) / hist.sum(1)
print '>>>', datetime.now(), 'Iteration', iter, 'mean accuracy', np.nanmean(acc)
# per-class IU
iu = np.diag(hist) / (hist.sum(1) + hist.sum(0) - np.diag(hist))
print '>>>', datetime.now(), 'Iteration', iter, 'mean IU', np.nanmean(iu)
freq = hist.sum(1) / hist.sum()
print '>>>', datetime.now(), 'Iteration', iter, 'fwavacc', \
(freq[freq > 0] * iu[freq > 0]).sum()
return hist
详细解读如下:
(1)fast_hist()函数
'''
产生n×n的分类统计表
参数a:标签图(转换为一行输入),即真实的标签
参数b:score层输出的预测图(转换为一行输入),即预测的标签
参数n:类别数
'''
def fast_hist(a, b, n):
#k为掩膜(去除了255这些点(即标签图中的白色的轮廓),其中的a>=0是为了防止bincount()函数出错)
k = (a >= 0) & (a < n)
#bincount()函数用于统计数组内每个非负整数的个数
#详见https://docs.scipy.org/doc/numpy/reference/generated/numpy.bincount.html
return np.bincount(n * a[k].astype(int) + b[k], minlength=n**2).reshape(n, n)
此函数用于产生n*n的分类统计表,还不理解的可以看如下分析:
假如输入的标签图a是3*3的,如下左图,图中的数字表示该像素点的归属,即每个像素点所属的类别(其中n=3,即共有三种类别);预测标签图b的大小和a相同,如右图所示(图中的数字也代表每个像素点的类别归属)。
直观上看,b中预测的标签有两个像素点预测出错,即
\[b_{01},b_{20}\]
源码中的这句语句是精华:np.bincount(n * a[k].astype(int) + b[k], minlength=n**2)
其作用是产生一行n*n个元素的向量,向量中的每个元素存储统计结果,假如该向量为d,则其中的d(i*n+j)表示预测结果为类别j,实际标签为类别i的所有像素点的数目。
将上述的a、b和n输入fast_hist(a, b, n),所产生的d为:d=(3,0,0,0,2,1,0,1,2),其中的d(1*3+1)=d(4)表示预测类别为1,实际标签也为1的所有像素点数目为2。
通过reshape(n, n)将向量d转换为3*3的矩阵,其结果如下表(该矩阵即为下表中的绿色部分):
其中绿色的3*3表格统计的含义,拿数字3所在的这一格为例,即预测标签中被预测为类别0的且其真实标签也为0的所有像素点数目之和。
上述表格有几点需要注意的是(这三条是用于计算一开始所讲的四个指标的基础):
①绿色表格中对角线元素上的数字即为该类别预测正确的像素点数目,非对角线元素都是预测错误的,拿最后一行的数字1为例,其含义即为有一个原本应属于类别2的像素点被错误地预测为类别1;
②绿色表格的每一行求和得到的数字的含义是真实标签中属于某一类别的所有像素点数目,拿第一行为例,3+0+0=3,即真实属于类别0的像素点一共3个;
③绿色表格的每一列求和得到的数字的含义是预测为某一类别的所有像素点数目,拿第二列为例,0+2+1=3,即预测为类别1的所有像素点共有3个。
(2)compute_hist()函数
调用fast_hist()函数,遍历测试集/验证集中的所有样本,统计总的分类统计表和loss。
def compute_hist(net, save_dir, dataset, layer='score', gt='label'):
n_cl = net.blobs[layer].channels #score层的特征图数目(也即类别数,例如VOC数据集,这里就为21)
if save_dir:
os.mkdir(save_dir) #创建目录
hist = np.zeros((n_cl, n_cl)) #创建一个n_cl×n_cl大小的零矩阵,用于存储分类统计表
loss = 0 #初始化损失
#循环统计每一张测试/验证图片的预测结果,并求和保存在hist中
for idx in dataset:
net.forward()
#net.blobs[gt].data[0, 0]存放着H×W大小的标签图数据
#net.blobs[layer].data[0]存放着C×H×W大小的预测图数据(共C张),并通过argmax()获得最终的
#H×W大小的预测图数据(数据范围和标签图一致)(argmax本身得到的是最大值处的数组索引号)
#flatten()函数平铺整个数组为一行
hist += fast_hist(net.blobs[gt].data[0, 0].flatten(),
net.blobs[layer].data[0].argmax(0).flatten(),
n_cl)
if save_dir:
#mode='P'表示产生一张单通道的彩色图
#(详见http://pillow.readthedocs.io/en/3.1.x/handbook/concepts.html#concept-modes)
im = Image.fromarray(net.blobs[layer].data[0].argmax(0).astype(np.uint8), mode='P')
im.save(os.path.join(save_dir, idx + '.png'))
# compute the loss as well
loss += net.blobs['loss'].data.flat[0] #累加每张测试图的loss
return hist, loss / len(dataset)
从最后一句 return hist, loss / len(dataset)也可以看出,返回的loss是总的loss除以总的样本数。
还有比较惊喜的一点是这里的im = Image.fromarray(net.blobs[layer].data[0].argmax(0).astype(np.uint8), mode='P')很有启发,这句语句采用mode='P'的形式生成一张单通道的彩色图(实际就是标签图),通过自行编写的代码验证,这个应该就是VOC数据集中产生标签图的一种方法。
代码如下:
import numpy as np
from PIL import Image
im = Image.open('C:/Users/Zheng Chen/Desktop/2007_000392.png')
in_ = np.array(im, dtype=np.uint8)
print in_
out = Image.fromarray(in_, mode='P')
im.save('C:/Users/Zheng Chen/Desktop/2008.png')
结果如下图(左图为原标签图,右图为上部分代码生成的标签图,可以说是一模一样)
(3)seg_tests()函数
此函数是score.py文件的入口,即调用此函数便能完成整个测试,并输出四个指标值,此函数的调用可以参见FCN中的solve.py文件中的最后一部分。
#调用此函数,完成分割测试
def seg_tests(solver, save_format, dataset, layer='score', gt='label'):
print '>>>', datetime.now(), 'Begin seg tests'
solver.test_nets[0].share_with(solver.net)
do_seg_tests(solver.test_nets[0], solver.iter, save_format, dataset, layer, gt)
(4)do_seg_tests()函数
此函数在compute_hist的基础上,计算出一开始就提出的四个评价指标。
def do_seg_tests(net, iter, save_format, dataset, layer='score', gt='label'):
n_cl = net.blobs[layer].channels #score层的特征图数目(也即类别数,例如VOC数据集,这里就为21)
if save_format:
save_format = save_format.format(iter)
#调用compute_hist统计分割结果
hist, loss = compute_hist(net, save_format, dataset, layer, gt)
# mean loss 平均loss(即所有测试集总体误差/测试集数目)
print '>>>', datetime.now(), 'Iteration', iter, 'loss', loss
# overall accuracy 总体准确度(hist对角线为正确分类结果,其余均为错误分类结果)
#np.diag()详见https://docs.scipy.org/doc/numpy/reference/generated/numpy.diag.html
acc = np.diag(hist).sum() / hist.sum()
print '>>>', datetime.now(), 'Iteration', iter, 'overall accuracy', acc
# per-class accuracy 每一类的准确度
acc = np.diag(hist) / hist.sum(1) #/为对应位相除;sum(1)为按行求和
#输出平均准确度(注意平均准确度与总体准确度但区别)
#np.nanmean()详见https://docs.scipy.org/doc/numpy/reference/generated/numpy.nanmean.html
print '>>>', datetime.now(), 'Iteration', iter, 'mean accuracy', np.nanmean(acc)
# per-class IU 每一类的交并比
#sum(0)为按列求和
iu = np.diag(hist) / (hist.sum(1) + hist.sum(0) - np.diag(hist))
#输出平均交并比
print '>>>', datetime.now(), 'Iteration', iter, 'mean IU', np.nanmean(iu)
freq = hist.sum(1) / hist.sum()
#输出频率加权交并比(frequency weighted IU)
print '>>>', datetime.now(), 'Iteration', iter, 'fwavacc', \
(freq[freq > 0] * iu[freq > 0]).sum()
return hist
在此,介绍一下FCN中用到的四个评价指标(对应FCN论文中的第5部分(Results部分))。
①像素准确度(对应源码解析中的overall accuracy):
\[\sum _{i}n_{ii}/\sum _{i}t_{i}\]
②平均准确度(对应源码中的mean accuracy):
\[(\frac{1}{n_{cl}})\sum _{i}n_{ii}/t_{i}\]
③mean IU(平均交并比):
\[(\frac{1}{n_{cl}})\sum _{i}\frac{n_{ii}}{t_{i}+\sum _{j}n_{ji}-n_{ii}}\]
④频率加权交并比(frequency weighted IU,对应源码中的fwavacc):
\[(\frac{1}{\sum _{k}t_{k}})\sum _{i}\frac{t_{i}n_{ii}}{t_{i}+\sum _{j}n_{ji}-n_{ii}}\]
其中nij为第i类像素被预测属于第j类的数目;ncl为类别数;
\[t_{i}=\sum _{j}n_{ij}\]
表示属于第i类的所有像素数目
对应do_seg_tests()函数中的源码,相信大家肯定能更好理解和掌握这四个指标的计算技巧。
其中还有一点,就是交并比IU,为所有真实属于第i类的像素点所组成的集合A与所有预测属于第i类的像素点所组成的集合B的交集和并集之比,如下图