天天看点

图像处理: jpg格式 存储-读写 时 像素值 微小变化 探究

起因

遇到问题

在做项目的过程中,想比较 同一幅图像 的 二值化处理结果 和 人工标注的ground_truth图 之间的差异。

因为这两幅用来比较的图在生成的时候都是 二值图像(即像素值非 0 即 255),所以用来求差异图的代码段,我想当然地这么写:

for i in range(h):
        for j in range(w):
            if thresh_pic[i, j, 0] == 0 and label_pic[i, j, 0] == 0:
                diff_pic[i, j, :] = 0
            elif thresh_pic[i, j, 0] == 255 and label_pic[i, j, 0] == 255:
                diff_pic[i, j, :] = 255
            elif thresh_pic[i, j, 0] == 0 and label_pic[i, j, 0] == 255:
                diff_pic[i, j, 2] = 255
            elif thresh_pic[i, j, 0] == 255 and label_pic[i, j, 0] == 0:
                diff_pic[i, j, 1] = 255
            else:
                logging.error('({},{}): thresh={}, label={}'.format(i, j, thresh_pic[i, j, 0], label_pic[i, j, 0]))           

复制

在差异比对效果图出来之后却看到了很诡异的现象,图片中出现了很多黑色麻花,如下图:

图像处理: jpg格式 存储-读写 时 像素值 微小变化 探究

logging打印出来的结果也显示有很多 既非 0 也非 255 的像素点:

...
ERROR:root:(279,535): thresh=254, label=0
ERROR:root:(279,576): thresh=253, label=0
ERROR:root:(279,581): thresh=253, label=0
...           

复制

修改代码

经过观察,我发现某些像素点在 存储为图片 之前 像素值 还是 255 或 0,存为图片 以后,就会变成了245~255或0~10范围内的随机数(在不懂原理的我看来感觉那就是随机偏移,真实情况其实应该是按照某个算法进行了对应的偏移)了。初步猜测是在 存储为图片时 或 从图片读取出来时,部分像素点 发生了 像素值 的 少许偏移。

于是我修改了代码,考虑到了一个 阈值为10 的 像素值偏移区间:

for i in range(h):
        for j in range(w):
            if thresh_pic[i, j, 0] < 10 and label_pic[i, j, 0] < 10:
                diff_pic[i, j, :] = 0
            elif thresh_pic[i, j, 0] > 245 and label_pic[i, j, 0] > 245:
                diff_pic[i, j, :] = 255
            elif thresh_pic[i, j, 0] < 10 and label_pic[i, j, 0] > 245:
                diff_pic[i, j, 2] = 255
            elif thresh_pic[i, j, 0] > 245 and label_pic[i, j, 0] < 10:
                diff_pic[i, j, 1] = 255
            else:
                logging.error('({},{}): thresh={}, label={}'.format(i, j, thresh_pic[i, j, 0], label_pic[i, j, 0]))           

复制

这次黑色麻花没有了,logging也没有打印error信息了:

图像处理: jpg格式 存储-读写 时 像素值 微小变化 探究

查找资料

经过上网查找,我发现原来一些细心的前辈们也发现这个问题了,并给出了解答:

图像处理: jpg格式 存储-读写 时 像素值 微小变化 探究

Soga,原来是因为:

  • .jpg 是 有损压缩格式,保存时会 压缩失真 ( .png 是 无损压缩格式) 。

那么好奇心大发作的我又想拿我最爱的妹子图来进一步探究一下。

实验

实验思路

  1. 将原图像 复制多份 ,分别 进行 不同轮次 的 循环存储-读写,经过 多轮次 的 循环 后,在 肉眼层面 查看 新图像 是否明显较 原图像 有失真;
  2. 比较 每一轮 循环存储-读写 后,图片上 各像素点 的 像素值 发生了哪些 变化(置色方案参见下表)。
像素点的像素值变化 置色方案
不变 黑色
增加 绿色
减少 红色

实验效果

原图像:

图像处理: jpg格式 存储-读写 时 像素值 微小变化 探究

100轮 存-读 之后的图像:

图像处理: jpg格式 存储-读写 时 像素值 微小变化 探究

原图像 与 第1轮 存-读 后的 图像差异:

图像处理: jpg格式 存储-读写 时 像素值 微小变化 探究

第1轮 存-读 后的图像 与 第2轮 存-读 后的 图像差异:

