尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

从Lena图像到矩阵运算:数字图像处理的数学本质与Python实践

从Lena图像到矩阵运算:数字图像处理的数学本质与Python实践 1. 项目概述从“Lena”到矩阵运算的视觉探索如果你对数字图像处理或计算机视觉有过接触哪怕只是浅尝辄止很大概率见过一张名为“Lena”或“Lenna”的女士肖像。这张源于1972年《花花公子》杂志插页的测试图因其丰富的纹理、平滑的肤色过渡和细节层次意外地成为了图像处理领域半个多世纪的“标准模特”。我们今天聊的“Lena and Matrix”正是将这张传奇图像与矩阵Matrix这一数学工具深度结合的一次实践。这不仅仅是把一张图片读入程序那么简单其核心在于我们如何用矩阵的视角去解构、分析和重塑这幅图像从而揭示数字图像最本质的数学结构并完成一些基础的、却至关重要的图像处理操作。对于初学者这是一个绝佳的起点你将直观地理解“图像即矩阵”这一核心概念。对于有经验的开发者这则是一次回归基础的旅程能让你重新审视那些习以为常的库函数如OpenCV的imread背后究竟发生了什么。我们将不使用任何高阶的图像处理库去“黑盒”操作而是从零开始用最基础的NumPy代表矩阵运算和Matplotlib用于可视化来亲手实现。你会发现灰度化、裁剪、旋转、滤波这些操作归根结底都是一系列矩阵运算。通过这个项目你不仅能掌握处理“Lena”这张特定图像的方法更能获得一套适用于任何数字图像的、基于矩阵的通用分析和处理框架。2. 核心思路将图像视为一个多维数值矩阵在开始敲代码之前我们必须建立起正确的思维模型。这是所有后续操作的基石。2.1 图像的数字本质一个三维张量一张彩色数字图像如JPEG、PNG格式在计算机内存中并非我们眼中看到的“一幅画”而是一个规整的数字阵列。对于标准的RGB彩色图像这个阵列是一个三维的或者说是三个二维矩阵的叠加。具体来说高度Height 图像有多少行像素。对应矩阵的行数。宽度Width 图像有多少列像素。对应矩阵的列数。通道Channels 通常为3分别代表红色R、绿色G、蓝色B三个颜色通道。因此一张512x512的彩色“Lena”图在内存中就是一个形状为(512, 512, 3)的NumPy数组。数组中的每一个元素都是一个0到255之间的整数8位无符号整型uint8代表该像素点在对应通道上的亮度强度。0表示完全没有该颜色255表示该颜色的最高亮度。注意 有些库如matplotlib.pyplot.imread可能会将像素值归一化到0到1之间的浮点数这在进行某些数学运算时更方便但本质未变。2.2 灰度图降维的二维矩阵彩色图像包含三个通道的信息有时为了简化问题或专注亮度信息我们会将其转换为灰度图。灰度化不是简单地删除两个通道而是根据人眼对不同颜色的敏感度对RGB三个通道进行加权平均。一个常见的公式是Gray 0.299 * R 0.574 * G 0.114 * B经过这个操作三维的(H, W, 3)矩阵就变成了二维的(H, W)矩阵。此时图像完全等同于一个数学上的二维矩阵每个元素代表该点的灰度值0为黑255为白。后续很多图像处理算法如边缘检测都是在灰度图上进行的。2.3 矩阵运算与图像变换的对应关系这是本项目最有趣的部分。几乎所有的基本图像处理操作都可以用矩阵运算来描述裁剪Cropping 就是矩阵的切片操作。例如image[100:400, 200:500]就是截取原图第100到399行第200到499列的区域。缩放Resizing 涉及矩阵的重新采样。虽然可以用简单的最近邻插值直接取或复制像素但高质量缩放通常需要更复杂的插值算法如双线性、双三次这些算法的核心也是基于局部像素矩阵的加权计算。旋转Rotation 一个经典的几何变换。每个像素点的新坐标(x‘, y’)可以通过一个2x2的旋转矩阵乘以原坐标(x, y)得到。实际操作中我们通常计算目标图像每个像素点在原图中的对应位置反向映射以避免出现空洞。卷积滤波Convolution Filtering 如图像平滑模糊或锐化。这需要定义一个小的卷积核例如3x3的矩阵然后将这个核在原图像矩阵上“滑动”每一步都计算核与对应图像子区域的元素乘积之和用结果替换中心像素。高斯模糊、Sobel边缘检测等都是卷积运算的应用。理解这些对应关系你就掌握了用代码“雕刻”图像的刻刀。3. 环境准备与“Lena”图像的获取我们选择Python作为实现语言因为它拥有强大的科学计算库生态系统。这个项目对计算资源要求极低任何能运行Python的电脑都可以完成。3.1 工具选型为什么是NumPy和MatplotlibNumPy 它是Python科学计算的基石。其核心ndarrayN维数组对象为我们提供了高效存储和操作大型数值数组的能力。图像对应的矩阵在NumPy中就是一个ndarray。NumPy优化过的向量化运算比用纯Python循环遍历像素要快成百上千倍。Matplotlib 一个强大的绘图库。它的pyplot模块提供了类似MATLAB的绘图接口我们可以用它来显示图像、绘制直方图直观地看到每一步矩阵运算对图像产生的视觉影响。SciPy可选但推荐 它构建在NumPy之上提供了更多高阶的科学计算工具。例如scipy.ndimage模块包含了许多现成的图像处理函数如旋转、滤波我们可以用它来验证自己实现的矩阵运算是否正确。安装非常简单使用pip即可pip install numpy matplotlib scipy3.2 获取并加载“Lena”图像由于版权和历史原因原始的“Lena”图并不总被直接包含在库中。不过scipy.misc曾经包含它但现在更推荐从可靠的学术资源获取。这里我们用一个更通用的方法直接从网络下载一个公开的测试版本或者使用matplotlib自带的示例图库它包含一个类似的图像“face”。为了完全贴合“Lena”主题我们将从网络下载。这里假设我们有一个名为lena_std.tif的经典512x512版本该版本在学术界被广泛使用和分发。import numpy as np import matplotlib.pyplot as plt from urllib.request import urlopen from io import BytesIO from PIL import Image # 使用PIL来读取图像数据 # 从一个可靠的学术镜像获取Lena图像 url “https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png“ # 注意实际使用时请确保链接可用或准备本地文件作为后备 # 从URL下载并转换为NumPy数组 try: resp urlopen(url) image_data resp.read() image_pil Image.open(BytesIO(image_data)).convert(‘RGB’) # 确保转换为RGB lena_array np.array(image_pil) print(f“图像加载成功形状{lena_array.shape} 数据类型{lena_array.dtype}”) except Exception as e: print(f“从网络加载失败{e}”) print(“将使用matplotlib自带的‘face’图像作为演示。”) # 后备方案 import matplotlib.image as mpimg # 这里需要你有一个本地的‘lena.png’文件或者使用其他来源 # 为了示例我们创建一个随机矩阵模拟 print(“由于网络获取不确定以下演示将基于一个模拟的512x512x3随机矩阵进行原理讲解。”) lena_array np.random.randint(0, 256, (512, 512, 3), dtypenp.uint8) # 显示原始图像 plt.figure(figsize(5, 5)) plt.imshow(lena_array) plt.axis(‘off’) # 关闭坐标轴 plt.title(‘Original Lena Image (RGB)’) plt.show()运行这段代码你就能看到这位“数字图像处理第一夫人”的真容并确认其矩阵形状为(512, 512, 3)。4. 核心操作一矩阵视角下的图像基本变换现在我们开始用矩阵运算来操作这张图像。让我们从最基础的开始。4.1 灰度化从三维张量到二维矩阵如前所述我们将使用加权公式将彩色图转为灰度图。注意np.dot函数在这里可以进行高效的矩阵向量乘法。def rgb_to_grayscale(rgb_image): “”“将RGB图像转换为灰度图像。 参数 rgb_image: 形状为(H, W, 3)的NumPy数组 dtype通常为uint8。 返回 形状为(H, W)的NumPy数组 dtype为uint8。 ”“” # 使用标准的亮度转换权重 # 注意如果图像是0-1的浮点数权重不变 weights np.array([0.299, 0.574, 0.114]) # 方法利用NumPy的广播和点乘进行向量化计算 # 等效于对每个像素点计算 dot(weights, [R,G,B]) grayscale np.dot(rgb_image[..., :3], weights) # 只取前三个通道以防有alpha通道 # 确保结果数据类型为uint8 grayscale grayscale.astype(np.uint8) return grayscale # 应用转换 lena_gray rgb_to_grayscale(lena_array) print(f“灰度图形状{lena_gray.shape}”) # 应输出 (512, 512) # 并排显示 fig, axes plt.subplots(1, 2, figsize(10, 5)) axes[0].imshow(lena_array) axes[0].set_title(‘Original Color’) axes[0].axis(‘off’) axes[1].imshow(lena_gray, cmap‘gray’) # 灰度图必须指定色彩映射为‘gray’ axes[1].set_title(‘Grayscale’) axes[1].axis(‘off’) plt.show()实操心得 这里np.dot的用法很巧妙。rgb_image[..., :3]的形状是(512,512,3)weights的形状是(3,)。在点乘操作中NumPy会将最后一个维度大小为3与weights的维度对齐并对前两个维度(512,512)进行广播最终高效地计算出每个像素的灰度值。这比写双层循环快得多。4.2 裁剪与切片矩阵的局部提取假设我们只对Lena的面部区域感兴趣。通过观察图像我们估计面部大致位于行100到400列200到500的区域。# 矩阵切片操作就是裁剪 face_region lena_array[100:400, 200:500, :] # 注意索引是左闭右开 [start:end] # 对于灰度图也一样 lena_gray[100:400, 200:500] print(f“裁剪后面部区域形状{face_region.shape}”) # 应输出 (300, 300, 3) plt.figure(figsize(4, 4)) plt.imshow(face_region) plt.axis(‘off’) plt.title(‘Cropped Face Region’) plt.show()这简单得令人惊讶但正是这种简洁性体现了将图像视为矩阵的威力。所有的像素数据都在这个子矩阵里。4.3 缩放下采样通过矩阵索引实现最近邻插值实现一个完整的双线性插值缩放稍复杂我们先实现最简单的最近邻插值来理解缩放的矩阵本质构建一个新的、尺寸不同的矩阵其每个像素值来源于原矩阵中最近点的值。def resize_nearest(image, new_height, new_width): “”“使用最近邻插值缩放图像。 参数 image: 输入图像可以是灰度(H,W)或彩色(H,W,C)。 new_height, new_width: 目标尺寸。 返回 缩放后的图像数组。 ”“” height, width image.shape[0], image.shape[1] # 计算目标图像每个像素坐标对应的原图坐标 # 使用线性映射并四舍五入到最近的整数索引 y_indices np.round(np.linspace(0, height-1, new_height)).astype(int) x_indices np.round(np.linspace(0, width-1, new_width)).astype(int) # 使用高级索引提取像素 if image.ndim 3: # 彩色图像 resized image[y_indices[:, None], x_indices[None, :], :] else: # 灰度图像 resized image[y_indices[:, None], x_indices[None, :]] return resized # 将图像缩放到256x256 lena_small resize_nearest(lena_array, 256, 256) print(f“缩放后图像形状{lena_small.shape}”) fig, axes plt.subplots(1, 2, figsize(10, 5)) axes[0].imshow(lena_array) axes[0].set_title(‘Original (512x512)’) axes[0].axis(‘off’) axes[1].imshow(lena_small) axes[1].set_title(‘Resized - Nearest (256x256)’) axes[1].axis(‘off’) plt.show()注意事项 最近邻插值会导致图像出现明显的锯齿特别是缩小很多时。在实际应用中OpenCV的cv2.resize或PIL的Image.resize提供了双线性、双三次等更平滑的选项它们的内部实现也是基于更复杂的像素矩阵权重计算。5. 核心操作二通过卷积实现图像滤波卷积是图像处理中最重要的矩阵运算之一。我们将手动实现一个3x3的均值模糊滤波器Box Blur和一个简单的边缘检测滤波器Sobel近似。5.1 理解卷积操作卷积核是一个小矩阵如3x3。对于图像中的每个像素边缘像素除外我们将核的中心对准它计算核内每个权重与图像对应像素值的乘积然后将所有乘积求和结果作为输出图像该位置的新像素值。这个过程就是核在图像矩阵上“滑动”。5.2 实现均值模糊滤波均值模糊的核所有元素值相等和为1。一个3x3的均值核是1/9 * [[1,1,1], [1,1,1], [1,1,1]]。它的效果是取周围9个像素的平均值从而平滑图像减少噪声。def convolve2d(image, kernel): “”“对二维图像灰度进行卷积操作。 这是一个简单的、非优化的实现用于理解原理。 参数 image: 二维NumPy数组灰度图。 kernel: 二维NumPy数组奇数尺寸如3x3, 5x5。 返回 卷积后的图像尺寸会略小。 ”“” kernel_height, kernel_width kernel.shape pad_h, pad_w kernel_height // 2, kernel_width // 2 # 为了简化我们使用‘valid’卷积输出尺寸会变小。更常见的做法是先填充图像。 # 这里我们采用简单填充用0填充边缘 image_padded np.pad(image, ((pad_h, pad_h), (pad_w, pad_w)), mode‘constant’, constant_values0) height, width image.shape output np.zeros_like(image, dtypenp.float64) # 使用浮点类型避免溢出 # 滑动窗口进行卷积 for i in range(height): for j in range(width): # 提取当前窗口区域 region image_padded[i:ikernel_height, j:jkernel_width] # 计算元素乘积和 output[i, j] np.sum(region * kernel) return output # 定义3x3均值核 mean_kernel np.ones((3, 3)) / 9.0 # 在灰度图上应用卷积 lena_gray_float lena_gray.astype(np.float64) # 转换为浮点以便卷积 lena_blurred convolve2d(lena_gray_float, mean_kernel) # 将结果转换回uint8并裁剪到有效范围 lena_blurred_uint8 np.clip(lena_blurred, 0, 255).astype(np.uint8) # 显示结果 fig, axes plt.subplots(1, 2, figsize(10, 5)) axes[0].imshow(lena_gray, cmap‘gray’) axes[0].set_title(‘Original Grayscale’) axes[0].axis(‘off’) axes[1].imshow(lena_blurred_uint8, cmap‘gray’) axes[1].set_title(‘3x3 Mean Blurred’) axes[1].axis(‘off’) plt.show()你会看到模糊后的图像细节变少更加平滑。这就是卷积核“平均化”局部像素的效果。5.3 实现简单的边缘检测边缘是图像中亮度变化剧烈的区域。我们可以使用Sobel算子来近似计算图像的梯度。Sobel有两个核分别检测水平和垂直方向的边缘。# 定义Sobel算子核用于检测垂直边缘 sobel_x_kernel np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtypenp.float64) # 定义Sobel算子核用于检测水平边缘 sobel_y_kernel np.array([[-1, -2, -1], [ 0, 0, 0], [ 1, 2, 1]], dtypenp.float64) # 分别进行水平和垂直方向的卷积 grad_x convolve2d(lena_gray_float, sobel_x_kernel) grad_y convolve2d(lena_gray_float, sobel_y_kernel) # 计算梯度幅值边缘强度 gradient_magnitude np.sqrt(grad_x**2 grad_y**2) # 归一化到0-255以便显示 gradient_magnitude_normalized (gradient_magnitude / gradient_magnitude.max() * 255).astype(np.uint8) plt.figure(figsize(6, 6)) plt.imshow(gradient_magnitude_normalized, cmap‘gray’) plt.axis(‘off’) plt.title(‘Edge Detection (Sobel Gradient Magnitude)’) plt.show()输出图像中亮线就对应原图中的边缘比如帽檐、面部轮廓、肩膀等。sobel_x_kernel对垂直边缘敏感如鼻梁侧面sobel_y_kernel对水平边缘敏感如帽檐。我们将两个方向的梯度结合就得到了整体的边缘图。实操心得 我们上面写的convolve2d函数使用了双重循环对于大图像来说非常慢。在实际项目中绝对不要这样用。NumPy和SciPy提供了优化过的卷积函数scipy.signal.convolve2dscipy.ndimage.convolve或者对于更大的图像和更复杂的网络使用深度学习框架如PyTorch, TensorFlow中基于GPU加速的卷积。 这里手动实现的目的纯粹是为了揭示其矩阵运算的本质。当你调用cv2.GaussianBlur时底层就是在做类似但高度优化过的卷积计算。6. 进阶探索矩阵分解与图像压缩初窥矩阵运算不仅能改变图像还能帮助我们分析图像。一个有趣的视角是使用奇异值分解Singular Value Decomposition, SVD。SVD可以将任意矩阵分解为三个矩阵的乘积A U * S * V^T。对于图像矩阵灰度图SVD有明确的物理意义U和V^T的列向量可以看作是图像的“特征图”而奇异值矩阵S对角线上的值代表了这些特征图的重要性。大的奇异值对应图像中主要的、全局性的结构如背景、大体轮廓小的奇异值对应细节和噪声。6.1 使用SVD进行低秩近似压缩我们可以只保留前k个最大的奇异值及其对应的特征向量用它们来重构图像。这样得到的图像是原图的一个低秩近似实现了有损压缩。# 对灰度图像矩阵进行SVD分解 U, S, Vt np.linalg.svd(lena_gray_float, full_matricesFalse) # S是一维数组包含奇异值 print(f“奇异值个数{len(S)}”) print(f“前10个奇异值{S[:10]}”) # 定义一个函数使用前k个奇异值重构图像 def reconstruct_svd(U, S, Vt, k): “”“使用前k个奇异值重构图像”” # 构建近似的对角矩阵 S_k np.diag(S[:k]) # 重构矩阵 A_k U[:, :k] S_k Vt[:k, :] # 确保值在合理范围并转换类型 A_k np.clip(A_k, 0, 255).astype(np.uint8) return A_k # 尝试不同的k值 ks [5, 20, 50, 100, 200] fig, axes plt.subplots(2, 3, figsize(15, 10)) axes axes.ravel() axes[0].imshow(lena_gray, cmap‘gray’) axes[0].set_title(f‘Original\nRank ~{len(S)}’) axes[0].axis(‘off’) for idx, k in enumerate(ks, start1): lena_compressed reconstruct_svd(U, S, Vt, k) axes[idx].imshow(lena_compressed, cmap‘gray’) axes[idx].set_title(f‘k{k}\nRank {k} Approx.’) axes[idx].axis(‘off’) # 计算压缩率非常粗略的估计 original_size lena_gray.size # 像素总数 # 存储U_k, S_k, Vt_k 所需元素数: k*(mn1) compressed_size k * (U.shape[0] Vt.shape[1] 1) ratio compressed_size / original_size print(f“k{k}: 理论存储比 ≈ {ratio:.2%}”) plt.tight_layout() plt.show()观察图像即使只使用前50个奇异值k50我们已经能清晰地辨认出Lena的面容虽然丢失了很多纹理细节如头发丝、帽子的羽毛。这就是JPEG等压缩算法背后的数学原理之一虽然JPEG用DCT而非SVD的直观体现丢弃对视觉贡献较小的信息以实现高压缩比。注意事项 对彩色图像进行SVD压缩需要对每个通道分别处理然后再合并。此外SVD分解本身计算量较大对于实时压缩并不实用但它提供了理解图像信息构成的一个优美数学视角。7. 常见问题与调试技巧实录在实际操作中你可能会遇到以下典型问题。这里记录了我的排查思路和解决方法。7.1 图像显示颜色异常或全黑/全白问题描述 使用plt.imshow()显示图像时图片颜色怪异或者灰度图显示为全黑/全白。原因分析数据类型不匹配imshow对于浮点数组默认期望值范围在0-1之间。如果你的图像矩阵是0-255的uint8但被误当作0-1的浮点数显示就会全白因为255被解释为亮度上限。反之0-1的浮点数被当作0-255的整数则会几乎全黑。色彩映射cmap错误 显示灰度图时必须指定cmap‘gray’。如果显示彩色图却指定了灰度色彩映射颜色会失真。通道顺序 OpenCV (cv2.imread) 默认以BGR顺序加载图像而Matplotlib的imshow期望RGB顺序。直接显示OpenCV读取的图像会红蓝对调。解决方案# 诊断打印数组的信息 print(“Shape:”, image.shape) print(“Data type:”, image.dtype) print(“Value range: [”, image.min(), “,”, image.max(), “]”) # 解决方案1确保数据类型和范围正确 if image.dtype np.uint8: # 对于uint8, imshow会自动处理0-255范围 plt.imshow(image) elif image.dtype in [np.float32, np.float64]: # 对于浮点数确保值在0-1之间 if image.max() 1.0: image_display image / 255.0 else: image_display image plt.imshow(image_display) # 解决方案2正确设置cmap if len(image.shape) 2: # 灰度图 plt.imshow(image, cmap‘gray’) else: # 彩色图 plt.imshow(image) # 不指定cmap或使用‘viridis’等 # 解决方案3转换OpenCV BGR到RGB # image_rgb cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)7.2 卷积操作后图像边缘有黑色边框问题描述 如我们上面手动实现的卷积输出图像四周有一圈黑边。原因分析 这是“valid”卷积或零填充zero-padding的固有结果。当卷积核滑动到图像边缘时边缘像素没有足够的邻居来完成卷积运算。我们用0来填充这些不存在的区域导致边缘区域的卷积结果倾向于0黑色。解决方案 根据需求选择不同的填充策略保持尺寸不变 在卷积前进行填充。除了零填充还可以使用‘edge’复制边缘像素、‘reflect’镜像反射或‘symmetric’对称等模式。scipy.ndimage.convolve函数可以通过mode参数指定。接受尺寸变小 如果后续处理不要求尺寸严格一致可以直接使用“valid”卷积的结果或者先裁剪掉边缘部分。使用‘same’卷积 许多库函数如scipy.signal.convolve2d的mode‘same’会自动进行填充并返回与原图相同尺寸的输出。7.3 矩阵运算如SVD内存占用过大或速度慢问题描述 处理大图像如4K图片时进行SVD分解或大型矩阵乘法导致内存不足或计算极其缓慢。原因分析 全尺寸SVD的时间复杂度约为O(min(mn^2, m^2n))对于百万像素级的图像计算量和内存需求巨大。解决方案降采样 先对图像进行缩放在小尺寸上进行分析或实验。使用随机SVD 对于大规模矩阵可以使用随机算法如sklearn.utils.extmath.randomized_svd来近似计算前k个奇异值和向量速度更快内存占用更少。分块处理 对于超大型图像可以考虑将其分割成小块分别处理后再合并需注意块边界效应。利用稀疏性 如果图像矩阵在某些变换域如小波变换后是稀疏的可以考虑使用稀疏矩阵运算库。终极方案 对于生产环境使用高度优化的专业库如OpenCV、PIL或利用GPU加速CuPy、PyTorch是唯一可行的路径。手动实现仅用于学习和原理验证。7.4 自己实现的滤波效果与库函数结果有细微差别问题描述 自己写的均值模糊函数结果与cv2.blur或scipy.ndimage.uniform_filter的结果在边缘处有1-2个像素值的差异。原因分析边界处理 库函数可能有更复杂或默认的边界处理方式如scipy的mode参数。数据类型与舍入 卷积过程中涉及浮点运算最后转换为整数时的舍入策略四舍五入、向下取整等可能不同。核的归一化 确保你的卷积核所有元素之和为1对于均值滤波否则会改变图像的整体亮度。排查步骤# 1. 在一个非常小的测试矩阵上对比 test_matrix np.array([[1,2,3],[4,5,6],[7,8,9]], dtypenp.uint8) kernel np.ones((3,3))/9.0 my_result convolve2d(test_matrix.astype(float), kernel) # 使用scipy验证 from scipy import ndimage scipy_result ndimage.convolve(test_matrix.astype(float), kernel, mode‘constant’, cval0.0) print(“My result:\n”, my_result) print(“Scipy result:\n”, scipy_result) print(“Difference:\n”, my_result - scipy_result) # 2. 检查核的和 print(“Kernel sum:”, kernel.sum()) # 应为1.0通过在小矩阵上对比可以快速定位是算法逻辑问题还是边界/舍入问题。经过这一系列的探索从加载、展示到灰度化、裁剪、滤波再到利用SVD进行压缩分析“Lena and Matrix”这个项目已经不再是一个简单的标题。它成了一条清晰的路径引导我们穿透图像的视觉表象直达其矩阵核心。你会发现许多复杂的图像处理算法剥开其精巧的外壳内里往往是一个或几个简洁而优美的矩阵运算。下次当你调用某个图像处理API时不妨想一想它背后的矩阵正在经历怎样的变换。这种理解能让你从API的使用者变为真正意义上的创造者。
返回列表