ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

OpenCV像素操作全指南:从矩阵本质到高效实战

OpenCV像素操作全指南:从矩阵本质到高效实战 1. 图像到底在内存里是什么搞懂这个再谈像素操作很多初学OpenCV的朋友上手第一件事就是cv2.imread()把图片读进来然后立刻用cv2.imshow()显示出来。看到窗口里弹出图片就以为我会OpenCV了。但等到真要动手改某个像素、抠某个区域、做点图像处理的时候瞬间就卡壳了图像在OpenCV里到底是个什么结构为什么访问像素要用img[y, x]而不是img[x, y]为什么读进来的图片用matplotlib显示颜色怪怪的这些问题不搞清楚后面所有的像素操作都是在背API换个场景就不会了。所以我先花点篇幅把图像的底层表示讲透。1.1 图像就是一张数字矩阵没有别的在OpenCV里读入的图像本质上就是一个NumPy数组。这是整个OpenCV Python版的核心认知。不管是320x240的小图还是4K大图不管彩色还是灰度最终都落在ndarray这个结构上。你可以用img.shape看一眼它的维度import cv2 img cv2.imread(demo.jpg) print(img.shape) # 比如 (480, 640, 3)这个元组的含义非常固定第一个值是高度行数第二个值是宽度列数第三个值是通道数。灰度图shape是 (H, W)没有第三个维度每个像素只有一个亮度值。彩色图shape是 (H, W, 3)第三个维度是通道——但这个通道顺序恰恰是初学者第一个会踩的坑下面马上讲。用矩阵的视角看图像你就明白了所谓像素操作本质就是对NumPy数组里数值的读取和修改。图像滤波是卷积核在矩阵上滑动图像缩放是重采样矩阵边缘检测是求矩阵梯度。所有的图像处理算法底层都是矩阵运算。这个认知一旦建立你再看OpenCV的文档很多函数的作用一眼就能猜个大概。1.2 行是y列是x顺序别搞反访问像素的时候用的是img[y, x]这个索引方式。注意方括号里第一个下标是行号y坐标第二个下标是列号x坐标。这一点几乎每个初学者都会至少搞错一次。因为从小到大学的笛卡尔坐标系都是横坐标x在前、纵坐标y在后到了图像处理里恰好反过来。而且OpenCV的错误提示不会告诉你坐标反了它只会告诉你索引越界或者干脆给你返回一个完全不相关的像素值排查起来特别消耗耐心。如果你在处理的关键步骤里发现结果总是差一块、偏一个方向先检查是不是坐标写反了。尤其是做ROI裁剪img[y1:y2, x1:x2]的时候顺序搞反裁剪出来的区域会完全不对。1.3 BGR还是RGB这是OpenCV最容易埋的雷OpenCV读入彩色图像后通道顺序是BGR不是很多图像处理库常用的RGB。具体来说img[y, x]返回的是[B, G, R]三个值索引0是蓝色分量索引1是绿色分量索引2是红色分量。这个设计源自历史原因——早期一些图像格式和显示设备的内部表示就是BGROpenCV继承了这个习惯。但问题在于Python生态里另一个非常常用的库matplotlib它认的是RGB。于是你直接用matplotlib显示OpenCV读进来的图会看到蓝色通道和红色通道对调图像整体发蓝发红颜色非常诡异。解决办法很简单显示前用cv2.cvtColor(img, cv2.COLOR_BGR2RGB)转换一下。import cv2 import matplotlib.pyplot as plt img cv2.imread(demo.jpg) # 直接plt.imshow(img)会显示错误的颜色 # 必须先转成RGB再显示 img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) plt.imshow(img_rgb) plt.show()只有把这个转换养成肌肉记忆你在配合matplotlib做分析的时候才不会频繁踩坑。记住一个原则OpenCV内部读写、处理都是用BGR只有在显示或交给其他库时才需要转换。1.4 数据类型uint8的边界和陷阱OpenCV默认读入的彩色图像数据类型是uint8也就是8位无符号整数取值范围0到255。0代表纯黑255代表纯白。这个设计是跟图像文件的存储格式对齐的——大部分常见格式JPEG、PNG都用8位来存一个通道的亮度信息。但这也带来一个经典的溢出问题如果你直接做img 50当某个像素值是230的时候230 50 280在uint8里会溢出变成 280 - 256 24结果不是变亮而是突然变暗。这不是OpenCV的bug而是整数类型本身的特性。后面我会专门讲怎么用cv2.add()或者np.clip()来规避这个溢出。现在你只需要记住uint8的255是上限处理时心里要绷着这根弦。除了uint8OpenCV也支持其他数据类型比如float32、uint16。一些科学计算场景比如深度学习的预处理、HDR图像的读取会把图像转成浮点型来避免精度损失和溢出问题。但日常的像素操作绝大多数都在uint8的范畴内。2. 逐个像素读写四种方式与性能分水岭理解了图像的矩阵本质接下来就是最核心的操作怎么读一个像素、改一个像素以及在高性能要求下怎么避免踩进Python循环遍历每个像素这个巨大的性能陷阱。先说结论在Python里用for循环遍历整张图的每个像素是非常慢的。处理一张1920x1080的彩色图两层for循环可能要好几秒甚至更久。同样的操作如果用NumPy的向量化写法只需要几十毫秒。差距是百倍量级。所以真实项目里几乎所有像素级操作都应该用向量化方式完成for循环只在少数调试场景下用。下面把四种方式都过一遍对比它们的用法、性能和适用场景。2.1 方式一直接索引img[y, x]这是最直观的写法。对于彩色图img[y, x]返回该位置的一个长度为3的数组代表BGR三个通道的值import cv2 img cv2.imread(demo.jpg) px img[100, 200] # 获取(100, 200)处的BGR值 print(px) # [某个B值, 某个G值, 某个R值] # 修改这个像素为纯红色注意OpenCV里红色是(0, 0, 255) img[100, 200] (0, 0, 255)这个写法清晰明了但它有两个细节要注意如果只取单个通道要用img[y, x, channel]其中channel的取值0、1、2分别对应B、G、R。直接索引会做边界检查索引越界会直接抛异常不会静默出错。这在调试阶段其实是好事。2.2 方式二.item()和.itemset()当你只需要读取单个通道的单个像素值时用img.item(y, x, channel)比数组索引更快而且它返回的是Python原生的标量int而不是一个长度为1的数组或NumPy标量。# 读取(100, 200)处蓝色通道的值 b_value img.item(100, 200, 0) print(b_value) # 直接是一个整数 # 修改(100, 200)处蓝色通道的值 img.itemset((100, 200, 0), 128).item()的性能优势主要体现在你需要大量读取单点数值的场景比如遍历一个稀疏点集的时候。它省去了中间数组的创建开销。但说实话对于大多数应用直接用索引就够了.item()更适合那种对单点访问频率极高的算法内部。2.3 方式三向量化操作实际项目的主力前面两种方式都是针对单个像素但如果要对整张图的所有像素做同样的操作比如整体提亮、反色、阈值过滤千万不要用for循环要利用NumPy的广播和向量化能力。举个例子把整张图所有像素的蓝色通道值设为200import numpy as np img[:, :, 0] 200这也是为什么OpenCV的Python风格代码里常看到[:, :, 0]这种切片写法——它把整张图当矩阵一次性处理。再比如把所有像素的亮度整体增加50bright_img cv2.add(img, (50, 50, 50, 0)) # 注意不是直接img 50因为uint8会溢出 # cv2.add内部会自动做饱和处理超过255就截断为2552.4 三种方式的性能实测文字说再多不如看数据。我用一张3000x3000的彩色噪声图做了一次简单测试把所有像素点的蓝色通道值加1。方式代码示意实测耗时双层for循环for y in range(H): for x in range(W): img[y,x,0] 1约12秒单层循环 .itemset()for i in range(n): img.itemset((y,x,0), val1)约8秒NumPy向量化img[:, :, 0] cv2.add(img[:, :, 0], 1)约8毫秒差距1500倍。真实项目里一张1080P的图也就几百万像素for循环遍历一遍要数秒而这只是做了最简单的操作。如果每个像素还要做复杂的逻辑判断耗时更恐怖。所以向量化不是优化建议而是必须遵守的基本功。那一开始学的时候要不要练for循环遍历像素我的建议是调试小图、验证算法逻辑的时候可以用理解像素的读取与修改方式可以用但一旦要上真数据立刻切换到向量化写法。这不是说for循环没用而是Python这个语言本身就不适合大规模数值循环把循环交给C语言实现的NumPy才是正确姿势。3. 从单个像素到区域操作ROI、通道拆分与条件掩码像素是图像的基本单元但真实项目里很少有人会一个个地点像素。更多的情况是我需要把图像的某个区域抠出来处理或者把整个通道拆出来分析或者把满足一定条件的像素批量替换。这一节讲的都是实战里用得非常频繁的操作。3.1 ROI裁剪选一块区域当作独立小图ROIRegion of Interest感兴趣区域是图像处理里的常用概念。在OpenCV里ROI就是通过切片实现的# 裁剪出左上角200x200的区域 roi img[0:200, 0:200]切片返回的是原图的一个视图view不是一份独立的数据拷贝。这意味着如果你修改了roi原图也会跟着变。这在某些场景下是特性比如你想在原图上直接打标记但在另一些场景下是坑比如你想先处理一个区域、处理完再贴回去结果发现原图已经被改了。如果需要独立的数据副本记得显式调用.copy()roi img[0:200, 0:200].copy()如何理解视图和拷贝的区别可以类比成在原文上用荧光笔划线和复印一份再划线的区别。视图是前者修改会同步到原图拷贝是后者独立修改互不影响。ROI最常见的应用是目标定位。比如人脸检测框定了人脸的位置后续的识别、比对都基于这个ROI来处理不需要整图操作。这既提高了效率也避免了无关区域的干扰。3.2 通道拆分与合并你以为的灰度图可能不是那么回事有时候你只关心图像的某一个通道。比如分析肤色的时候红色通道的信息往往更有区分度做工业缺陷检测时灰度图就够用。通道操作有三种常见写法import cv2 img cv2.imread(demo.jpg) # 方法1直接索引推荐快 b_channel img[:, :, 0] g_channel img[:, :, 1] r_channel img[:, :, 2] # 方法2cv2.split方便但稍慢 b, g, r cv2.split(img) # 合并通道 img_merge cv2.merge([b, g, r])cv2.split写起来更直观但它需要创建三个新的数组内部有一次性拷贝的开销而直接索引只是取视图几乎零成本。所以我个人习惯用索引方式取得通道只有需要一次拆多个通道、且对性能不敏感的时候才用split。还有一个非常实用的技巧把三个通道全部合并成灰度图最简单的方式是cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)它内部做了加权平均不是简单的三个通道取平均符合人眼对亮度感知的权重比例。虽然从数学上你也可以自己用0.114*B 0.587*G 0.299*R来算但直接用官方函数更省心。3.3 条件掩码批量改像素的魔法剪刀如果你想把图像中所有亮度值大于200的像素变成白色或者把所有饱和度高的区域变暗这类需求用条件掩码mask是最高效的。掩码的本质是一个与图像同尺寸的二值数组True表示这个位置的像素满足条件、需要被处理False表示跳过。NumPy里可以直接用布尔条件构造掩码import numpy as np import cv2 img cv2.imread(demo.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 构造掩码亮度大于200的位置标记为True mask gray 200 # 把满足条件的像素设置为白色BGR全255 img[mask] (255, 255, 255)这一行img[mask] (255, 255, 255)内部做的事情相当于遍历了所有像素、判断条件、赋值但整个循环被NumPy在C语言层面完成了所以速度极快。同样的思路还可以做更复杂的操作。比如把某个区域的颜色统一调整# 只处理图像的左上角100x100区域中亮度大于150的像素 region img[0:100, 0:100] region_gray cv2.cvtColor(region, cv2.COLOR_BGR2GRAY) mask_region region_gray 150 region[mask_region] (0, 0, 255) # 染成红色条件掩码是像素操作和图像分析之间的一座重要桥梁。很多看似高级的功能比如背景抠除、颜色分割、阈值分割底层都是先构造掩码再按掩码批量修改像素。理解了这个逻辑OpenCV里很多看似高级的函数你都能猜出实现思路。4. 像素级算法实战反色、亮度调节与阈值分割前面讲了底层原理和基础操作这一章来三个完整的实战小案例。每一个都很简单但都是真实项目里的基本构件而且能让你把前面的知识串起来。4.1 反色最简单的写法比你想的还简单反色也叫负片效果就是把图像中的黑变白、白变黑。对于uint8图像反色公式是新值 255 - 原值。import cv2 import numpy as np img cv2.imread(demo.jpg) # 方法1直接用NumPy计算 inverted 255 - img # 方法2用OpenCV的bitwise_not inverted2 cv2.bitwise_not(img) # 显示对比 cv2.imshow(original, img) cv2.imshow(inverted, inverted) cv2.waitKey(0) cv2.destroyAllWindows()255 - img虽然看起来像普通的算术运算但在NumPy里它会作用在每一个元素上不需要任何for循环。而且减法不会产生uint8溢出的问题——255减去任何一个0到255之间的数结果依然在0到255之间。cv2.bitwise_not是OpenCV内置的按位取反操作本质上也是逐像素计算。两种方式结果完全一样用哪个看个人习惯。我个人偏向NumPy写法因为它更直观——一眼就能看出数学关系。4.2 亮度调节的三种思路以及溢出问题怎么办亮度调节的核心是给每个像素的BGR值加上一个偏移量。负数是变暗正数是变亮。错误示范是直接img 50# 这段代码会导致溢出 bright_wrong img 50前面讲过uint8的值超过255会发生溢出。例如像素值230加50后变成280溢出后变成24结果这个像素反而变暗了。图像上会表现为出现很多奇怪的暗斑。正确的做法有两个import cv2 import numpy as np # 方法1cv2.add自动饱和截断 bright1 cv2.add(img, (50, 50, 50, 0)) # 方法2np.clip 手动截断 bright2 np.clip(img.astype(np.int16) 50, 0, 255).astype(np.uint8)cv2.add内部的处理叫作饱和运算saturate operation超过255的值直接截断为255低于0的值截断为0。这是图像处理里处理边界值的标准方式。np.clip的思路是先把uint8转成更大的数据类型比如int16避免中间计算溢出然后截断到0到255最后再转回uint8。这两种方法结果基本一致cv2.add效率更高、代码更短所以日常首选。把亮度调节写成函数可以很方便地做图像增强的对比实验。4.3 阈值分割用掩码把想要的和不要的分开阈值分割是图像处理里最常用的操作之一。经典应用是把一张灰度图中的亮部与暗部分离开。比如工业检测里经常需要提取缺陷区域——如果缺陷比背景亮或暗用阈值分割就能快速圈出位置。import cv2 import numpy as np img cv2.imread(demo.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 大津法自动计算阈值 _, thresh cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 手动指定阈值 _, thresh2 cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY) # 把阈值结果转成白色前景黑色背景的三通道图方便展示 result cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR) cv2.imshow(thresh, thresh) cv2.waitKey(0) cv2.destroyAllWindows()cv2.threshold返回两个值第一个是最终使用的阈值第二个是二值图像。大津法OTSU会根据图像的灰度分布自动找一个最优阈值适合双峰明显的图像。如果图像的光照不均匀、灰度直方图不明显单纯靠阈值就不太可靠了这时候可以考虑自适应阈值或更高级的分割算法但那是后话。做完阈值分割之后得到的二值图本质上就是一个掩码。你可以直接用它来提取原图的感兴趣区域也可以统计前景的像素数量来估算缺陷面积。像素操作和图像分析在这里完美衔接上了。4.4 颜色空间转换后做像素操作HSV的妙用前面一直在BGR空间里操作但有些场景下用BGR空间做判断并不方便。比如你想把图像中红色的区域找出来。在BGR空间里红色可能是[B低, G低, R高]的组合但不同光照下红色在BGR里的取值范围很宽很难用一个简单的阈值框住。这时候把图像转到HSV色相、饱和度、明度空间问题就变得简单了。HSV把颜色信息集中到了H通道色相上红色的H值在0到10和170到180附近判断起来比BGR直观得多。import cv2 import numpy as np img cv2.imread(demo.jpg) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 红色范围的H值大致在0~10和170~180S和V适当放宽 lower_red1 np.array([0, 50, 50]) upper_red1 np.array([10, 255, 255]) lower_red2 np.array([170, 50, 50]) upper_red2 np.array([180, 255, 255]) mask1 cv2.inRange(hsv, lower_red1, upper_red1) mask2 cv2.inRange(hsv, lower_red2, upper_red2) mask cv2.bitwise_or(mask1, mask2) # 用掩码提取红色区域 result cv2.bitwise_and(img, img, maskmask)这里cv2.inRange的作用是生成一个掩码图像中每个像素的HSV值如果在范围内掩码对应位置就是白色255否则是黑色0。cv2.bitwise_and(img, img, maskmask)则是用掩码把原图中的非红色区域全部清掉。这种玩法在颜色识别、物体跟踪、背景抠除中非常常用。理解了像素操作和掩码的关系这些高级功能对你来说就不再是黑盒了。5. 像素操作最容易踩的几个坑每个都让人头大理论讲了一堆、代码也跑通了但真实项目里像素操作最容易出错的地方往往不是算法本身而是那些不起眼的细节。我把这些年踩过的坑集中列一下每个都附上场景和解决办法。5.1 坑一读入的图片是None后面的操作全崩img cv2.imread(不存在的路径.jpg) print(img) # None这个是最低级但最常见的错误。很多人在本地测试的时候路径写对了代码没有任何问题一到部署或者换机器跑就报NoneType object has no attribute shape。其实原因很简单imread找不到文件不会报错而是返回None然后你继续用img.shape操作None自然就崩了。建议在读取图片后立刻加一个判断img cv2.imread(demo.jpg) if img is None: raise ValueError(图像读取失败检查路径)如果确认路径没问题还是读到None另一个常见原因是图片文件本身损坏或者路径含有中文而你的OpenCV版本是用旧版编译的。Linux下还可能是权限问题。这类问题排查起来很费时间一开始就加判断能省掉很多不必要的调试。5.2 坑二uint8溢出导致图像出现奇怪的暗斑前面讲了img 50会导致溢出。这种现象在真实项目里经常用图像上突然出现很多黑点的症状出现而且位置随机非常迷惑。破解方法就是记住一点只要是对uint8图做加减法就用cv2.add或cv2.subtract不要直接用和-。做乘法比如对比度调整也一样直接用img * 1.5会出问题结果超过255时溢出低于0时也可能因为类型转换产生意外。可以用cv2.multiply或者先把图像转成float类型、计算完再clip回uint8。5.3 坑三切片是视图修改会同步到原图这是看起来像bug但其实是特性的典型。很多人写代码时想保存原图副本于是写了img_copy img # 这其实只是给img多起了一个名字并没有复制数据然后对img_copy做修改发现原图img也跟着变了。这是因为Python里img_copy img只是让两个变量指向同一个数组对象数值是共享的。真正的复制要用img_copy img.copy()同样地ROI切片img[0:100, 0:100]得到的也是视图修改它会改原图。如果你希望处理完一个区域再放回去要么用.copy()得到独立副本要么就在原图上直接操作——关键是想清楚需求。一个简单的判断方法如果你不确定是视图还是拷贝就修改一下看原图变不变或者在文档里查函数是否返回copy。养成随时注意数据所有权的好习惯能避免很多冥思苦想的玄学bug。5.4 坑四坐标顺序和通道顺序的混淆汇总一下我见过的所有坐标/通道混淆场景访问img[x, y]而实际应该是img[y, x]用cv2.resize(img, (width, height))但也有人记成 (height, width)OpenCV的resize第一个参数是 (宽, 高)和shape的 (高, 宽) 相反用cv2.circle(img, (x, y), r, ...)画圆的时候圆心坐标是 (x, y)没问题但如果你从img[y, x]取出来的坐标再传给circle就容易在传参时搞混BGR和RGB的混淆尤其是在跟matplotlib、PIL配合显示的时候这些坑的本质是同一个图像的坐标在不同语境下有不同表示方式。解决的办法就是统一约定比如我自己的习惯是访问数组用img[y, x]传给OpenCV画图函数的坐标用(x, y)图像尺寸相关参数脑子里始终绷着一根弦先宽后高还是先高后宽只要在代码里固定住这个约定出错的概率会大幅下降。5.5 坑五遍历像素点太慢导致程序卡死之前提到过用Python的for循环逐像素遍历一张大图会非常慢。实际项目里不少人遇到过这种情况写了一个看似合理的算法比如对每个像素做颜色判断结果一张图处理了几十秒甚至几分钟最终怀疑是程序死循环了。其实程序没有死就是慢。深层原因是Python是解释型语言for循环每条语句都需要解释执行而图像有几百万甚至上千万个像素每个像素都要执行一次循环体这个开销是灾难性的。而NumPy的向量化操作把循环下沉到C语言层配合底层的SIMD指令单指令多数据一次能同时处理多个数值所以快得多。如果你已经陷入了不得不用for循环的境地比如算法的逻辑太复杂很难向量化可以考虑这几个优化方向先把图像缩小cv2.resize再测试算法验证逻辑正确后再上原图。用NumPy的np.frompyfunc或者尝试用Numba做JIT加速。想办法把算法拆解成几个可以用向量化表达的死板步骤组合起来实现同样的逻辑。换个思路OpenCV内置了大量成熟的函数很多看起来需要逐像素判断的操作实际用cv2.threshold、cv2.inRange、cv2.bitwise_and组合就能实现。记住在OpenCV的Python环境里不写循环本身就是一种核心技能。5.6 坑六灰度图还是彩色图单通道操作时形状不对有时候你给我一张(H, W)的灰度图我按(H, W, 3)的彩色图去处理img[:, :, 0]就会报IndexError。反过来彩色图被当成单通道处理也会得到一堆莫名其妙的错误。真实项目里颜色空间和通道数的混乱是新手高频翻车点。建议在读取图像后立即输出一下shape确认img cv2.imread(demo.jpg, cv2.IMREAD_COLOR) # 强制读成彩色图 img_gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 明确转灰度如果你希望程序同时兼容灰度图和彩色图可以在函数开头判断一下if len(img.shape) 2: img cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)这段代码会把灰度图转成三通道的BGR图后续逻辑统一按三通道处理省去很多分支。6. 一些关于像素操作的进阶思考聊完了基础操作和常见坑最后一节讲点进阶但又很实用的内容。像素操作虽然听起来简单但很多复杂功能最终的落点依然是对像素的高效读写。6.1 模板匹配是怎么工作的用像素操作理解会很清晰cv2.matchTemplate是OpenCV里的模板匹配函数。它的原理是把一个小模板图在大图上滑动每个位置都做一次像素级别的相似度计算最终得到一个相似度热力图用来定位目标。这个概念本身很简单但如果你理解了像素操作你会知道它在内存层面做了什么——每次滑动都需要读取模板区域的像素值计算差异。如果你自己用Python写这个双重循环处理一张稍大一点的图就会非常慢这就是为什么实际中要用C或者OpenCV的优化实现。6.2 图像拼接的最底层操作像素值叠加图像拼接、全景图生成底层逻辑也离不开像素操作。权重融合、多频段混合这些听起来很高级的算法本质上都是在做多个来源的像素值按权重相加。图像拼接的简单版本就是把两张图重叠区域的像素取平均复杂版本是检测特征点、计算变换矩阵、最后按权重混合像素。所以你看像素操作是整个OpenCV大厦的地基。地基打牢了后面盖什么楼都稳当。最后再分享一个小技巧当你写代码需要验证自己的像素操作逻辑是否正确时千万不要直接盯着几百行日志看。用最笨的办法截取图像的一小块比如10x10打印这一小块的像素值然后手动心算一遍期望的结果对比一下就知道对不对了。这个小技巧帮我排查了无数个看起来毫无问题但结果就是不对的场景。像素操作是OpenCV里最基础但也最值得反复琢磨的部分。熟练之后你会发现很多复杂的图像处理算法拆到最底层就是那些你见过的像素读取、通道拆分、条件掩码的组合而已。多动手练多写几个不依赖现成函数的小工具比单纯背API有效得多。
返回列表