【图像后处理】python实现全连接CRFs后处理
前言目前图像像素级语义分割比较流行使用深度学习全卷积神经网络FCN或者各种FCN的改进版U-Net、V-Net、SegNet等方法。这些模型中使用了反卷积层进行上采样操作,虽然能够将特征图恢复至原图尺寸,但也造成了特征的损失,自然而然产生了分类目标边界模糊的问题。为了得到更精确的最终分类结果,通常要进行一些图像后处理。全连接CRFs是在目前深度学习图像分割应用中常用的一种图像后处理方式,它是CRFs的改进模式,能够结合原始影像中所有像素之间的关系对深度学习得到的分类结果进行处理,优化分类图像中粗糙和不确定性的标记,修正细碎的错分区域,同时得到更细致的分割边界。
全连接CRFs原理
物体通常由大的空间相邻的区域表示,每个像素都拥有一个类别标签,而且还有一个对应观测值,这样在无向图中每个像素点都成为某一个节点,若使用像素与像素之间的关系作为边连接,这样就组成了条件随机场。最终目的就是使用观测值来推测这个像素i本来拥有的类别标签,如下图所示:
CRFs
全连接CRFs与稀疏CRFs的最大差别在于:每个像素点都与所有的像素点相连接构成连接边。如果一张图像大小是256*256,那么就相当于有65536个像素点,因此如果采用全连接CRFs的话,那么就会构造出65536*65536条边。如果图像大小再大一些,那么就会变得十分恐怖,普通条件随机场推理算法根本行不通。这时,文献《Efficient Inference in Fully Connected CRFs with Gaussian Edge Potentials》给出了快速推理算法。接着我们就简单讲解具体的求解算法。
全连接条件随机场符合吉布斯分布,如下公式所示:
https://www.zhihu.com/equation?tex=P%28x%3DX%7CI%29%3D%5Cfrac%7B1%7D%7BZ%28I%29%7De%5E%7B-E%28X%7CI%29%7D
其中 https://www.zhihu.com/equation?tex=x 是观测值, https://www.zhihu.com/equation?tex=E%28X%7CI%29 是能量函数,该能量函数由一元势函数和二元势函数构成,如下公式所示:
https://www.zhihu.com/equation?tex=E%28x%7CI%29%3D%5Csum_%7Bi%7D%5E%7B%7D%7B%5Cpsi_%7Bu%7D%28x_%7Bi%7D%29%7D%2B%5Csum_%7Bi%2Cj%7D%5E%7B%7D%7B%5Cpsi_%7Bp%7D%28x_%7Bi%7D%2Cy_%7Bj%7D%29%7D
其中的一元势函数用于衡量当像素点的观测值为时,该像素点属于类别标签的概率,来自卷积神经网络网络的后端输出。二元势函数用于衡量两事件同时发生的概率 https://www.zhihu.com/equation?tex=p%28x_%7Bi%7D%2Cy_%7Bi%7D%29 ,简单来说就是,我们希望两个相邻的像素点,如果颜色值、 https://www.zhihu.com/equation?tex=y_%7Bj%7D 非常接近,那么这两个像素点、 https://www.zhihu.com/equation?tex=x_%7Bj%7D 属于同一个类别的概率应该比较大才对;反之如果颜色差异比较大,那么我们分割的结果从这两个像素点裂开的概率应该比较大才对。这一个能量项正是为了让我们的分割结果尽量从图像边缘的地方裂开,也就是为了弥补前面所说的卷积神经网络分割的分类目标边界模糊的问题。计算公式如下公式所示:
https://www.zhihu.com/equation?tex=%5Cpsi_%7Bp%7D%28x_%7Bi%7D%2Cy_%7Bj%7D%29%3Du%28x_%7Bi%7D%2Cy_%7Bj%7D%29%5CSigma+w%5E%7Bm%7DK_%7BG%7D%5E%7Bm%7D%28f_%7Bi%7D%2Cf_%7Bj%7D%29
https://www.zhihu.com/equation?tex=u%28x_%7Bi%7D%2Cy_%7Bj%7D%29 叫做标签兼容项(Label Compatibility),它约束了像素间传导的条件,只有相同标签(label)条件下,能量才可以相互传导。后面的加和项中, https://www.zhihu.com/equation?tex=w%5E%7Bm%7D 是权值参数, https://www.zhihu.com/equation?tex=K_%7BG%7D%5E%7Bm%7D%28f_%7Bi%7D%2Cf_%7Bj%7D%29 是特征函数,如下公式所示:
https://www.zhihu.com/equation?tex=K_%7BG%7D%5E%7Bm%7D%28f_%7Bi%7D%2Cf_%7Bj%7D%29%3DW%5E%7B%281%29%7De%5E%7B-%5Cfrac%7B%7Cp_%7Bi%7D-p_%7Bj%7D%7C%5E%7B2%7D%7D%7B2%5Ctheta_%7B%5Calpha%7D%5E%7B2%7D%7D-%5Cfrac%7B%7CI_%7Bi%7D-I_%7Bj%7D%7C%5E%7B2%7D%7D%7B2%5Ctheta_%7B%5Cbeta%7D%5E%7B2%7D%7D%7D%2BW%5E%7B%282%29%7De%5E%7B-%5Cfrac%7B%7Cp_%7Bi%7D-p_%7Bj%7D%7C%5E%7B2%7D%7D%7B2%5Ctheta_%7B%5Cgamma%7D%5E%7B2%7D%7D%7D
公式以特征的形式表示了不同像素之前的“亲密度”,第一项被称作表面核,第二项被称作平滑核。
全连接条件随机场使用二元势函数解释了一个像素与另一个像素之间的关系,给像素关系紧密的两个像素赋予相同的类别标签,而关系相差很大的两个像素会赋予不同的类别标签,这个“关系”的判断与像素的颜色值、像素间的相对距离都有关系。全连接条件随机场中二元势函数解释了每一个像素与其他所有像素的关系,与条件随机场相比,“全连接”更加紧密一些。
在全连接CRFs进行影像后处理的实际操作中,一元势能为概率分布图,即由模型输出的特征图经过softmax函数运算得到的结果;二元势能中的位置信息和颜色信息由原始影像提供。当能量E(x)越小时,预测的类别标签X就越准确,我们通过迭代最小化能量函数,得到最终的后处理结果。
编写代码准备库
我们需要numpy库和skimage以及pydensecrf库
pydensecrf安装直接pip install pydensecrf命令就可以安装,但有时会报错,点击本文最后面网址查看解决方案
详细代码
import numpy as np
import pydensecrf.densecrf as dcrf
try:
from cv2 import imread, imwrite
except ImportError:
# 如果没有安装OpenCV,就是用skimage
from skimage.io import imread, imsave
imwrite = imsave
from pydensecrf.utils import unary_from_labels, create_pairwise_bilateral, create_pairwise_gaussian
"""
original_image_path原始图像路径
predicted_image_path之前用自己的模型预测的图像路径
CRF_image_path即将进行CRF后处理得到的结果图像保存路径
"""
def CRFs(original_image_path,predicted_image_path,CRF_image_path):
print("original_image_path: ",original_image_path)
img = imread(original_image_path)
# 将predicted_image的RGB颜色转换为uint32颜色 0xbbggrr
anno_rgb = imread(predicted_image_path).astype(np.uint32)
anno_lbl = anno_rgb[:,:,0] + (anno_rgb[:,:,1] << 8) + (anno_rgb[:,:,2] << 16)
# 将uint32颜色转换为1,2,...
colors, labels = np.unique(anno_lbl, return_inverse=True)
# 如果你的predicted_image里的黑色(0值)不是待分类类别,表示不确定区域,即将分为其他类别
# 那么就取消注释以下代码
#HAS_UNK = 0 in colors
#if HAS_UNK:
#colors = colors
# 创建从predicted_image到32位整数颜色的映射。
colorize = np.empty((len(colors), 3), np.uint8)
colorize[:,0] = (colors & 0x0000FF)
colorize[:,1] = (colors & 0x00FF00) >> 8
colorize[:,2] = (colors & 0xFF0000) >> 16
# 计算predicted_image中的类数。
n_labels = len(set(labels.flat))
#n_labels = len(set(labels.flat)) - int(HAS_UNK) ##如果有不确定区域,用这一行代码替换上一行
###########################
### 设置CRF模型 ###
###########################
use_2d = False
# use_2d = True
###########################################################
##不是很清楚什么情况用2D
##作者说“对于图像,使用此库的最简单方法是使用DenseCRF2D类”
##作者还说“DenseCRF类可用于通用(非二维)密集CRF”
##但是根据我的测试结果一般情况用DenseCRF比较对
#########################################################33
if use_2d:
# 使用densecrf2d类
d = dcrf.DenseCRF2D(img.shape, img.shape, n_labels)
# 得到一元势(负对数概率)
U = unary_from_labels(labels, n_labels, gt_prob=0.2, zero_unsure=None)
#U = unary_from_labels(labels, n_labels, gt_prob=0.2, zero_unsure=HAS_UNK)## 如果有不确定区域,用这一行代码替换上一行
d.setUnaryEnergy(U)
# 增加了与颜色无关的术语,只是位置-----会惩罚空间上孤立的小块分割,即强制执行空间上更一致的分割
d.addPairwiseGaussian(sxy=(3, 3), compat=3, kernel=dcrf.DIAG_KERNEL,
normalization=dcrf.NORMALIZE_SYMMETRIC)
# 增加了颜色相关术语,即特征是(x,y,r,g,b)-----使用局部颜色特征来细化它们
d.addPairwiseBilateral(sxy=(80, 80), srgb=(13, 13, 13), rgbim=img,compat=10,
kernel=dcrf.DIAG_KERNEL,
normalization=dcrf.NORMALIZE_SYMMETRIC)
&#39;&#39;&#39;
addPairwiseGaussian函数里的sxy为公式中的 $\theta_{\gamma}$,
addPairwiseBilateral函数里的sxy、srgb为$\theta_{\alpha}$ 和 $\theta_{\beta}$
&#39;&#39;&#39;
else:
# 使用densecrf类
d = dcrf.DenseCRF(img.shape * img.shape, n_labels)
# 得到一元势(负对数概率)
U = unary_from_labels(labels, n_labels, gt_prob=0.5, zero_unsure=None)
#U = unary_from_labels(labels, n_labels, gt_prob=0.7, zero_unsure=HAS_UNK)## 如果有不确定区域,用这一行代码替换上一行
d.setUnaryEnergy(U)
# 这将创建与颜色无关的功能,然后将它们添加到CRF中
feats = create_pairwise_gaussian(sdims=(3, 3), shape=img.shape[:2])
d.addPairwiseEnergy(feats, compat=8,kernel=dcrf.DIAG_KERNEL,
normalization=dcrf.NORMALIZE_SYMMETRIC)
# 这将创建与颜色相关的功能,然后将它们添加到CRF中
feats = create_pairwise_bilateral(sdims=(80, 80), schan=(13, 13, 13),
img=img, chdim=2)
d.addPairwiseEnergy(feats, compat=10,
kernel=dcrf.DIAG_KERNEL,
normalization=dcrf.NORMALIZE_SYMMETRIC)
####################################
### 做推理和计算 ###
####################################
# 进行5次推理
Q = d.inference(10)
# 找出每个像素最可能的类
MAP = np.argmax(Q, axis=0)
# 将predicted_image转换回相应的颜色并保存图像
MAP = colorize
imwrite(CRF_image_path, MAP.reshape(img.shape))
print(&#34;CRF图像保存在&#34;,CRF_image_path,&#34;!&#34;)后记
处理结果图示例:
原始图像
标签图像
全卷积神经网络分割图像
全连接CRFs后处理图像
CRFs+填充孔洞
填充孔洞算法间我另外一篇文字:
参考文献 很棒的教程,有个小问题哈,请问在CRF时候“predicted_image_path”指的是那种类别图像如01二值图像,还是那种mask过的原图啊? predicted_image_path是指你的机器学习图像分割模型预测得到的结果。 您好,我尝试了下您的代码,我的输入的predicted_image_path指的是0or255的标签图像,结果输出是全黑的图片,很奇怪诶,请问有哪里需要改变的呢? 方便私聊我一下把一组图片发个我看看吗? 您好,在图像像素是256*256的情况下是可以运行的,但在854*480的情况下会变成全黑,请问该如何解决呢?一幅图一幅图地设置路径地址太过麻烦,请问有办法批量改图吗?非常感谢!麻烦您了 题主您好,请问Bad shape for unary energy (Need (2, 262144), got (2, 329960))这种报错是哪里不对呢 crf输入的不应该是原始rgb图像和网络softmax后的预测概率吗,这边代码里第二个参数怎么是处理后的rgb图像 确实是网络的预测概率,这里为了代码简便所以这样写的 写个循环就可以批量改了,至于全黑是不是你的参数问题