一、怎么用数据表示图像?

    首先,要对一张图像进行压缩处理,那么应该知道怎么用数据来表示一张具体的图像。

    我们选择用Python中的第三方工具库的Pillow来读取一张样例照片,观察参数信息。

    以下是一张lenna.jpg

需要提前安装第三方库pillow

pip install pillow

 安装好之后,将图片放在源文件所在路径下,我们先来读取一下图片信息:

import numpy as np
from PIL import Image

originalImage = Image.open(r'lenna.jpg', 'r')
imageArray = np.array(originalImage)

print(imageArray.shape)
print(imageArray)
(600, 601, 3)
[[[186 102  68]
  [191 107  73]
  [190 106  72]
  ...
  [ 49  35  35]
  [ 54  40  40]
  [ 53  39  39]]

 [[192 108  74]
  [191 107  73]
  [191 107  73]
  ...
  [ 54  40  40]
  [ 50  36  36]
  [ 49  35  35]]

 [[192 108  74]
  [190 106  72]
  [190 106  72]
  ...
  [ 52  38  37]
  [ 52  38  37]
  [ 50  36  35]]

 ...

 [[237 199 186]
  [237 199 186]
  [239 201 190]
  ...
  [168 106  93]
  [166 104  91]
  [169 105  93]]

 [[242 204 191]
  [241 203 190]
  [241 201 191]
  ...
  [170 108  95]
  [178 116 103]
  [174 110  98]]

 [[237 199 186]
  [240 202 189]
  [242 202 192]
  ...
  [161  99  86]
  [171 109  96]
  [170 106  94]]]

Process finished with exit code 0

    我们从结果可以看到,这张图被表示为一个三维的ndarray数组对象:imageArray。

    数组的维度是 600 x 601 x 3 。我们称为3D张量。这个3D张量由图片的三个维度信息组成,

分别为:高度信息、宽度信息、颜色通道信息。

    程序的结果可以看出:这张图片是高度X宽度为600 x 601 的像素点阵,而颜色通道的具体取值用来对应的描述每个像素点的颜色信息,明确每个位置上的像素点颜色。由此通过整个彩色像素点阵,最终可以构建出完整的彩色图像。

    我们在样例上用的是一张JPG格式的图片,从程序结果看到,JPG格式图片的颜色通道维数是三维的,依次分别对应R、G、B 3个通道的实际取值,每个通道都是使用8位无符号整形数来进行表示,取值范围0-255。

    这三个通道的含义分别表示:红色、绿色、蓝色三个颜色通道,通过对不同取值3个通道进行数据叠加,从而可以最终产生各种所需要的颜色。另外,除了R、G、B三个通道,还有一个A通道,代表不透明度,A取值越大不透明度越高,反之亦然。如果A通道取值255表示这张图片是完全不透明的,如果取值为0表示图像完全透明。

    样例图片没有用到A通道。

二、灰度图像处理

    灰度图片的压缩过程比较简单、直观。灰度图片就是我们说的黑白图。这种情况颜色通道只用一维就足够了,通过0-255范围内的不同取值,用来表示白-灰-黑的深浅程度。

    这种情况就比较简单,原来3D张量退化为一个简单的矩阵,我们称之为image矩阵,image矩阵的形状就是用图像的高 X 宽来进行描述,而矩阵的元素值就是对应像素的灰度值。

    得到用来表示灰度图的image矩阵之后,接下来的过程就是对image矩阵进行奇异值分解(SVD),获取 U ,\SigmaV^T 三个核心矩阵要素,按照压缩的实际需要,取前k个奇异值机相对应的左、右特征向量,就能够按照下面的公式完成图像的压缩重建过程。

    image \approx \sigma _1 u_1v^T_1 + \sigma _2 u_2v^T_2 +\sigma _3 u_13v^T_3+...+\sigma _k u_kv^T_k

三、彩色图像压缩的具体思路

    实际情况没有灰度图处理那么简单。JPG格式的彩色图片不同于灰度图,它有3个颜色通道。

    所以,我们有以下具体解决思路:

    第一步:通道分离    

    对于JPG格式的彩色图片,拥有3个颜色通道,R、G、B,那么可以尝试将每个颜色通道进行分离,产生3个形状均为图像高 x 宽 的单通道剧展,即imageR,imageG,imageB。

    第二步:矩阵压缩

    对每个单通道矩阵进行奇异值分解,按照压缩的实际需要取前k个奇异值,进行3个单通道的矩阵的压缩近似,各自的处理过程同灰度图的处理过程完全一样。最后分别形成3个压缩后的矩阵:imageRC,imageGC,imageBC。

    第三步:图像重建

    将3个压缩后的单通道矩阵合并形成表示JPG格式的3D张量,通过该3D张量重构出压缩后的彩色图像。

    第一步:通道分离的实现过程

    首先进行通道分离,将imageArray数组中的每个通道分别单独取出来,得到3个高 x 宽的二维数组。这3个二维数组中每个位置上的取值就是对应像素的某个颜色通道的取值。

 

