python数字图像处理(三)边缘检测常用算子
在该文将介绍基本的几种应用于边缘检测的滤波器,首先我们读入saber用来做为示例的图像
#读入图像代码,在此之前应当引入必要的opencv matplotlib numpy
saber = cv2.imread("saber.png")
saber = cv2.cvtColor(saber,cv2.COLOR_BGR2RGB)
plt.imshow(saber)
plt.axis("off")
plt.show()
使用上图作为滤波器使用的图形
Roberts 交叉梯度算子
Roberts交叉梯度算子由两个\(2\times2\)的模版构成,如图。
-1 & 0 \\
0 & 1
\end{bmatrix}
\]
0 & -1 \\
1 & 0
\end{bmatrix}
\]
对如图的矩阵分别才用两个模版相乘,并加在一起
Z_1 & Z_2 & Z_3 \\
Z_4 & Z_5 & Z_6 \\
Z_7 & Z_8 & Z_9 \\
\end{bmatrix}
\]
简单表示即
\(|z_9-z_5|+|z_8-z_6|\)。
可以简单用编程实现
首先将原图像进行边界扩展,并将其转换为灰度图。
gray_saber = cv2.cvtColor(saber,cv2.COLOR_RGB2GRAY)
gray_saber = cv2.resize(gray_saber,(200,200))
在接下来的的代码中我们对图像都会进行边界扩展,是因为将图像增加宽度为1的图像能够使在计算3*3的算子时不用考虑边界情况
def RobertsOperator(roi):
operator_first = np.array([[-1,0],[0,1]])
operator_second = np.array([[0,-1],[1,0]])
return np.abs(np.sum(roi[1:,1:]*operator_first))+np.abs(np.sum(roi[1:,1:]*operator_second))
def RobertsAlogrithm(image):
image = cv2.copyMakeBorder(image,1,1,1,1,cv2.BORDER_DEFAULT)
for i in range(1,image.shape[0]):
for j in range(1,image.shape[1]):
image[i,j] = RobertsOperator(image[i-1:i+2,j-1:j+2])
return image[1:image.shape[0],1:image.shape[1]]
Robert_saber = RobertsAlogrithm(gray_saber)
plt.imshow(Robert_saber,cmap="binary")
plt.axis("off")
plt.show()
python的在这种纯计算方面实在有些慢,当然这也是因为我过多的使用了for循环而没有利用好numpy的原因
我们可以看出Roberts算子在检测边缘效果已经相当不错了,但是它对噪声相当敏感,我们给它加上噪声在看看它的效果
噪声代码来源于stacksoverflow
def noisy(noise_typ,image):
if noise_typ == "gauss":
row,col,ch= image.shape
mean = 0
var = 0.1
sigma = var**0.5
gauss = np.random.normal(mean,sigma,(row,col,ch))
gauss = gauss.reshape(row,col,ch)
noisy = image + gauss
return noisy
elif noise_typ == "s&p":
row,col,ch = image.shape
s_vs_p = 0.5
amount = 0.004
out = np.copy(image)
num_salt = np.ceil(amount * image.size * s_vs_p)
coords = [np.random.randint(0, i - 1, int(num_salt))
for i in image.shape]
out[coords] = 1
num_pepper = np.ceil(amount* image.size * (1. - s_vs_p))
coords = [np.random.randint(0, i - 1, int(num_pepper)) for i in image.shape]
out[coords] = 0
return out
elif noise_typ == "poisson":
vals = len(np.unique(image))
vals = 2 ** np.ceil(np.log2(vals))
noisy = np.random.poisson(image * vals) / float(vals)
return noisy
elif noise_typ =="speckle":
row,col,ch = image.shape
gauss = np.random.randn(row,col,ch)
gauss = gauss.reshape(row,col,ch)
noisy = image + image * gauss
return noisy
dst = noisy("s&p",saber)
plt.subplot(121)
plt.title("add noise")
plt.axis("off")
plt.imshow(dst)
plt.subplot(122)
dst = cv2.cvtColor(dst,cv2.COLOR_RGB2GRAY)
plt.title("Robert Process")
plt.axis("off")
dst = RobertsAlogrithm(dst)
plt.imshow(dst,cmap="binary")
plt.show()
这里我们可以看出Robert算子对于噪声过于敏感,在加了噪声后,人的边缘变得非常不清晰
Prewitt算子
Prewitt算子是一种\(3\times3\)模板的算子,它有两种形式,分别表示水平和垂直的梯度.
垂直梯度
-1 & -1 & -1 \\
0 & 0 & 0 \\
1 & 1 & 1 \\
\end{bmatrix}
\]
水平梯度
-1 & 0 & 1 \\
-1 & 0 & 1 \\
-1 & 0 & 1 \\
\end{bmatrix}
\]
对于如图的矩阵起始值
Z_1 & Z_2 & Z_3 \\
Z_4 & Z_5 & Z_6 \\
Z_7 & Z_8 & Z_9 \\
\end{bmatrix}
\]
就是以下两个式子
\]
\]
1与-1更换位置对结果并不会产生影响
def PreWittOperator(roi,operator_type):
if operator_type == "horizontal":
prewitt_operator = np.array([[-1,-1,-1],[0,0,0],[1,1,1]])
elif operator_type == "vertical":
prewitt_operator = np.array([[-1,0,1],[-1,0,1],[-1,0,1]])
else:
raise("type Error")
result = np.abs(np.sum(roi*prewitt_operator))
return result
def PreWittAlogrithm(image,operator_type):
new_image = np.zeros(image.shape)
image = cv2.copyMakeBorder(image,1,1,1,1,cv2.BORDER_DEFAULT)
for i in range(1,image.shape[0]-1):
for j in range(1,image.shape[1]-1):
new_image[i-1,j-1] = PreWittOperator(image[i-1:i+2,j-1:j+2],operator_type)
new_image = new_image*(255/np.max(image))
return new_image.astype(np.uint8)
在PreWitt算子执行中要考虑大于255的情况,使用下面代码
new_image = new_image*(255/np.max(image))
将其放缩到0-255之间,并使用astype将其转换到uint8类型
plt.subplot(121)
plt.title("horizontal")
plt.imshow(PreWittAlogrithm(gray_saber,"horizontal"),cmap="binary")
plt.axis("off")
plt.subplot(122)
plt.title("vertical")
plt.imshow(PreWittAlogrithm(gray_saber,"vertical"),cmap="binary")
plt.axis("off")
plt.show()
现在我们看一下Prewitt对噪声的敏感性
dst = noisy("s&p",saber)
plt.subplot(131)
plt.title("add noise")
plt.axis("off")
plt.imshow(dst)
plt.subplot(132)
plt.title("Prewitt Process horizontal")
plt.axis("off")
plt.imshow(PreWittAlogrithm(gray_saber,"horizontal"),cmap="binary")
plt.subplot(133)
plt.title("Prewitt Process vertical")
plt.axis("off")
plt.imshow(PreWittAlogrithm(gray_saber,"vertical"),cmap="binary")
plt.show()
选择水平梯度或垂直梯度从上图可以看出对于边缘的影响还是相当大的.
Sobel算子
Sobel算子根据像素点上下、左右邻点灰度加权差,在边缘处达到极值这一现象检测边缘。对噪声具有平滑作用,提供较为精确的边缘方向信息,边缘定位精度不够高。当对精度要求不是很高时,是一种较为常用的边缘检测方法。相比Prewitt他周边像素对于判断边缘的贡献是不同的.
垂直梯度
-1 & -2 & -1 \\
0 & 0 & 0 \\
1 & 2 & 1 \\
\end{bmatrix}
\]
水平梯度
-1 & 0 & 1 \\
-2 & 0 & 2 \\
-1 & 0 & 1 \\
\end{bmatrix}
\]
对上面的代码稍微修改算子即可
def SobelOperator(roi,operator_type):
if operator_type == "horizontal":
sobel_operator = np.array([[-1,-2,-1],[0,0,0],[1,2,1]])
elif operator_type == "vertical":
sobel_operator = np.array([[-1,0,1],[-2,0,2],[-1,0,1]])
else:
raise("type Error")
result = np.abs(np.sum(roi*sobel_operator))
return result
def SobelAlogrithm(image,operator_type):
new_image = np.zeros(image.shape)
image = cv2.copyMakeBorder(image,1,1,1,1,cv2.BORDER_DEFAULT)
for i in range(1,image.shape[0]-1):
for j in range(1,image.shape[1]-1):
new_image[i-1,j-1] = SobelOperator(image[i-1:i+2,j-1:j+2],operator_type)
new_image = new_image*(255/np.max(image))
return new_image.astype(np.uint8)
plt.subplot(121)
plt.title("horizontal")
plt.imshow(SobelAlogrithm(gray_saber,"horizontal"),cmap="binary")
plt.axis("off")
plt.subplot(122)
plt.title("vertical")
plt.imshow(SobelAlogrithm(gray_saber,"vertical"),cmap="binary")
plt.axis("off")
plt.show()
无论是Sobel还是Prewitt其实是求了单个方向颜色变化的极值
在某个方向上颜色的变化如图(此图来源于opencv官方文档)
对其求导可以得到这样一条曲线
由此我们可以得知边缘可以通过定位梯度值大于邻域的相素的方法找到(或者推广到大于一个阀值).
而Sobel就是利用了这一定理
Laplace算子
无论是Sobel还是Prewitt都是求单方向上颜色变化的一阶导(Sobel相比Prewitt增加了权重),那么如何才能反映空间上的颜色变化的一个极值呢?
我们试着在对边缘求一次导
在一阶导数的极值位置,二阶导数为0。所以我们也可以用这个特点来作为检测图像边缘的方法。 但是, 二阶导数的0值不仅仅出现在边缘(它们也可能出现在无意义的位置),但是我们可以过滤掉这些点。
在实际情况中我们不可能用0作为判断条件,而是设定一个阈值,这就引出来Laplace算子
\begin{bmatrix}
0 & 1 & 0 \
1 & -4 & 1 \
0 & 1 & 0 \
\end{bmatrix}
\begin{bmatrix}
1 & 1 & 1 \
1 & -8 & 1 \
1 & 1 & 1 \
\end{bmatrix}
def LaplaceOperator(roi,operator_type):
if operator_type == "fourfields":
laplace_operator = np.array([[0,1,0],[1,-4,1],[0,1,0]])
elif operator_type == "eightfields":
laplace_operator = np.array([[1,1,1],[1,-8,1],[1,1,1]])
else:
raise("type Error")
result = np.abs(np.sum(roi*laplace_operator))
return result
def LaplaceAlogrithm(image,operator_type):
new_image = np.zeros(image.shape)
image = cv2.copyMakeBorder(image,1,1,1,1,cv2.BORDER_DEFAULT)
for i in range(1,image.shape[0]-1):
for j in range(1,image.shape[1]-1):
new_image[i-1,j-1] = LaplaceOperator(image[i-1:i+2,j-1:j+2],operator_type)
new_image = new_image*(255/np.max(image))
return new_image.astype(np.uint8)
plt.subplot(121)
plt.title("fourfields")
plt.imshow(LaplaceAlogrithm(gray_saber,"fourfields"),cmap="binary")
plt.axis("off")
plt.subplot(122)
plt.title("eightfields")
plt.imshow(LaplaceAlogrithm(gray_saber,"eightfields"),cmap="binary")
plt.axis("off")
plt.show()
上图为比较取值不同的laplace算子实现的区别.
Canny算子
Canny边缘检测算子是一种多级检测算法。1986年由John F. Canny提出,同时提出了边缘检测的三大准则:
- 低错误率的边缘检测:检测算法应该精确地找到图像中的尽可能多的边缘,尽可能的减少漏检和误检。
- 最优定位:检测的边缘点应该精确地定位于边缘的中心。
- 图像中的任意边缘应该只被标记一次,同时图像噪声不应产生伪边缘。
Canny边缘检测算法的实现较为复杂,主要分为以下步骤,
- 高斯模糊
- 计算梯度幅值和方向
- 非极大值 抑制
- 滞后阈值
高斯模糊
在前面的章节中已经实现过均值模糊,模糊在图像处理中经常用来去除一些无关的细节.
均值模糊引出了另一个思考,周边每个像素点的权重设为一样是否合适?
(周边像素点的权重是很多算法的区别,比如Sobel和Prewitt)
因此概率中非常熟悉的正态分布函数便被引入.正太分布函数的密度函数就是高斯函数,使用高斯函数来给周边像素点分配权重.
更详细的可以参考高斯模糊的算法
在opencv的官方文档中它介绍了这样一个\(5\times5\)的高斯算子
相比直接的使用密度函数来分配权重,Opencv介绍的高斯算子是各像素点乘以一个大于1的比重,最后乘以一个\(1 \frac {159}\),其实就是归一化的密度函数.这有效避免了计算机计算的精度问题
我们在此处直接使用\(3\times3\)的算子
我们将其归一化然后代入上面的代码即可
def GaussianOperator(roi):
GaussianKernel = np.array([[1,2,1],[2,4,2],[1,2,1]])
result = np.sum(roi*GaussianKernel/16)
return result
def GaussianSmooth(image):
new_image = np.zeros(image.shape)
image = cv2.copyMakeBorder(image,1,1,1,1,cv2.BORDER_DEFAULT)
for i in range(1,image.shape[0]-1):
for j in range(1,image.shape[1]-1):
new_image[i-1,j-1] =GaussianOperator(image[i-1:i+2,j-1:j+2])
return new_image.astype(np.uint8)
smooth_saber = GaussianSmooth(gray_saber)
plt.subplot(121)
plt.title("Origin Image")
plt.axis("off")
plt.imshow(gray_saber,cmap="gray")
plt.subplot(122)
plt.title("GaussianSmooth Image")
plt.axis("off")
plt.imshow(smooth_saber,cmap="gray")
plt.show()
计算梯度幅值和方向
我们知道边缘实质是颜色变换的极值,可能出现在各个方向上,因此能否在计算时考虑多个方向的梯度,是算法效果优异的体现之一
在之前的Sobel等算子中我们将水平和垂直方向是分开考虑的.在Canny算子中利用下面公式将多个方向都考虑到
Gx = SobelAlogrithm(smooth_saber,"horizontal")
Gy = SobelAlogrithm(smooth_saber,"vertical")
G = np.sqrt(np.square(Gx.astype(np.float64))+np.square(Gy.astype(np.float64)))
cita = np.arctan2(Gy.astype(np.float64),Gx.astype(np.float64))
cita求出了图像每一个点的梯度,在之后的非极大值抑制中会有作用
plt.imshow(G.astype(np.uint8),cmap="gray")
plt.axis("off")
plt.show()
我们发现对于图像使用sobel算子使得获得的边缘较粗,下一步我们将使图像的线条变细
非最大值抑制
在获得梯度的方向和大小之后,应该对整幅图想做一个扫描,出去那些非边界上的点。对每一个像素进行检查,看这个点的梯度是不是周围具有相同梯度方向的点中最大的。
(图像来源于opencv官方文档)
非极大值的算法实现非常简单
- 比较当前点的梯度强度和正负梯度方向点的梯度强度。
- 如果当前点的梯度强度和同方向的其他点的梯度强度相比较是最大,保留其值。否则抑制,即设为0。比如当前点的方向指向正上方90°方向,那它需要和垂直方向,它的正上方和正下方的像素比较。
def NonmaximumSuppression(image,cita):
keep = np.zeros(cita.shape)
cita = np.abs(cv2.copyMakeBorder(cita,1,1,1,1,cv2.BORDER_DEFAULT))
for i in range(1,cita.shape[0]-1):
for j in range(1,cita.shape[1]-1):
if cita[i][j]>cita[i-1][j] and cita[i][j]>cita[i+1][j]:
keep[i-1][j-1] = 1
elif cita[i][j]>cita[i][j+1] and cita[i][j]>cita[i][j-1]:
keep[i-1][j-1] = 1
elif cita[i][j]>cita[i+1][j+1] and cita[i][j]>cita[i-1][j-1]:
keep[i-1][j-1] = 1
elif cita[i][j]>cita[i-1][j+1] and cita[i][j]>cita[i+1][j-1]:
keep[i-1][j-1] = 1
else:
keep[i-1][j-1] = 0
return keep*image
这里的代码我们先创建了一个keep矩阵,凡是在cita中梯度大于周边的点设为1,否则设为0,然后与原图相乘.(这是我想得比较巧妙的一个方法)
nms_image = NonmaximumSuppression(G,cita)
nms_image = (nms_image*(255/np.max(nms_image))).astype(np.uint8)
plt.imshow(nms_image,cmap="gray")
plt.axis("off")
plt.show()
在上面的极大值抑制中我们使边缘的线变得非常细,但是仍然有相当多的小点,得使用一种算法将他们去除
滞后阈值
滞后阈值的算法如下
Canny 使用了滞后阈值,滞后阈值需要两个阈值(高阈值和低阈值):
- 如果某一像素位置的幅值超过 高 阈值, 该像素被保留为边缘像素。
- 如果某一像素位置的幅值小于 低 阈值, 该像素被排除。
- 如果某一像素位置的幅值在两个阈值之间,该像素仅仅在连接到一个高于 高 阈值的像素时被保留。
在opencv官方文档中推荐我们用2:1或3:1的高低阈值.
我们使用3:1的阈值来进行滞后阈值算法.
滞后阈值的算法可以分成两步首先使用最大阈值,最小阈值排除掉部分点.
MAXThreshold = np.max(nms_image)/4*3
MINThreshold = np.max(nms_image)/4
usemap = np.zeros(nms_image.shape)
high_list = []
for i in range(nms_image.shape[0]):
for j in range(nms_image.shape[1]):
if nms_image[i,j]>MAXThreshold:
high_list.append((i,j))
direct = [(0,1),(1,1),(-1,1),(-1,-1),(1,-1),(1,0),(-1,0),(0,-1)]
def DFS(stepmap,start):
route = [start]
while route:
now = route.pop()
if usemap[now] == 1:
break
usemap[now] = 1
for dic in direct:
next_coodinate = (now[0]+dic[0],now[1]+dic[1])
if not usemap[next_coodinate] and nms_image[next_coodinate]>MINThreshold \
and next_coodinate[0]<stepmap.shape[0]-1 and next_coodinate[0]>=0 \
and next_coodinate[1]<stepmap.shape[1]-1 and next_coodinate[1]>=0:
route.append(next_coodinate)
for i in high_list:
DFS(nms_image,i)
plt.imshow(nms_image*usemap,cmap="gray")
plt.axis("off")
plt.show()
可以发现这次的效果并不好,这是由于最大最小阈值设定不是很好导致的,我们将上面代码整理,将最大阈值,最小阈值改为可变参数,调参来优化效果.
Canny边缘检测代码
def CannyAlogrithm(image,MINThreshold,MAXThreshold):
image = GaussianSmooth(image)
Gx = SobelAlogrithm(image,"horizontal")
Gy = SobelAlogrithm(image,"vertical")
G = np.sqrt(np.square(Gx.astype(np.float64))+np.square(Gy.astype(np.float64)))
G = G*(255/np.max(G)).astype(np.uint8)
cita = np.arctan2(Gy.astype(np.float64),Gx.astype(np.float64))
nms_image = NonmaximumSuppression(G,cita)
nms_image = (nms_image*(255/np.max(nms_image))).astype(np.uint8)
usemap = np.zeros(nms_image.shape)
high_list = []
for i in range(nms_image.shape[0]):
for j in range(nms_image.shape[1]):
if nms_image[i,j]>MAXThreshold:
high_list.append((i,j))
direct = [(0,1),(1,1),(-1,1),(-1,-1),(1,-1),(1,0),(-1,0),(0,-1)]
def DFS(stepmap,start):
route = [start]
while route:
now = route.pop()
if usemap[now] == 1:
break
usemap[now] = 1
for dic in direct:
next_coodinate = (now[0]+dic[0],now[1]+dic[1])
if next_coodinate[0]<stepmap.shape[0]-1 and next_coodinate[0]>=0 \
and next_coodinate[1]<stepmap.shape[1]-1 and next_coodinate[1]>=0 \
and not usemap[next_coodinate] and nms_image[next_coodinate]>MINThreshold:
route.append(next_coodinate)
for i in high_list:
DFS(nms_image,i)
return nms_image*usemap
我们试着调整阈值,来对其他图片进行检测
通过调整阈值可以减少毛点的存在,控制线的清晰
(PS:在使用canny过程中为了减少python运算的时间,我将图片进行了缩放,这也导致了效果不够明显,canny边缘检测在高分辨率情况下效果会更好)
参考资料
Opencv文档
stackoverflow
Numpy设计的是如此出色,以至于在实现算法时我能够忽略如此之多无关的细节
python数字图像处理(三)边缘检测常用算子的更多相关文章
- python skimage图像处理(三)
python skimage图像处理(三) This blog is from: https://www.jianshu.com/p/7693222523c0 霍夫线变换 在图片处理中,霍夫变换主要 ...
- python数字图像处理(17):边缘与轮廓
在前面的python数字图像处理(10):图像简单滤波 中,我们已经讲解了很多算子用来检测边缘,其中用得最多的canny算子边缘检测. 本篇我们讲解一些其它方法来检测轮廓. 1.查找轮廓(find_c ...
- 「转」python数字图像处理(18):高级形态学处理
python数字图像处理(18):高级形态学处理 形态学处理,除了最基本的膨胀.腐蚀.开/闭运算.黑/白帽处理外,还有一些更高级的运用,如凸包,连通区域标记,删除小块区域等. 1.凸包 凸包是指一 ...
- python数字图像处理(1):环境安装与配置
一提到数字图像处理编程,可能大多数人就会想到matlab,但matlab也有自身的缺点: 1.不开源,价格贵 2.软件容量大.一般3G以上,高版本甚至达5G以上. 3.只能做研究,不易转化成软件. 因 ...
- 初始----python数字图像处理--:环境安装与配置
一提到数字图像处理编程,可能大多数人就会想到matlab,但matlab也有自身的缺点: 1.不开源,价格贵 2.软件容量大.一般3G以上,高版本甚至达5G以上. 3.只能做研究,不易转化成软件. 因 ...
- python数字图像处理(二)关键镜头检测
镜头边界检测技术简述 介绍 作为视频最基本的单元帧(Frame),它的本质其实就是图片,一系列帧通过某种顺序组成在一起就构成了视频.镜头边界是视频相邻两帧出现了某种意义的变化,即镜头边界反映了视频内容 ...
- python数字图像处理(5):图像的绘制
实际上前面我们就已经用到了图像的绘制,如: io.imshow(img) 这一行代码的实质是利用matplotlib包对图片进行绘制,绘制成功后,返回一个matplotlib类型的数据.因此,我们也可 ...
- python数字图像处理(9):直方图与均衡化
在图像处理中,直方图是非常重要,也是非常有用的一个处理要素. 在skimage库中对直方图的处理,是放在exposure这个模块中. 1.计算直方图 函数:skimage.exposure.histo ...
- python数字图像处理(15):霍夫线变换
在图片处理中,霍夫变换主要是用来检测图片中的几何形状,包括直线.圆.椭圆等. 在skimage中,霍夫变换是放在tranform模块内,本篇主要讲解霍夫线变换. 对于平面中的一条直线,在笛卡尔坐标系中 ...
随机推荐
- kwargs - Key words arguments in python function
This is a tutorial of how to use *args and **kwargs For defining the default value of arguments that ...
- Delphi GridPanel Percent百分比设置
可能很多人都有这个困扰,为什么每次设置一个百分比后,值都会改变,只有设置成absolute才会正常,经摸索发现,是因为精度引起,设置percent的时候,需要将精确到多个小数位.如要有3列,需要设置 ...
- 6,Stack
一,Stack简介 Stack是栈.它的特性是:先进后出(FILO, First In Last Out). java工具包中的Stack是继承于Vector(矢量队列)的,由于Vector是通过数组 ...
- ReactNative的学习笔记
一.安装nodejs 查看是否安装:npm -v 二.安装react-native命令工具 npm install -g react-native-cli 三.查看 react-native --he ...
- ThreadPoolExecutor实现异步多线程
import time from concurrent.futures import ThreadPoolExecutor executor = ThreadPoolExecutor(max_work ...
- 轮询,WebSocket和P2P--记一次QQ交谈IM
问题:
- List of yellow pages
List of yellow pages From Wikipedia, the free encyclopedia [hide]This article has multiple issues. ...
- leetcode 125. 验证回文串(python)
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写. 说明:本题中,我们将空字符串定义为有效的回文串. 示例 1: 输入: "A man, a plan, a c ...
- firefox SSL_ERROR_RX_RECORD_TOO_LONG burpsuit 报错 解决方案
禁用TLS 1.3 .在Firefox的地址栏中输入“ about:config ”. .在搜索条目“tls.version.max ”.将值从4更改为3. .值4所表示TLS 1.3,3个代表TLS ...
- JSP中四种属性保存范围(2)
1.session <%@ page language="java" contentType="text/html" pageEncoding=" ...