图像处理: jpg格式 存储-读写 时 像素值 微小变化 探究

第2轮 存-读 后的图像 与 第3轮 存-读 后的 图像差异:

图像处理: jpg格式 存储-读写 时 像素值 微小变化 探究

第3轮 存-读 后的图像 与 第4轮 存-读 后的 图像差异:

图像处理: jpg格式 存储-读写 时 像素值 微小变化 探究

第4轮 存-读 后的图像 与 第5轮 存-读 后的 图像差异:

图像处理: jpg格式 存储-读写 时 像素值 微小变化 探究

实验代码

# coding=utf-8

first_path = './data/first.jpg'
second_path = './data/second.jpg'
third_path = './data/third.jpg'
forth_path = './data/forth.jpg'
fifth_path = './data/fifth.jpg'

one_hundred_path = './data/one_hundred.jpg'

import cv2
import numpy as np
import logging

# 生成并保存 俩图像 的 差异图
def compare(pic1, pic2, dst_path):
    h, w, c = pic1.shape
    diff_pic = np.zeros_like(pic1, dtype=np.uint8)
    for i in range(h):
        for j in range(w):
            if pic1[i, j, 0] == pic2[i, j, 0]:  # 像素值 不变,该点 置 黑色
                diff_pic[i, j, :] = 0
            elif pic1[i, j, 0] < pic2[i, j, 0]:  # 像素值 增加,该点 置 绿色
                diff_pic[i, j, 1] = 255
            elif pic1[i, j, 0] > pic2[i, j, 0]:  # 像素值 减少,该点 置 红色
                diff_pic[i, j, 2] = 255
            else:
                logging.error('({},{}): pic1={}, pic2={}'.format(i, j, pic1[i, j, 0], pic2[i, j, 0]))

    cv2.imwrite(dst_path, diff_pic)
    return diff_pic

# 指定轮数 进行 循环 存储和读取
def save_and_read_cycle(pic, path, num):
    for _ in xrange(num):
        cv2.imwrite(path, pic)
        pic = cv2.imread(path)
    return pic

# 原图像
origin_pic = cv2.imread('./data/girl.jpg')

# 需要 循环 1次 的图像
first_pic = origin_pic.copy()
first_pic = save_and_read_cycle(first_pic, first_path, 1)

# 需要 循环 2次 的图像
second_pic = origin_pic.copy()
second_pic = save_and_read_cycle(second_pic, second_path, 2)

# 需要 循环 3次 的图像
third_pic = origin_pic.copy()
third_pic = save_and_read_cycle(third_pic, third_path, 3)

# 需要 循环 4次 的图像
forth_pic = origin_pic.copy()
forth_pic = save_and_read_cycle(forth_pic, forth_path, 4)

# 需要 循环 5次 的图像
fifth_pic = origin_pic.copy()
fifth_pic = save_and_read_cycle(fifth_pic, fifth_path, 5)

# 需要 循环 100次 的图像
one_hundred_pic = origin_pic.copy()
one_hundred_pic = save_and_read_cycle(one_hundred_pic, one_hundred_path, 100)

# 求 循环 存储和读取 后 该图片 与 原图像 差异
ori_fir = compare(origin_pic, first_pic, 'data/ori_fir.jpg')
fir_sec = compare(first_pic, second_pic, 'data/fir_sec.jpg')
sec_thi = compare(second_pic, third_pic, 'data/sec_thi.jpg')
thi_for = compare(third_pic, forth_pic, 'data/thi_for.jpg')
for_fif = compare(forth_pic, fifth_pic, 'data/for_fif.jpg')
ori_hundred = compare(origin_pic, one_hundred_pic, 'data/ori_hundred.jpg')           

复制

实验结论

  1. 经过 多轮次 的 循环 后,在 肉眼层面 , 新图像 较 原图像 没有明显的失真;
  2. 每一轮 循环存储-读写 后,图片上 各像素点 的 像素值 发生的 变化 会越来越少;
  3. .jpg 是有损压缩格式。

实验不足与展望

不足之处
没有进一步探究压缩算法的原理
没有实验出像素值的偏移区间范围
没有探究循环读写的失真率变化原因
没有制作循环读写的失真率变化曲线图
缺少其他图片进行对比试验,验证实验结论的泛化性
没有在单通道灰度图像上做实验