目录
[三、图像数组:浅拷贝 vs 深拷贝](#三、图像数组:浅拷贝 vs 深拷贝)
上一章我们学习了 OpenCV 全套绘图能力,可以在空白画布上绘制直线、圆形、多边形、文字,也做了 RGB 滑动调色工具。绘图本质就是修改图像每一处的像素。本章我们继续向下深挖图片底层:理解 BGR 三通道原理,学会通道拆分与合并;同时弄懂 numpy 图像数组的浅拷贝、深拷贝的区别,搞懂为什么修改一张图另一张也跟着变,全部案例附带完整注释代码。
一、前置知识点回顾
-
OpenCV图像本质是numpy矩阵,彩色图片shape格式:
(高度, 宽度, 通道数) -
彩色图固定3个通道,顺序为 B(蓝)‑G(绿)‑R(红),每个像素取值0‑255;
-
所有图像在内存中都是数组,数组之间赋值会涉及内存引用问题,也就是深浅拷贝。
二、通道的拆分与合并
一张彩色图片由蓝、绿、红三张单通道灰度图叠加而成。
cv2.split(img):把彩色图拆开,返回b,g,r三个单通道数组;cv2.merge([b,g,r]):把三个单通道重新合并回一张彩色图像。
注意:merge接收的是列表
[b,g,r],顺序严格B‑G‑R,顺序错图片颜色直接错乱。
python
# opencv色块通道分离.py
import cv2
import numpy as np
创建200×200黑色三通道画布
img = np.zeros(shape=(200,200,3),dtype=np.uint8)
将彩色图拆分为b、g、r三个独立单通道
b,g,r = cv2.split(img)
在10~100行、10~100列区域修改通道像素
b[10:100,10:100] = 255
g[10:100,10:100] = 255
r[10:100,10:100] = 2 # 如果数值是0,则表示纯黑
把三个通道重新合并,传入列表[b,g,r]
img2 = cv2.merge([b,g,r])
cv2.imshow('img',img)
cv2.imshow('img2',img2)
while 1:
key = cv2.waitKey(10)
if key == ord('q'):
break
cv2.destroyAllWindows()
生活通俗理解
可以想象彩色图片是三张透明玻璃纸叠在一起:蓝色玻璃、绿色玻璃、红色玻璃。
split 就是把三张玻璃拆分开,我们能够单独修改其中任意一张的像素;merge 再把三张玻璃重新堆叠,合成一张彩色图片。
**注意:**普通图片只有 BGR 三层,只有带透明度的图片才会多出 Alpha 透明层。
常见使用场景
- 单独增强某一个颜色通道;
- 制作单色滤镜效果;
- 处理带透明通道(Alpha)的图片(如PNG格式的BGRA四通道图像)。
性能与适用场景对比
cv2.split() 与直接索引的性能差异
在 OpenCV 中,cv2.split() 和直接使用 numpy 索引(如 img[:, :, 0])都可以访问单个通道,但它们在性能、内存占用和适用场景上有所不同:
| 对比维度 | cv2.split() |
直接索引(如 img[:, :, 0]) |
|---|---|---|
| 性能 | 较慢(需要创建三个独立数组副本) | 极快(返回原数组的视图,不复制数据) |
| 内存占用 | 高(三个完整单通道数组副本) | 低(只是原数组的一个视图) |
| 数据独立性 | 独立副本,修改不影响原图 | 视图,修改会直接影响原图对应通道 |
| 适用场景 | 需要独立操作每个通道(如分别滤波、阈值处理) | 快速查看、简单修改单个通道(如置零、缩放) |
| 代码简洁性 | 需要 split + merge 两步 | 单行操作,无需合并 |
简单性能测试代码示例:
python
import cv2
import numpy as np
import time
创建一张较大图片用于测试
img = np.random.randint(0, 256, (1000, 1000, 3), dtype=np.uint8)
测试 cv2.split() 性能
start = time.time()
for _ in range(100):
b, g, r = cv2.split(img)
# 对蓝色通道进行简单操作
# 注意:b 是 uint8 类型,乘以 0.5 后会变成 float 类型
# 这里仅做耗时演示,真实代码运算后需要转回 np.uint8
b = (b * 0.5).astype(np.uint8) # 正确写法:先乘后转回 uint8
img_split = cv2.merge([b, g, r])
time_split = time.time() - start
print(f"cv2.split() + merge 100次耗时: {time_split:.4f} 秒")
测试直接索引性能
start = time.time()
for _ in range(100):
# 直接操作蓝色通道
img_direct = img.copy()
img_direct[:, :, 0] = img_direct[:, :, 0] * 0.5
time_direct = time.time() - start
print(f"直接索引操作 100次耗时: {time_direct:.4f} 秒")
print(f"直接索引比 split+merge 快 {time_split/time_direct:.2f} 倍")
结论与建议:
- 追求性能 :如果只是简单查看或修改单个通道,优先使用直接索引(
img[:, :, 通道索引])。 - 需要独立副本 :如果需要对每个通道进行复杂、独立的处理(如分别应用不同的滤波器),使用
cv2.split()获取独立数组更安全。 - 内存敏感 :处理大图像时,直接索引几乎不增加内存占用,而
cv2.split()会创建三个完整副本,内存消耗是原图的3倍。 - 代码可读性 :
cv2.split()语义更清晰,明确表示"拆分通道";直接索引更简洁,适合快速操作。
在实际项目中,可以根据具体需求灵活选择:简单通道操作用直接索引,复杂多通道独立处理用 cv2.split()。
三、图像数组:浅拷贝 vs 深拷贝
新手高频坑点:直接用
img2 = img1并不是复制图片,只是多了一个别名,两者指向同一块内存 。修改其中一个,另一个同步跟着改变,这就是浅拷贝。如果希望两张图片完全互不干扰,必须使用
.copy()做深拷贝,开辟全新内存。
python
# opencv深浅拷贝.py
import cv2
读取原图
注意:路径中的 'your name.jpg' 仅为示例,请替换为你自己的图片路径
建议路径中不要包含中文、空格等特殊字符,避免部分环境读取失败
img1 = cv2.imread(r'D:\PYTHON\your name.jpg')
图片读取判空:如果图片读取失败,img1 将为 None,后续操作会报错
if img1 is None:
print("图片读取失败,请检查路径是否正确,路径中不要包含中文、空格等特殊字符")
exit()
浅拷贝:view(),img1、img2共用同一块内存空间
img2 = img1.view()
深拷贝:copy(),开辟全新内存,两张图像完全独立互不影响
img3 = img1.copy()
修改img1局部像素:0‑100行,10‑100列区域,设置为紫色BGR(255,0,255)
img1[0:100,10:100] = [255,0,255]
展示三张图片,观察变化
cv2.imshow('img1',img1)
cv2.imshow('img2',img2)
cv2.imshow('img3',img3)
while 1:
key = cv2.waitKey(10)
if key == ord('q'):
break
cv2.destroyAllWindows()
运行现象:
- img1被修改,出现紫色色块;
- img2浅拷贝,和原图共享内存,也会出现紫色色块;
- img3是深拷贝副本,不受原图修改的任何影响,保持原始画面。
三种赋值方式对比表
| 写法 | 拷贝类型 | 内存关系 | 修改后表现 |
|---|---|---|---|
img2 = img1 |
浅拷贝(直接引用) | 共用同一块内存 | 修改任意一个全部变化 |
img2 = img1.view() |
浅拷贝view | 共用内存,视图不同 | 修改原图副本同步改变 |
img3 = img1.copy() |
深拷贝 | 全新独立内存 | 修改互不干扰 |
补充说明: numpy 的 .view() 方法主要作用是改变数组的视图(如形状、维度)或数据类型(dtype),而不会复制底层数据。对于普通的图像数组,img2 = img1 和 img2 = img1.view() 在内存共享方面的效果几乎一致,都是浅拷贝。很多新手会误以为 view() 是复制图像,实际上它只是创建了一个新的数组视图,底层数据仍然是共享的,因此修改原图时,通过 view() 创建的视图也会同步变化。
重要提醒:很多同学写代码想保留原图,做处理后的新图,直接写
new_img = img,处理完发现原图被改掉,就是踩了浅拷贝的坑,一定要.copy()。
四、像素直接读写与切片拓展
图像本质numpy数组,可以直接用下标读写像素,格式:img[y, x],y代表行(高度),x代表列(宽度)。
python
import cv2
import numpy as np
img = np.zeros((300,300,3),np.uint8)
读取坐标(50,60)像素,y=50行,x=60列
px = img[50,60]
print("该点BGR像素值:",px)
修改单个点像素为蓝色
img[50,60] = (255,0,0)
切片:修改一片矩形区域
img[100:200, 100:200] = (0,255,0)
cv2.imshow("demo",img)
cv2.waitKey(0)
cv2.destroyAllWindows()
易错:
img[y,x],先写行y(高度),再写列x(宽度),不要写成(x,y) ,这点和绘图函数cv2.line(img,(x,y),...)坐标顺序相反。
**重点难点强化:**这是 OpenCV 新手头号坑!请务必牢记:
- numpy 数组像素读写:
img[y, x]→ 先行 y(高度/行),后列 x(宽度) - OpenCV 绘图函数(circle、rectangle、line 等):
cv2.line(img, (x, y), ...)→ 先 x(横向/列),后 y(纵向/行)
两者 (x, y) 顺序完全颠倒!写反之后画面坐标会完全错乱,比如你想在 (100, 50) 位置画点,如果写成 img[100, 50] 和 cv2.circle(img, (100, 50), ...),实际上操作的是两个不同的位置。建议在代码中统一使用 (x, y) 变量命名,但根据上下文区分顺序。
五、通道拆分+拷贝组合练习
读取一张图片,做两件事:
- 深拷贝一份原图保护原始数据;
- 拆分通道,把蓝色通道置零,生成偏黄风格图片。
python
import cv2
import numpy as np
img_origin = cv2.imread(r"D:\PYTHON\your name.jpg")
if img_origin is None:
print("图片读取失败,请检查路径")
exit()
深拷贝,保护原始图片
img_work = img_origin.copy()
拆分BGR三通道
b,g,r = cv2.split(img_work)
将蓝色通道全部置0
b[:] = 0 # 修改原通道数组,等价于:b = np.zeros_like(b)
重新合并通道
img_yellow = cv2.merge([b,g,r])
cv2.imshow("原图",img_origin)
cv2.imshow("去掉蓝色通道",img_yellow)
while True:
k = cv2.waitKey(10)
if k == ord('q'):
break
cv2.destroyAllWindows()
小拓展:不用 split 也能直接修改单通道
很多同学不知道,numpy 数组切片可以直接操作通道,不需要先 split 再 merge:
python
# 直接把图片蓝色通道全部置0,等价于 split+merge
img_work[:, :, 0] = 0
除了使用 cv2.split() 拆分通道,numpy 数组可以直接通过索引访问通道:
img[:, :, 0]代表全部像素的第 0 号 B 通道(蓝色)img[:, :, 1]代表全部像素的第 1 号 G 通道(绿色)img[:, :, 2]代表全部像素的第 2 号 R 通道(红色)
这种直接索引的方式在很多场景下可以省去 split+merge 的步骤,代码更简短。当然,cv2.split() 仍然有其优势,比如需要单独拿某个单通道做复杂运算时,split 后的独立数组更方便操作。
代码说明:
b[:] = 0:这是对原数组b的所有元素进行原地赋值,将每个像素值设为 0。这种方式直接修改了b数组的内容。b = np.zeros_like(b):这是创建一个与b形状和数据类型完全相同但所有元素都为 0 的新数组,然后将变量b指向这个新数组。原数组如果没有其他引用,会被垃圾回收。
两种写法最终效果相同(蓝色通道全为0),但内存操作方式不同:b[:] = 0 是原地修改,而 b = np.zeros_like(b) 是创建新数组并重新赋值。在通道操作中,两种方式都可以使用,根据实际需求选择。
六、本章高频踩坑清单
- 通道顺序B‑G‑R,不是RGB
merge合并的时候顺序写错,图片颜色完全错乱。 - 混淆x,y下标
numpy数组像素下标img[y,x](先行后列),OpenCV绘图函数坐标是(x,y),顺序相反,很容易写反。 - 忘记深拷贝,原图被意外修改
直接new_img = img,处理新图片,原始图片跟着改变。需要保留原图,务必.copy()。 - split、merge传参格式错误
cv2.merge()接收列表[b,g,r],不要直接写cv2.merge(b,g,r)会报错。 - 浅拷贝view()只是视图共享内存,不是复制图片,不要用来做备份。
- 循环内部waitKey不能写0
waitKey(0)会无限阻塞等待按键,如果放在while循环里面程序会卡死。循环中必须传大于0的数字,例如waitKey(10),表示等待10毫秒后继续执行。
七、本章知识小结
- 彩色图像是BGR三通道矩阵,可以用
cv2.split()拆分成单通道,cv2.merge()重新合并; - 图像是numpy数组,存在浅拷贝、深拷贝的区别;浅拷贝共享内存,修改互相影响;
copy()深拷贝开辟独立内存,两张图片互不干扰; - 像素读写使用数组切片
img[y,x],先y行,后x列,和绘图坐标顺序相反; - 如果程序中需要保留原始图片不被破坏,处理之前一定要执行
.copy(); - 利用通道拆分合并可以简单实现单色滤镜、调色效果。
- 通道操作性能优化:简单通道修改(如置零、缩放)优先使用numpy直接索引(
img[:, :, 通道索引]),速度快且内存占用低;多通道独立复杂运算(如分别滤波)则选用cv2.split()获取独立副本更安全。 - 数组修改方式差异:
b[:] = 0是原地修改原数组;b = np.zeros_like(b)是生成全新数组并重新赋值,两者内存行为不同,需根据场景选择。
下一章我们进入图像滤波降噪专题,学习高斯滤波、中值滤波、双边美颜滤波,理解卷积操作,为后面边缘检测打下基础。