SVD(奇异值分解)应用的python实现(一:图片压缩)

先简单介绍一下原理:

以下为用SVD压缩图片的python实现:

import numpy as np
from PIL import Image
from matplotlib import pyplot as plt
from os.path import getsize


def svd(img_array, ratio):
    """
    使用奇异值分解对图片进行压缩
    :param img_array: 图片数组
    :param ratio: 压缩比率
    :return: 压缩后的图片数组
    """
    U, s, V = np.linalg.svd(img_array)  # 奇异值分解
    diag_s = np.diag(s)  # 构造对角矩阵
    k = np.argmax(np.cumsum(s) / np.sum(s) > ratio)  # 根据给定的压缩比率选择保留的奇异值数量
    diag_s = diag_s[:k, :k]  # 保留前k个奇异值
    # 压缩图片
    U = U[:, :k]
    V = V[:k, :]
    return U.dot(diag_s).dot(V)


def svd_compress(img_path, ratio_list=[0.95]):
    """
    对给定路径的图片进行一系列压缩,并显示压缩后的图片及其相关信息
    :param img_path: 图片路径
    :param ratio_list: 压缩比率列表
    :return: None
    """
    img = Image.open(img_path)
    img_array = np.array(img)
    red_array, green_array, blue_array = img_array[:, :, 0], img_array[:, :, 1], img_array[:, :, 2]  # 分离RGB通道
    fig = plt.figure()
    img_num = len(ratio_list) + 1  # 图片数量
    img_size = getsize(img_path)
    index = 1
    # 压缩图片并显示相关信息
    for ratio in ratio_list:
        compressed_red_array, compressed_green_array, compressed_blue_array = (
            svd(red_array, ratio), svd(green_array, ratio), svd(blue_array, ratio))  # 压缩RGB通道
        compressed_img_array = np.stack((compressed_red_array, compressed_green_array, compressed_blue_array), axis=2)  # 合并RGB通道
        compressed_img = Image.fromarray(compressed_img_array.astype(np.uint8))  # 转换为图片
        compressed_img_path = f'{img_path[:-4]}_svd_{ratio:.2f}.jpg'
        compressed_img.save(compressed_img_path)  # 保存压缩后的图片
        compression_ratio = getsize(compressed_img_path) / img_size  # 计算压缩率
        # 显示压缩后的图片及其相关信息
        ax = fig.add_subplot(1, img_num, index)
        ax.imshow(compressed_img)
        ax.set_title(f'Ratio:{ratio:.2f}\nCR:{compression_ratio*100:.2f}%')
        index += 1

    # 显示原始图片
    ax = fig.add_subplot(1, img_num, img_num)
    ax.imshow(img)
    ax.set_title('Original Image')
    plt.show()


if __name__ == "__main__":
    svd_compress('lenna.bmp', np.linspace(0.2, 0.99, 9))

输入:

输出:

注意原始图片为.bmp格式,压缩后的图片为.jpg格式,该图片单纯由.bmp转为.jpg的压缩率为5.07%。

从输出的图片中可以直观地看到选取k值的比率对压缩率及成像质量的影响:

当k取奇异值总和的前50%时,可以得到一张勉强能够辨认的图片,压缩率为3.41%;

当k取奇异值总和的前60%时,可以得到一张清晰但有大量噪点的图片,压缩率为4.17%;

当k取奇异值总和的前90%时,可以得到一张与原图几乎一模一样、仅在物体边界处有少量噪点的图片,压缩率为4.97%;

当k取奇异值总和的前99%时,可以得到一张与原图一模一样、肉眼无法看出差别的图片,压缩率为5.06%。

优化k取值的一个策略:

目前为止对RGB三个颜色采用的都是相同的取值策略,但RGB的k值“能量”分布其实略有不同:

可以取三个颜色各自斜率为设定值的临界点的k值,如设定值为1表示取三个颜色各自增长速度小于y=x的临界点。

修改k取值策略后的代码如下:

import numpy as np
from PIL import Image
from matplotlib import pyplot as plt
from os.path import getsize


def difference(array):
    """
    计算数组的差分
    :param array: 一维数组
    :return: 差分数组
    """
    array_sum = sum(array)
    return [array[i] / array_sum for i in range(len(array))]


def svd(img_array, ratio):
    """
    使用奇异值分解对图片进行压缩
    :param img_array: 图片数组
    :param ratio: 压缩比率
    :return: 压缩后的图片数组
    """
    U, s, V = np.linalg.svd(img_array)  # 奇异值分解
    diag_s = np.diag(s)  # 构造对角矩阵
    ds = difference(s)  # 差分奇异值
    k = np.argmax(ds <= ratio)  # 找到第一个小于等于压缩比率的奇异值下标
    diag_s = diag_s[:k, :k]  # 保留前k个奇异值
    # 压缩图片
    U = U[:, :k]
    V = V[:k, :]
    return U.dot(diag_s).dot(V)


def svd_compress(img_path, ratio_list=[0.95]):
    """
    对给定路径的图片进行一系列压缩,并显示压缩后的图片及其相关信息
    :param img_path: 图片路径
    :param ratio_list: 压缩比率列表
    :return: None
    """
    img = Image.open(img_path)
    img_array = np.array(img)
    red_array, green_array, blue_array = img_array[:, :, 0], img_array[:, :, 1], img_array[:, :, 2]  # 分离RGB通道
    fig = plt.figure()
    img_num = len(ratio_list) + 1  # 图片数量
    img_size = getsize(img_path)
    index = 1
    # 压缩图片并显示相关信息
    for ratio in ratio_list:
        compressed_red_array, compressed_green_array, compressed_blue_array = (
            svd(red_array, ratio), svd(green_array, ratio), svd(blue_array, ratio))  # 压缩RGB通道
        compressed_img_array = np.stack((compressed_red_array, compressed_green_array, compressed_blue_array), axis=2)  # 合并RGB通道
        compressed_img = Image.fromarray(compressed_img_array.astype(np.uint8))  # 转换为图片
        compressed_img_path = f'{img_path[:-4]}_svd_{ratio:.3f}.jpg'
        compressed_img.save(compressed_img_path)  # 保存压缩后的图片
        compression_ratio = getsize(compressed_img_path) / img_size  # 计算压缩率
        # 显示压缩后的图片及其相关信息
        ax = fig.add_subplot(1, img_num, index)
        ax.imshow(compressed_img)
        ax.set_title(f'Ratio:{ratio:.3f}\nCR:{compression_ratio * 100:.2f}%')
        index += 1

    # 显示原始图片
    ax = fig.add_subplot(1, img_num, img_num)
    ax.imshow(img)
    ax.set_title('Original Image')
    plt.show()


if __name__ == "__main__":
    svd_compress('lenna.bmp', np.logspace(1, 3, 9, base=0.1))

优化前后的压缩率及成像质量对比(上方为优化后,下方为优化前):

选取压缩率差别小于10%的图片放大对比:

在k值较小的情况下,优化后的图片更加清晰,但噪点也更多;

优化后 CR=3.68%
优化前 CR=3.41%

在k值较大的情况下,两者差别不明显。

优化后 CR=4.95%
优化前 CR=4.97%

  • 12
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值