数字图像处理中使用的基本数学工具有三种,分别是:
1. 列阵与矩阵操作
2. 基于像素灰度值的线性操作非线性操作
3. 对图片对应位置像素处理的算数操作
1. 尺寸调整
1.1. 比例缩放
#图像大小调整 ori_h, ori_w = image.shape[:2] image = cv2.resize(image, (int(ori_w/ori_h*400), 400), interpolation=cv2.INTER_CUBIC) image = cv2.resize(src, dsize, dst, fx, fy, interpolation]]]])
参数说明
1. src
: np.array 【必需】原图像
2. dsize
: tuple
- 【必需】输出图像所需大小 (x,y)
3. fx
【可选】沿水平轴的比例因子
4. fy
【可选】沿垂直轴的比例因子
5. interpolation
- 【可选】插值方式
1.2. 图像裁剪
import cv2 img = cv2.imread('test.png',1) dst = img[200:600, 0:300] # 裁剪坐标为[y0:y1, x0:x1] cv2.imshow('image',dst)
图像金字塔与图片尺寸缩放
2. 颜色空间转换
在 OpenCV 中有超过 150 种颜色空间转换的方法。但我们仅需要研究两个最常使用的方法,他们是 BGR -> Gray
、BGR ->HSV
。
image=cv.cvtColor(image, flag)
2.1. BGR转换RGB
OpenCV默认情况下会以蓝色、绿色、红色通道(BGR)
打开图像,正常显示的彩色图片是RGB格式,需要转化
image_path ='dog2.JPG' image= cv2.imread(image_path) image_cv = cv2.cvtColor(image,cv2.COLOR_BGR2RGB) type(gray) >>> numpy.ndarray gray.shape >>>(w,h,3)
2.2. 灰度化处理
将一个像素点的三个颜色变量相等,R=G=B,此时该值称为灰度值
$$Gray=R0.299+G0.587+B*0.114$$
#读入原始图像 img=cv2.imread('test.jpg') #灰度化处理 gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) type(gray) >>> numpy.ndarray gray.shape >>>(436, 750)
3. 灰度变换
灰度变换是图像处理中最简单的技术,如果x和y分别是处理前和处理后的像素值,灰度变换相当于使用函数:
$$y = T(x)$$
灰度变换本质上是二维向量转换。
将灰度值x映射到灰度值y,常用的灰度变换函数有:
1. 线性函数
2. 对数函数
3. 冥律函数
3.1. 线性变换
$$y =a\times x+b$$
参数a
影响图像的对比度,参数 b
影响图像的亮度,具体分为可分为以下几种情况:
- a>1:增强图像的对比度,图像看起来更加清晰
- a<1:减小了图像的对比度,图像看起来变暗
- a<0 and b=0:图像的亮区域变暗,暗区域变亮
- a=1 and b≠0:图像整体的灰度值上移或者下移,也就是图像整体变亮或者变暗,不会改变图像的对比度,b>0时图像变亮,b<0时图像变暗
- a=-1 and b=255:图像翻转
3.1.1. 对比度调整
3.1.1.1. 增强对比度
#a>1: 增强图像的对比度,图像看起来更加清晰 a, b = 1.5, 20 new_img2 = np.ones((gray.shape[0], gray.shape[1]), dtype=np.uint8) for i in range(new_img2.shape[0]): for j in range(new_img2.shape[1]): if gray[i][j]*a + b > 255: new_img2[i][j] = 255 else: new_img2[i][j] = gray[i][j]*a + b
3.1.1.2. 降低对比度
#a<1: 减小了图像的对比度, 图像看起来变暗 a, b = 0.5, 0 new_img3 = np.ones((gray.shape[0], gray.shape[1]), dtype=np.uint8) for i in range(new_img3.shape[0]): for j in range(new_img3.shape[1]): new_img3[i][j] = gray[i][j]*a + b
3.1.2. 亮度调整
3.1.2.1. 增加亮度
#a=1且b≠0, 图像整体的灰度值上移或者下移, 也就是图像整体变亮或者变暗, 不会改变图像的对比度 a, b = 1, 50 ## b >0 new_img4 = np.ones((gray.shape[0], gray.shape[1]), dtype=np.uint8) for i in range(new_img4.shape[0]): for j in range(new_img4.shape[1]): pix = gray[i][j]*a + b if pix > 255: new_img4[i][j] = 255 elif pix < 0: new_img4[i][j] = 0 else: new_img4[i][j] = pix
3.1.2.2. 降低亮度
#a=1且b≠0, 图像整体的灰度值上移或者下移, 也就是图像整体变亮或者变暗, 不会改变图像的对比度 a, b = 1, -50 ## b <0 new_img4 = np.ones((gray.shape[0], gray.shape[1]), dtype=np.uint8) for i in range(new_img4.shape[0]): for j in range(new_img4.shape[1]): pix = gray[i][j]*a + b if pix > 255: new_img4[i][j] = 255 elif pix < 0: new_img4[i][j] = 0 else: new_img4[i][j] = pix
3.1.2.3. 反向亮度变换
#a<0 and b=0: 图像的亮区域变暗,暗区域变亮 a, b = -0.5, 0 new_img1 = np.ones((gray.shape[0], gray.shape[1]), dtype=np.uint8) for i in range(new_img1.shape[0]): for j in range(new_img1.shape[1]): new_img1[i][j] = gray[i][j]*a + b
3.1.3. 反色变换
Opencv中的反色变换:对原图像像素值的颜色进行反转,即黑色变为白色,白色变为黑色。
#a=-1, b=255, 图像翻转 new_img5 = 255 - gray
3.2. 幂律变换(伽马变换)
$$y=c\times x^w$$
根据 w
的大小,主要可分为一下两种情况:
1. w>1
: 处理漂白的图片,进行灰度级压缩
2. w<1
: 处理过黑的图片,对比度增强,使得细节看的更加清楚
#伽马变换 gamma=copy.deepcopy(gray) rows=img.shape[0] cols=img.shape[1] for i in range(rows): for j in range(cols): gamma[i][j]=3*pow(gamma[i][j],0.8)
3.3. 对数变换
$$y=c\times\log{(1+x)}$$
# 对数变换 logc = copy.deepcopy(gray) for i in range(rows): for j in range(cols): logc[i][j] = 3 * math.log(1 + logc[i][j])
4. 直方图均衡化
直方图均衡化是通过拉伸像素强度的分布范围,使得在0~255灰阶上的分布更加均衡,提高了图像的对比度,达到改善图像主观视觉效果的目的。对比度较低的图像适合使用直方图均衡化方法来增强图像细节。
在OpenCV中,我们使用的是 cv2.equalizeHis()
这个函数来实现直方图均衡化:
#直方图的应用 ## 直方图均衡化(即调整图像的对比度) ## 直方图即统计各像素点的频次 import cv2 as cv #全局直方图均衡化 def eaualHist_demo(image): gray = cv.cvtColor(image, cv.COLOR_RGB2GRAY) #opencv的直方图均衡化要基于单通道灰度图像 cv.namedWindow('input_image', cv.WINDOW_NORMAL) cv.imshow('input_image', gray) dst = cv.equalizeHist(gray) #自动调整图像对比度,把图像变得更清晰 cv.namedWindow("eaualHist_demo", cv.WINDOW_NORMAL) cv.imshow("eaualHist_demo", dst) #局部直方图均衡化 def clahe_demo(image): gray = cv.cvtColor(image, cv.COLOR_RGB2GRAY) clahe = cv.createCLAHE(5, (8,8)) dst = clahe.apply(gray) cv.namedWindow("clahe_demo", cv.WINDOW_NORMAL) cv.imshow("clahe_demo", dst) src = cv.imread('1.png') eaualHist_demo(src) clahe_demo(src) cv.waitKey(0) cv.destroyAllWindows()
import cv2 as cv from matplotlib import pyplot as plt def plot_demo(image): plt.hist(image.ravel(), 256, [0, 256]) #numpy的ravel函数功能是将多维数组降为一维数组 plt.show() def image_hist(image): #画三通道图像的直方图 color = ('b', 'g', 'r') #这里画笔颜色的值可以为大写或小写或只写首字母或大小写混合 for i , color in enumerate(color): hist = cv.calcHist([image], [i], None, [256], [0, 256]) #计算直方图 plt.plot(hist, color) plt.xlim([0, 256]) plt.show() src = cv.imread('1.png') cv.namedWindow('input_image', cv.WINDOW_NORMAL) cv.imshow('input_image', src) plot_demo(src) image_hist(src) cv.waitKey(0) cv.destroyAllWindows()
6. 边缘检测
6.1. 边缘是什么?
边缘就是灰度值变化较大的的像素点的集合。一道黑边一道白边中间就是边缘,它的灰度值变化是最大的,在图像中,用梯度来表示灰度值的变化程度和方向。
由于图像中不可避免的存在噪声和模糊,边缘检测往往与滤波操作结合使用。边缘检测可以通过计算图片中像素点的一阶导数或者二阶导数实现。
边缘检测本质上就是一种滤波算法,区别在于滤波器的选择,滤波的规则是完全一致的。
6.2. 基本步骤
边缘检测的一般步骤:
1. 滤波——消除噪声
2. 增强——使边界轮廓更加明显
3. 检测——选出边缘点
图像的滤波一般是基于灰度图进行的。
边缘检测是基于灰度突变实现图像分割最常用的方法,根据灰度剖面分类,边缘模型有:
1. 台阶模型
2. 斜坡模型
3. 屋顶边缘模型
6.3. 高级算法
6.3.1. Canny
Canny边缘检测是一种流行的边缘检测算法。它是由约翰坎尼在1986年开发的。这是一个多阶段的算法。其目标是找到一个最优的边缘,其最优边缘的定义是:
1. 好的检测 --算法能够尽可能多地标示出图像中的实际边缘
2. 好的定位 --标识出的边缘要与实际图像中的实际边缘尽可能接近
3. 最小响应 --图像中的边缘只能标识一次,并且可能存在的图像噪声不应该标识为边缘
步骤:
1. Noise Reduction
2. Finding Intensity Gradient of the Image
3. Non-Maximun Suppression
4. Double Threhold
5. Edge tracking by hysteresis
6.3.1.1. 降噪
图片中的高频信息指颜色快速变化,低频信息指颜色平缓的变化。边缘检测过程中需要检测的图片边缘属于高频信息。而图片中噪声部分也属于高频信息,因此我们需要对图像进行去噪处理。
常用的是使用5*5的高斯滤波核来平滑图像,滤波核的数量呈高斯分布。
6.3.1.2. 找出梯度较大的区域
计算像素梯度的幅值以及方向,常用的算子有Rober,sobel,计算水平及垂直方向的差分。找出梯度较大的区域,这部分区域属于图像增强的区域,此时得到的边缘信息比较粗大。
6.3.1.3. 非极大值抑制
非极大值抑制属于一种边缘细化的方法,梯度大的位置有可能为边缘,在这些位置沿着梯度方向,找到像素点的局部最大值,并将非最大值抑制。
6.3.1.4. 双阀值
由于存在很多伪边缘,因此Canny算法中所采用的算法为双阈值法,具体思路为选取两个阈值,将小于低阈值的点认为是假边缘置0,将大于高阈值的点认为是强边缘置1,介于中间的像素点需进行进一步的检查。
双阀值方法,设置一个maxval,以及一个minval,梯度大于maxval则为强边缘,梯度值介于maxval与minval则为弱边缘点,小于minval为抑制点。
6.3.1.5. 滞后边缘追踪
滞后边缘追踪,主要处理梯度值位于maxval,minval中的一些像素点。由于边缘是连续的,因此可以认为弱边缘如果为真实边缘则和强边缘是联通的,可由此判断其是否为真实边缘。
def edge_detect(img): #1. 高斯降噪 blurred = cv2.GaussianBlur(img,(3,3),0) # 1.2 灰度图像 gray = cv2.cvtColor(blurred,cv2.COLOR_RGB2GRAY) #2. 图像梯度 xgrad = cv2.Sobel(gray,cv2.CV_16SC1,1,0) ygrad = cv2.Sobel(gray,cv2.CV_16SC1,0,1) #3. 计算边缘 #50和150参数必须符合1:3或者1:2 edge_output = cv2.Canny(xgrad,ygrad,50,150) dst = cv2.bitwise_and(img,img,mask=edge_output) return edge_output, dst
edges = cv2.Canny(image, threshold1, threshold2) # 参数: # image:图片 # threshold1:minval # threshold2:maxval # kernel = 3
import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread('doge.jpg',0) edges = cv2.Canny(img,100,200) #参数:图片,minval,maxval,kernel = 3 plt.subplot(121) #121表示行数,列数,图片的序号即共一行两列,第一张图 plt.imshow(img,cmap='gray') #cmap :colormap 设置颜色 plt.title('original image'),plt.xticks([]),plt.yticks([]) #坐标轴起点,终点的值 plt.subplot(122),plt.imshow(edges,cmap = 'gray') plt.title('edge image'),plt.xticks([]),plt.yticks([]) plt.show()
6.3.2. Marr-Hildreth
Marr-Hildreth算子
7. 轮廓检测
# openCV2 openCV4 contours,hierarchy=cv2.findContours(image, mode, method[, contours[, hierarchy[, offset ]]]) # openCV3 img, countours, hierarchy=cv2.findContours(image, mode, method[, contours[, hierarchy[, offset ]]]) # image: np.array, 二值图,即黑白的(不是灰度图) # mode: cv2 class 检测模式 # # cv2.RETR_EXTERNAL 表示只检测外轮廓 # # cv2.RETR_LIST 检测的轮廓不建立等级关系 # # cv2.RETR_CCOMP 建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息。如果内孔内还有一个连通物体,这个物体的边界也在顶层。 # # cv2.RETR_TREE 建立一个等级树结构的轮廓。 # method: cv2 class 轮廓的近似方法 # # cv2.CHAIN_APPROX_NONE 存储所有的轮廓点,相邻的两个点的像素位置差不超过1,即max(abs(x1-x2),abs(y2-y1))==1 # # cv2.CHAIN_APPROX_SIMPLE 压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需4个点来保存轮廓信息 # # cv2.CHAIN_APPROX_TC89_L1 使用teh-Chinl chain 近似算法 # # cv2.CV_CHAIN_APPROX_TC89_KCOS 使用teh-Chinl chain 近似算法 contours: list [contour1,contours2,...] contour1: np.array
8. 图像梯度计算
8.1. 一维梯度算子
一维梯度算子只能计算一个方向的梯度变化,常用的一维梯度算子有:
1. Roberts算子
2. Prewitt 算子
3. Sobel 算子
8.1.1. Roberts算子
$$
S_x=\left[
\begin{matrix}
1 & 0 \
0 & -1
\end{matrix}
\right],S_y=\left[
\begin{matrix}
0 & 1 \
-1 & 0
\end{matrix}
\right]
$$
其中$S_x 、S_y$分别表示对于X轴、Y轴的边缘检测算子
8.1.2. Prewitt 算子
$$
S_x=\left[
\begin{matrix}
-1 & 0 & 1\
-1 & 0 & 1\
-1 & 0 & 1
\end{matrix}
\right],S_y=\left[
\begin{matrix}
1 & 1 & 1\
0 & 0 & 0\
-1 & -1 & -1
\end{matrix}
\right]
$$
其中$S_x 、S_y$分别表示对于X轴、Y轴的边缘检测算子
8.1.3. Sobel 算子
Sobel算子是一种高斯平滑加微分的联合算子,因此对噪声的抵抗能力更强。可以指定要获取的垂直或水平导数的方向(通过参数, yorder 和 xorder 分别)。也可以通过参数指定内核的大小 ksize . 如果ksize=-1,则使用3x3 Scharr滤波器,其结果比3x3 Sobel滤波器更好。请参阅所用内核的文档。
$$
S_x=\left[
\begin{matrix}
-1 & 0 & 1\
-2 & 0 & 2\
-1 & 0 & 1
\end{matrix}
\right],S_y=\left[
\begin{matrix}
1 & 2 & 1\
0 & 0 & 0\
-1 & -2 & -1
\end{matrix}
\right]
$$
其中$S_x 、S_y$分别表示对于X轴、Y轴的边缘检测算子
dst=cv2.Sobel(src, ddepth, dx, dy, dst=None, ksize=None, scale=None, delta=None, borderType=None) type(dst) >>> np.array # src: np.array 原图 # ddepth: int 参数是图像的深度,-1表示采用的是与原图像相同的深度。目标图像的深度必须大于等于原图像的深度; # dx: x方向上求导的阶数,0表示这个方向上没有求导,一般为0、1、2。 # dy: y方向上求导的阶数,0表示这个方向上没有求导,一般为0、1、2。 # ksize是Sobel算子的大小,必须为1、3、5、7。 # scale是缩放导数的比例常数,默认情况下没有伸缩系数; # delta是一个可选的增量,将会加到最终的dst中,同样,默认情况下没有额外的值加到dst中; # borderType是判断图像边界的模式。这个参数默认值为cv2.BORDER_DEFAULT
8.1.4. Scharr
Scharr 是对 Sobel(使用小的卷积核求解求解梯度角度时)的优化。
虽然Sobel算子可以有效的提取图像边缘,但是对图像中较弱的边缘提取效果较差。因此为了能够有效的提取出较弱的边缘,需要将像素值间的差距增大,因此引入Scharr算子。Scharr算子是对Sobel算子差异性的增强,因此两者之间的在检测图像边缘的原理和使用方式上相同。Scharr算子的边缘检测滤波的尺寸为3×3,因此也有称其为Scharr滤波器。可以通过将滤波器中的权重系数放大来增大像素值间的差异,Scharr算子就是采用的这种思想,其在X方向和Y方向的边缘检测算子如下所示。
$$
S_x=\left[
\begin{matrix}
-3 & 0 & 3\
-10 & 0 & 10\
-3 & 0 & 3
\end{matrix}
\right],S_y=\left[
\begin{matrix}
-3 & -10 & -3\
0 & 0 & 0\
3 & 10 & 3
\end{matrix}
\right]
$$
# cv2 4.0+ dst=cv2.Scharr(src, ddepth, dx, dy[, dst[, scale[, delta[, borderType]]]]) # src:待提取边缘的图像, # dst:输出图像,与输入图像src具有相同的尺寸和通道数,数据类型由第三个参数ddepth控制。 # ddepth:输出图像的数据类型(深度),根据输入图像的数据类型不同拥有不同的取值范围,具体的取值范围在表5-1给出,当赋值为-1时,输出图像的数据类型自动选择。 # dx:X方向的差分阶数 # dy:Y方向的差分阶数 # scale:对导数计算结果进行缩放的缩放因子,默认系数为1,不进行缩放。 # delta:偏值,在计算结果中加上偏值。 # borderType:像素外推法选择标志,取值范围在表3-5中给出,默认参数为BORDER_DEFAULT,表示不包含边界值倒序填充。
8.2. 二维梯度算子
8.2.1. 拉普拉斯算子
dst = cv2.Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]]) # 前两个是必须的参数: # src:np.array 参数是需要处理的图像; # ddepth: int 参数是图像的深度,-1表示采用的是与原图像相同的深度。目标图像的深度必须大于等于原图像的深度; # 其后是可选的参数: # ksize: int 是算子的大小,必须为1、3、5、7。默认为1。 # scale: int 是缩放导数的比例常数,默认情况下没有伸缩系数; # delta: int 是一个可选的增量,将会加到最终的dst中,同样,默认情况下没有额外的值加到dst中 # borderType: int 是判断图像边界的模式。这个参数默认值为 cv2.BORDER_DEFAULT。
9. 阈值操作
固定阈值操作
Threshold()函数
ret,im_fixed=cv2.threshold(gray,50,255,cv2.THRESH_BINARY_INV)
自适应阈值操作:adaptiveThreshold()函数
阈值操作在图像分割领域处于核心地位。阈值操作可以将物体像素和背景像素有效地分割开,具有直观、实现简单和计算快速的优点。
当阈值操作的阈值对于整个图像而言是一个常数时,称为全局阈值,当阈值随着处理像素位置或者像素的领域情况改变时,称为局部阈值。
9.1. 全局阈值
全局阈值中除了单阈值操作还有多阈值操作,本文使用了Otsu 方法的最佳全局阈值处理和基于均值的自适应阈值处理方法。
9.1.1. 单阈值操作
参数决定。类型如下
cv2.THRESH_BINARY cv2.THRESH_BINARY_INV cv2.THRESH_TRUNC cv2.THRESH_TOZERO cv2.THRESH_TOZERO_INV # 阈值取邻近区域的平均值 cv2.ADAPTIVE_THRESH_MEAN_C # 阈值取带权邻近区域值的和,并且权值是高斯窗口 cv2.ADAPTIVE_THRESH_GAUSSIAN_C
9.1.2. 多阈值操作
9.2. 局部阈值
9.3. 二值化处理
二值化处理:将一个像素点的值突出为0或255,使得图片呈现黑白两种颜色。
$$ y=\left{
\begin{aligned}
0\
255\
\end{aligned}
\right.
$$
#二值化处理 cv2.THRESH_BINARY >>>0 ret,im_fixed=cv2.threshold(gray,50,255,cv2.THRESH_BINARY) # 只能是gray 二维图片 #反二值化处理 cv2.THRESH_BINARY_INV >>>1 ret,im_fixed=cv2.threshold(gray,50,255,cv2.THRESH_BINARY_INV)
10. 滤波操作
第5章 图像滤波 132
5.1 图像卷积 132
5.2 噪声的种类与生成 136
5.2.1 椒盐噪声 136
5.2.2 高斯噪声 139
5.3 线性滤波 142
5.3.1 均值滤波 142
5.3.2 方框滤波 145
5.3.3 高斯滤波 147
5.3.4 可分离滤波 151
5.4 非线性滤波 154
5.4.1 中值滤波 154
5.4.2 双边滤波 156
滤波操作可以分为
1. 空间滤波
2. 频率域滤波
当对图像像素进行的是线性操作时,被称为线性滤波器
,线性空间滤波器和频率域滤波器存在一一对应关系。
滤波器的作用效果可以分为两类:
- 平滑
- 钝化
对于空间滤波器就是对图片中的一个邻域比如矩形区域执行预定义的操作,对于线性滤波器而言,就是通过使用一个核函数和图片中的区域做空间相关操作,一般来说,核函数是大小为奇数的正方形
11. MSER
mser=cv2.MSER_create(_delta=None, _min_area=None, _max_area=None, _max_variation=None, _min_diversity=None, _max_evolution=None, _area_threshold=None, _min_margin=None, _edge_blur_size=None) # 若输入为**灰度**图像,那么采取`MSER`极值区域检测算法 regions, boxes = mser.detectRegions(gray) hulls = [cv2.convexHull(p.reshape(-1, 1, 2)) for p in regions] # 绘制文本区域 cv2.polylines(img, hulls, 1, (255, 0, 0)) cv2.namedWindow("img", 0) cv2.resizeWindow("img", 800, 640) # 限定显示图像的大小 cv2.imshow('img', img) cv2.imwrite('img.jpg', img) # 若输入为**彩色**图像,那么采取`MSCR`极值区域检测算法 regions, boxes = mser.detectRegions(rbg_image)
MSER参数说明
_delta
: 该值将用于 $(size_i - size_{i-delta})/size_{i-delta}$ 的比较,默认为 5。_min_areas
: 小于该面积的区域将被忽略,默认值为 60。_max_areas
: 大于该面积的区域将被忽略,默认值为 14400。_max_variation
: 与其子区域相似的区域将被忽略,默认值 0.25。
MSEC参数说明
_delta
: 该值将用于 $(size_i - size_{i-delta})/size_{i-delta}$ 的比较,默认为 5。_min_areas
: 小于该面积的区域将被忽略,默认值为 60。_max_areas
: 大于该面积的区域将被忽略,默认值为 14400。_max_variation
: 与其子区域相似的区域将被忽略,默认值 0.25。_min_diversity
: 当前区域与稳定区域的变化率,默认值 2。_max_evolution
_area_threshold
_min_margin
: double_edge_blur_size
: int
12. 滤波操作
12.1. 图像平滑
12.2. 图像模糊
# sharpen_op = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]], dtype=np.float32) sharpen_op = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], dtype=np.float32) sharpen_image = cv.filter2D(src, cv.CV_32F, sharpen_op) sharpen_image = cv.convertScaleAbs(sharpen_image) cv.imshow("sharpen_image", sharpen_image)