import numpy as np
from PIL import Image

originalImage = Image.open(r'lenna.jpg', 'r')
imageArray = np.array(originalImage)
R = imageArray[:, :, 0]
G = imageArray[:, :, 1]
B = imageArray[:, :, 2]
print(R)
print(G)
print(B)

[[186 191 190 ...  49  54  53]
 [192 191 191 ...  54  50  49]
 [192 190 190 ...  52  52  50]
 ...
 [237 237 239 ... 168 166 169]
 [242 241 241 ... 170 178 174]
 [237 240 242 ... 161 171 170]]
[[102 107 106 ...  35  40  39]
 [108 107 107 ...  40  36  35]
 [108 106 106 ...  38  38  36]
 ...
 [199 199 201 ... 106 104 105]
 [204 203 201 ... 108 116 110]
 [199 202 202 ...  99 109 106]]
[[ 68  73  72 ...  35  40  39]
 [ 74  73  73 ...  40  36  35]
 [ 74  72  72 ...  37  37  35]
 ...
 [186 186 190 ...  93  91  93]
 [191 190 191 ...  95 103  98]
 [186 189 192 ...  86  96  94]]

    从程序的运行结果来看,我们成功得到了3个二维ndarray数组,将R、G、B三个通道成功进行了分离。

    第二步:矩阵压缩的具体实现:

    利用奇异值分解压缩各个通道矩阵。

def imgCompress(channel,percent):
# channel表示需要进行压缩处理的单个颜色通道矩阵
# percent表示利用SVD进行矩阵重建过程保留奇异值的百分比
    U,sigma,V_T = np.linalg.svd(channel)
    #对通道矩阵进行奇异值分解
    m = U.shape[0]
    n = V_T.shape[0]
    reChannel = np.zeros((m,n))
    # 初始化mxn大小的全零矩阵reChannel
    for k in range (len(sigma)):
        reChannel = reChannel + sigma[k] * np.dot(U[:,k].reshape(m,1),V_T[k,:].reshape(1,n))
        #依照percent参数值,取前k个奇异值
        #依照近似公式重建经过压缩处理的通道矩阵reChannel
        if float(k) / len(sigma) > percent:
            reChannel[reChannel < 0] = 0
            reChannel[reChannel > 255] = 255
            #需要把处理后的数据约束在0-255范围内
            break
        return np.rint(reChannel).astype("unit8")
        #对数值进行取整,类型为8位无符号整型

    第三步:通道重建的具体实现

    最后完成通道的重建工作,将经过奇异值分解处理的3个单通道矩阵合并成表示RGB3通道的完整3D张量,在此基础上观察不同奇异值个数比例下的图像压缩效果。

    for p in [0.001, 0.005, 0.01, 0.02, 0.03, 0.04, 0.05, 
              0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]:
    #p表示取所有奇异值的前多少比例
        reR = imgCompress(R,p)
        reG = imgCompress(G,p)
        reB = imgCompress(B,p)
        reI = np.stack((reR,reG,reB),2)
        Image.fromarray(reI).save("{}".format(p)+"img.png")

 

四、源代码

import numpy as np
from PIL import Image

def imgCompress(channel,percent):
    U, sigma, V_T = np.linalg.svd(channel)
    m = U.shape[0]
    n = V_T.shape[0]
    reChannel = np.zeros((m,n))

    for k in range(len(sigma)):
        reChannel = reChannel + sigma[k]* np.dot(U[:,k].reshape(m,1),V_T[k,:].reshape(1,n))
        if float(k)/len(sigma) > percent:
            reChannel[reChannel < 0] = 0
            reChannel[reChannel > 255] = 255
            break

    return np.rint(reChannel).astype("uint8")


oriImage = Image.open(r'lenna.jpg', 'r')
imgArray = np.array(oriImage)

R = imgArray[:, :, 0]
G = imgArray[:, :, 1]
B = imgArray[:, :, 2]
# A = imgArray[:, :, 3]

for p in [0.001,0.005,0.01,0.02,0.03,0.04,0.05,
          0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]:
    reR = imgCompress(R, p)
    reG = imgCompress(G, p)
    reB = imgCompress(B, p)
    # reA = imgCompress(A, p)
    reI = np.stack((reR, reG, reB), 2)

    Image.fromarray(reI).save("{}".format(p)+"img.png")

 

五、查看结果

从最后的结果图可以看见:

取前0.05%奇异值重建的图像是一个非常模糊的,只能看到基本的轮廓。

取前0.1%奇异值亦是如此。

取前1%奇异值基本能看到眼睛、鼻子、嘴巴的轮廓,进本可以看出是一个人像。

取前2%,3%,4%,5%,10%,20%奇异值越来越清晰。除了还有一些噪点。

当取前30%奇异值的时候,已经基本和原图一致了。

 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