本文文件仓库 本文所用到的所有内容均保存在该仓库中:https://github.com/chmoe/FaceRecognize_Superpixel_Colorblock
运行环境 项目 版本 Device MacMini(2020) M1 OS macOS Big [email protected] RAM 16G SSD 1T Python 3.8.6 based conda IDE PyCharm 2021.2.2(Community Edition)
面部识别 具体要求 2021年9月24日,教授为我布置了研究生的第一个课题,目的是测试我的编程能力的样子。内容是使用一下四种面容检测器框出人脸。
Haar特徴量 + Cascade識別器 HOG特徴量 + SVM識別器 CNN MTCNN 参考链接:https://iatom.hatenablog.com/entry/2020/11/01/152307
实现代码 Haar特徴量 + Cascade識別器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import cv2 import copycascade_fn = "./haarcascade_frontalface_alt.xml" face_cascade = cv2.CascadeClassifier(cascade_fn) for i in range (37 ): img = cv2.imread(r'./picture/' + str (i) + '.jpg' ) img = cv2.resize(img, dsize=(480 , 640 )) face_frame = copy.deepcopy(img) gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray_img = cv2.equalizeHist(gray_img) dets = face_cascade.detectMultiScale(gray_img, scaleFactor=1.3 , minNeighbors=3 , minSize=(30 , 30 ), flags=cv2.CASCADE_SCALE_IMAGE) for (x, y, w, h) in dets: face_image = face_frame[y:y + h, x:x + w] cv2.putText(img, "Haar" , (x, y - 4 ), cv2.FONT_HERSHEY_SIMPLEX, 0.5 , (0 , 255 , 0 ), 1 , cv2.LINE_AA) cv2.rectangle(img, (x, y), (x + w, y + h), (0 , 255 , 0 ), 2 ) cv2.imwrite('./result/haar_face/' + str (i) + '.png' , img)
HOG特徴量 + SVM識別器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import cv2 import dlib import copyprint (dlib.__file__)detector = dlib.get_frontal_face_detector() for i in range (37 ): img = cv2.imread(r'./picture/' + str (i) + '.jpg' ) img = cv2.resize(img, dsize=(480 , 640 )) face_frame = copy.deepcopy(img) dets = detector(img, 1 ) for k, d in enumerate (dets): face_image = face_frame[d.top():d.bottom(), d.left():d.right()] cv2.putText(img, "Dlib" , (int (d.left()), int (d.top()) - 4 ), cv2.FONT_HERSHEY_SIMPLEX, 0.5 , (0 , 255 , 0 ), 1 , cv2.LINE_AA) cv2.rectangle(img, (int (d.left()), int (d.top())), (int (d.right()), int (d.bottom())), (0 , 255 , 0 ), 2 ) cv2.imwrite('./result/Dlib_HOG_CSM/' + str (i) + '.png' , img)
CNN 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import dlibimport cv2import copycnn_fn = './mmod_human_face_detector.dat' cnn_face_detector = dlib.cnn_face_detection_model_v1(cnn_fn) for i in range (37 ): img = cv2.imread(r'./picture/' + str (i) + '.jpg' ) img = cv2.resize(img, dsize=(480 , 640 )) face_frame = copy.deepcopy(img) dets = cnn_face_detector(img, 1 ) for face in dets: face_image = face_frame[face.rect.top():face.rect.bottom(), face.rect.left():face.rect.right()] cv2.putText(img, "CNN" , (int (face.rect.left()), int (face.rect.top()) - 4 ), cv2.FONT_HERSHEY_SIMPLEX, 0.5 , (0 , 255 , 0 ), 1 , cv2.LINE_AA) cv2.rectangle(img, (face.rect.left(), face.rect.top()), (face.rect.right(), face.rect.bottom()), (0 , 255 , 0 ), 2 ) cv2.imwrite('./result/CNN/' + str (i) + '.png' , img)
MTCNN 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import cv2from mtcnn import MTCNN import copydetector = MTCNN() for i in range (37 ): img = cv2.imread(r'./picture/' + str (i) + '.jpg' ) img = cv2.resize(img, dsize=(480 , 640 )) face_frame = copy.deepcopy(img) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) dets = detector.detect_faces(img_rgb) for face in dets: box_x, box_y, box_w, box_h = face['box' ] face_image = face_frame[box_y:box_y + box_h, box_x:box_x + box_w] cv2.putText(face_frame, "MTCNN" , (box_x, box_y - 4 ), cv2.FONT_HERSHEY_SIMPLEX, 0.5 , (0 , 255 , 0 ), 1 , cv2.LINE_AA) cv2.rectangle(face_frame, (box_x, box_y), (box_x + box_w, box_y + box_h), (0 , 255 , 0 ), 2 ) for key, value in face['keypoints' ].items(): cv2.circle(face_frame, value, 1 , (0 , 0 , 255 ), -1 ) cv2.imwrite('./result/MTCNN/' + str (i) + '.png' , face_frame)
总结 上述四个方式的实现并不需要过多的代码,仅仅是使用网路上下载的意境训练好的人脸识别的模型进行应用一下即可。
超像素处理 具体要求 2021年9月27日,教授布置了第二个研究生课题,内容是使用面部识别 中第四种方式(MTCNN)所得到的人脸,将框处的人脸部分使用SLIC(Simple Linear Iterative Clustering,简单的线性迭代聚类)进行超像素化处理,并且将结果输出。
参考链接:https://techblog.nhn-techorus.com/archives/7793
解析及想法 想法 看过众多的解释之后,在这里我想写下自己的理解。
所谓超像素,并非从名字的理解上,将图片的像素数进行增加,而是某种意义上降低图片的像素数目。
这里说的某种意义上是因为其并非真正降低了图片所存在的像素数目,只是将相同颜色的像素数理解成为同一像素。例如经过超像素化后的图片(如下图),其输出结果分辨率为192 × 226 192 \times 226 192 × 226 ,但是在肉眼看上去,其色块只有6 × 7 6\times7 6 × 7 (图片中每一个色块中心的点为颜色的定位点,在这里需要忽略考虑)。
所以说这个处理为什么要叫做超像素(Superpixel),而不能够叫做减像素(Fallpixel)呢?
理解了这一点,那么接下来就可以继续思考了。
根据网路查阅资料显示,使用该方法首先需要将图片读取为Lab
的色彩表示方式。至于为什么如此,看了网络上列举的很多理由,诸如Lab
相较于RGB
更加接近于人类生理视觉等各种原因,我还是无法理解其原因。不过作为初学者,本项目的重点是学会如何使用,而非探求其太过于细节的部分,因此这一点就让我直接踩在巨人的肩膀上,不过多过问好了。
具体步骤 为了实现超像素(我真的不想使用这个名字),根据上文分析的原理,可以分为下述几个步骤进行实现。
均匀播种 查询周边 找到同类 交给时间 下面对此进行一一介绍
均匀播种 首先为了实现超像素化,需要在图片中找到需要划分个数
的颜色点作为超像素的中心点。但是,作为计算机,一个笨笨的家伙,她看图的方式和人类看图的方式是不同的,是没有办法从图片整体下手的,只能够一个像素点一个像素点的进行检查(这里不考虑特征提取),所以是没有办法直觉判断应该选择哪个像素作为中心点的(当然按照数组迷宫的方式进行计算的话也是可以的,但是那样只会增加无形的计算量,所以不采取这种方式)。
因此,干脆直接让计算机平均的将图片划分为需要划分个数
个块,并且将每个块的中心点作为色块的中心点。
具体实现方式,就是在图像内均匀分配需要划分个数
个像素点作为种子点。
这里假设需要划分个数
=K
,图片的总像素值为N
,那么每个超像素的大小便为N/K
,这里的每个超像素应近似为正方形,因此每个种子相邻的距离(步长)可以近似为S = N K S=\sqrt{\frac{N}{K}} S = K N 。
其具体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 def init_clusters (self ): """ 初始化超像素 :return: self """ h = self.S / 2 w = self.S / 2 while h < self.image_height: while w < self.image_width: self.clusters.append(self.make_cluster(h, w)) w += self.S w = self.S / 2 h += self.S
查询周边 经过上一个步骤的处理,我们成功的在图片上放置了K
个超像素点(种子)。
正常情况下来说,我们会要求超像素的种子落在比较正常的地方,也就是说,不希望种子位于超像素的分界位置,更不希望种子所选的地方恰巧位于噪点上。
这里的噪点可以形象的想象为显示器中的坏点,或者可以理解为万花丛中一点绿,因为那一点点绿无法用于表示整个花丛中的颜色。
基于上述原理,我们需要对该种子周围的地方进行判别,以减小或防止上述两个问题的发生。
因此需要对该种子为中心的n*n
范围内的像素点计算梯度值,并且找到梯度值最小的点作为新的该区域的新的种子点,并将种子移动到该像素。
根据参考链接中的「图像梯度的基本原理」文章所述,图像的梯度相当于两个相邻像素点之间的差值
,因此就有如下的梯度计算方式了,其对应代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 def get_gradient (self, h, w ): """ 获取梯度 :param h: :param w: :return: """ if w + 1 >= self.image_width: w = self.image_width - 2 if h + 1 >= self.image_height: h = self.image_height - 2 gradient = self.data[h + 1 ][w + 1 ][0 ] - self.data[h][w][0 ] + \ self.data[h + 1 ][w + 1 ][1 ] - self.data[h][w][1 ] + \ self.data[h + 1 ][w + 1 ][2 ] - self.data[h][w][2 ] return gradient def move_clusters (self ): for cluster in self.clusters: cluster_gradient = self.get_gradient(cluster.h, cluster.w) for dh in range (-1 , 2 ): for dw in range (-1 , 2 ): _h = cluster.h + dh _w = cluster.w + dw new_gradient = self.get_gradient(_h, _w) if new_gradient < cluster_gradient: cluster.update(_h, _w, self.data[_h][_w][0 ], self.data[_h][_w][1 ], self.data[_h][_w][2 ]) cluster_gradient = new_gradient
找到同类 到了这一步,种子点已经找到了一个相对较好的位置了。
接下来要做的,是在每个种子点周围找到属于该种子点的像素,并且将该像素归于该种子点所在的超像素块中。
在前文,我们计算过每一个超像素之间的距离(步长)应为S
,但是我们并不能够确定当前超像素块所包含的像素都能够聚集在这大小为S*S
的范围内,因此这里将其搜索范围设定为2S*2S
。
在搜索的过程中,我们需要对每一个像素进行计算,找到与该像素距离最近的超像素并且将其划分到该超像素中。
注意:这里的距离需要计算空间距离和颜色距离。
如果只计算空间距离的话,最后划分的结果应该会为非常标准的正方形,但是在真实的图像中,不难见到类似于融化感觉的图片,这个时候划分就会出错。
在开始的时候,我们导入的图像颜色空间是Lab
的,因此在这里计算颜色距离时,我们也需要使用该方式进行计算。由于颜色是用三维数据表示的,因此在计算颜色距离的时候,可以考虑成计算三维空间坐标系中两个点的距离,即
d c ( o l o r ) = ( l j − l i ) 2 + ( a j − a i ) 2 + ( b j − b i ) 2 d_{c(olor)}=\sqrt{(l_j-l_i)^2+(a_j-a_i)^2+(b_j-b_i)^2} d c ( o l or ) = ( l j − l i ) 2 + ( a j − a i ) 2 + ( b j − b i ) 2
在计算空间距离的时候,相当于计算二维坐标系下两个点之间的距离,即使用两点坐标公式即可计算
d s = ( x j − x i ) 2 + ( y j − y i ) 2 d_s=\sqrt{(x_j-x_i)^2+(y_j-y_i)^2} d s = ( x j − x i ) 2 + ( y j − y i ) 2
那么如何将这两个距离联合呢,在这里我并没有找到相关资料进行证明,根据其他讲述SLIC(参考内容中有链接)的文章所述,需要使用下述公式进行计算。
D ′ = ( d c N c ) 2 + d s N s D'=\sqrt{(\frac{d_c}{N_c})^2+\frac{d_s}{N_s}} D ′ = ( N c d c ) 2 + N s d s
并且N s N_s N s 表示距离空间中的最大值,也就是超像素之间的步长即S
,而N c N_c N c 所代表的是颜色距离的最大值,常会使用常数m
来代表,并且这个m
是可以自主修改的,范围是[1, 40],一般会取10。
因为在这里的检索范围是2S*2S
,所以同一个像素点可能会被多个超像素种子点进行检查,因此需要记录像素点到所有检索到其的超像素块的种子点的综合距离,并且找到最小的距离,将当前像素归于距离最小的种子点所在的超像素。
遍历所有像素点,并且按照距离分类好超像素块之后,需要对每一个超像素块中的种子点进行更新,将种子点移动到该超像素块的最中间的点。
其实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 def assignment (self ): for cluster in self.clusters: for h in range (cluster.h - 2 * self.S, cluster.h + 2 * self.S): if h < 0 or h >= self.image_height: continue for w in range (cluster.w - 2 * self.S, cluster.w + 2 * self.S): if w < 0 or w >= self.image_width: continue L, A, B = self.data[h][w] Dc = math.sqrt( math.pow (L - cluster.l, 2 ) + math.pow (A - cluster.a, 2 ) + math.pow (B - cluster.b, 2 )) Ds = math.sqrt( math.pow (h - cluster.h, 2 ) + math.pow (w - cluster.w, 2 )) D = math.sqrt(math.pow (Dc / self.M, 2 ) + math.pow (Ds / self.S, 2 )) if D < self.dis[h][w]: if (h, w) not in self.label: self.label[(h, w)] = cluster cluster.pixels.append((h, w)) else : self.label[(h, w)].pixels.remove((h, w)) self.label[(h, w)] = cluster cluster.pixels.append((h, w)) self.dis[h][w] = D def update_cluster (self ): """ 将超像素的lab值取得属于当前超像素的所有像素的最中间的值 :return: """ for cluster in self.clusters: sum_h = sum_w = number = 0 for p in cluster.pixels: sum_h += p[0 ] sum_w += p[1 ] number += 1 _h = int (sum_h / number) _w = int (sum_w / number) cluster.update(_h, _w, self.data[_h][_w][0 ], self.data[_h][_w][1 ], self.data[_h][_w][2 ])
交给时间 上述过程如果只进行一次的话,很有可能会不准确。
因此需要对上一个步骤重复进行(不断迭代),直到聚类的中心点不再发生变化为止。
对于这句突然的内容我实在是无法理解,因此去看了下原文
The L2 norm is used to compute a residual error E between the new cluster center locations and previous cluster center locations. The assignment and update steps can be repeated iteratively until the error converges, but we have found that 10 iterations suffices for most images, and report all results in this paper using this criteria.
L2范数用于计算新生成的超像素中心点和原来的中心点之间的残差,不断重复迭代assignment
和update
两个函数直到残差收敛。在论文中表示,经过实验发现对于大多数的图像来说,迭代10次都能够得到比较理想的结果。
综上,需要设置一个循环,并且循环次数设定为10,不断重复调用上一个步骤中的两个函数,即assignment
和update
两个函数。
其代码实现如下
1 2 3 for _ in trange(10 ): self.assignment() self.update_cluster()
实现代码 MTCNN部分 超像素处理部分所需要使用的图片需要经过MTCNN方式的检测,并且和第三部分面部识别
的需求不同,因此需要对该部分代码进行修改,修改后的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import cv2from mtcnn import MTCNN import copydetector = MTCNN() for i in range (37 ): img = cv2.imread(r'./picture/' + str (i) + '.jpg' ) img = cv2.resize(img, dsize=(480 , 640 )) face_frame = copy.deepcopy(img) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) dets = detector.detect_faces(img_rgb) for face in dets: box_x, box_y, box_w, box_h = face['box' ] face_image = face_frame[box_y:box_y + box_h, box_x:box_x + box_w] cv2.imwrite('./result/' + str (i) + '.png' , face_image)
SLIC部分 该算法的伪代码如下图所示
代码解释
cluster[]: 用于保存为超像素位置 label{}: 用于保存每个像素对应的超像素 具体实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 import mathfrom skimage import io, colorimport numpy as npfrom tqdm import trangeclass Cluster (object ): cluster_index = 1 def __init__ (self, h, w, l=0 , a=0 , b=0 ): self.update(h, w, l, a, b) self.pixels = [] self.no = self.cluster_index Cluster.cluster_index += 1 def update (self, h, w, l, a, b ): self.h = h self.w = w self.l = l self.a = a self.b = b def __str__ (self ): return "{},{}:{} {} {} " .format (self.h, self.w, self.l, self.a, self.b) def __repr__ (self ): return self.__str__() class SLICProcessor (object ): @staticmethod def open_image (path ): """ 打开图片 Return: 高(row), 宽(col), 颜色([lab]) """ rgb = io.imread(path) lab_arr = color.rgb2lab(rgb) return lab_arr @staticmethod def save_lab_image (path, lab_arr ): """ 将图片从lab转换回rgb,然后保存到指定的path :param path: :param lab_arr: :return: """ rgb_arr = color.lab2rgb(lab_arr) io.imsave(path, rgb_arr) def make_cluster (self, h, w ): h = int (h) w = int (w) return Cluster(h, w, self.data[h][w][0 ], self.data[h][w][1 ], self.data[h][w][2 ]) def __init__ (self, filename, K, M ): self.K = K self.M = M self.data = self.open_image(filename) self.image_height = self.data.shape[0 ] self.image_width = self.data.shape[1 ] self.N = self.image_height * self.image_width self.S = int (math.sqrt(self.N / self.K)) self.clusters = [] self.label = {} self.dis = np.full((self.image_height, self.image_width), np.inf) def init_clusters (self ): """ 初始化超像素 :return: self """ h = self.S / 2 w = self.S / 2 while h < self.image_height: while w < self.image_width: self.clusters.append(self.make_cluster(h, w)) w += self.S w = self.S / 2 h += self.S def get_gradient (self, h, w ): """ 获取梯度 :param h: :param w: :return: """ if w + 1 >= self.image_width: w = self.image_width - 2 if h + 1 >= self.image_height: h = self.image_height - 2 gradient = self.data[h + 1 ][w + 1 ][0 ] - self.data[h][w][0 ] + \ self.data[h + 1 ][w + 1 ][1 ] - self.data[h][w][1 ] + \ self.data[h + 1 ][w + 1 ][2 ] - self.data[h][w][2 ] return gradient def move_clusters (self ): for cluster in self.clusters: cluster_gradient = self.get_gradient(cluster.h, cluster.w) for dh in range (-1 , 2 ): for dw in range (-1 , 2 ): _h = cluster.h + dh _w = cluster.w + dw new_gradient = self.get_gradient(_h, _w) if new_gradient < cluster_gradient: cluster.update(_h, _w, self.data[_h][_w][0 ], self.data[_h][_w][1 ], self.data[_h][_w][2 ]) cluster_gradient = new_gradient def assignment (self ): for cluster in self.clusters: for h in range (cluster.h - 2 * self.S, cluster.h + 2 * self.S): if h < 0 or h >= self.image_height: continue for w in range (cluster.w - 2 * self.S, cluster.w + 2 * self.S): if w < 0 or w >= self.image_width: continue L, A, B = self.data[h][w] Dc = math.sqrt( math.pow (L - cluster.l, 2 ) + math.pow (A - cluster.a, 2 ) + math.pow (B - cluster.b, 2 )) Ds = math.sqrt( math.pow (h - cluster.h, 2 ) + math.pow (w - cluster.w, 2 )) D = math.sqrt(math.pow (Dc / self.M, 2 ) + math.pow (Ds / self.S, 2 )) if D < self.dis[h][w]: if (h, w) not in self.label: self.label[(h, w)] = cluster cluster.pixels.append((h, w)) else : self.label[(h, w)].pixels.remove((h, w)) self.label[(h, w)] = cluster cluster.pixels.append((h, w)) self.dis[h][w] = D def update_cluster (self ): """ 将超像素的lab值取得属于当前超像素的所有像素的最中间的值 :return: """ for cluster in self.clusters: sum_h = sum_w = number = 0 for p in cluster.pixels: sum_h += p[0 ] sum_w += p[1 ] number += 1 _h = int (sum_h / number) _w = int (sum_w / number) cluster.update(_h, _w, self.data[_h][_w][0 ], self.data[_h][_w][1 ], self.data[_h][_w][2 ]) def save_current_image (self, name ): image_arr = np.copy(self.data) for cluster in self.clusters: for p in cluster.pixels: image_arr[p[0 ]][p[1 ]][0 ] = cluster.l image_arr[p[0 ]][p[1 ]][1 ] = cluster.a image_arr[p[0 ]][p[1 ]][2 ] = cluster.b image_arr[cluster.h][cluster.w][0 ] = 0 image_arr[cluster.h][cluster.w][1 ] = 0 image_arr[cluster.h][cluster.w][2 ] = 0 self.save_lab_image(name, image_arr) def iterate_10times (self, j ): self.init_clusters() self.move_clusters() for _ in trange(10 ): self.assignment() self.update_cluster() name = './SLIC_result/{}.png' .format (j) self.save_current_image(name) if __name__ == '__main__' : for i in range (37 ): print ("第{}/{}个正在运行" .format (i + 1 , 37 )) p = SLICProcessor('./result/{}.png' .format (i), 40 , 40 ) p.iterate_10times(i)
参考内容 SLIC超像素分割详解(一):简介: https://blog.csdn.net/electech6/article/details/45509779
超像素SLIC算法: https://www.jianshu.com/p/f2bc9dbbd9b2
SLIC算法分割超像素原理及Python实现: https://www.kawabangga.com/posts/1923
图像梯度的基本原理: https://blog.csdn.net/saltriver/article/details/78987096
ACHANTA, Radhakrishna, et al. SLIC superpixels compared to state-of-the-art superpixel methods. IEEE transactions on pattern analysis and machine intelligence, 2012, 34.11: 2274-2282.
色块提出 具体要求 2021年9月29日,教授布置了第三个课题,内容是使用超像素处理 超像素化后的图片,在其中找到每个小块中颜色≥95%的最小矩形框,并且将矩形块进行输出。
这里要求输出的举行色块是超像素化后每个小块单独进行输出,输出时,该超像素所对应的最小正方形区域显示图片最初始的状态,其余区域将其颜色通道设置为0,具体表现为黑色。
这次没有参考链接,由于中间隔着一个国庆,需要去处理一些非常麻烦的事情(具体详情日记篇 ),所以再次认真完成的时候中间已经过去了6天
解析及想法 解析 因为是需要使用超像素化处理后的图片,因此按照正常思路来说设想是需要使用处理好的图片作为输入进行处理。
但是因为考虑到对于图片遍历需要使用到大量的循环,有可能会产生不必要的系统开销,因此最初决定对于SLIC算法的代码进行修改,在生成SLIC的同时可以保存图像色块的信息,在产生超像素图像之后,可以进一步通过保存的色块信息进行划分。
因此在代码实现上我需要面临如下几个问题
识别出每一张超像素处理后的图片的每个色块 对于每一个色块进行精准定位 按照色块对图像进行划分 找到覆盖范围≥95%的最小矩形 不过在考量处理的数据量之后,我判断循环的速度要远远快与处理SLIC,因此决定重新写一段算法来满足本次的需求。
对于上述的问题1,可以采取的方式是将图像以像素级进行遍历,找到同样颜色(颜色的RGB值相同)且相邻的所有像素进行记录,并且打上相同的标记。
这样做的好处是不需要对于SLIC算法的代码进行重构,减少修改和多余的计算时长。
因此接下来需要考虑问题4的内容,即如何能够找到覆盖范围≥95%的矩形,且要求矩形面积最小。
在画出图思考之后,我突然想到了很久之前听过的滑动窗口这一概念,这一概念最早用于网络通信的流量控制中,但是后来也可以用于解决数组、字符串的子元素等问题。
虽然在大学的课程和实践中曾经多次听过这一名词,但是我却从来没有亲自用代码实现过 ,毕竟编程的精髓在于cmd + c和cmd + v 。
那么既然没有代码上的经验,需要做的便是考虑如何构思这样一个算法,说干就干,于是拿出了iPad开始画图慢慢分析。
于是画出的图就成了上面这种画风了(毕竟是草稿就不要吐槽我的字丑了)
大概描述一下就是首先找到一个最小的边长,然后使用这个边长框出一个正方形在图像上滑动,每次检测该正方形框住的待求颜色的像素个数,并用该像素个数除以该颜色所在色块的所有像素值,将得到的比率与给定的比率(95%)进行比较,若不满足则移动该正方形,若移动后仍不满足则正方形边长+1重复上述步骤。
具体步骤 识别记录色块 首先需要读取图片进入内存,这里使用的是OpenCV的imread
函数,因此需要转换为RGB的颜色空间。
由于需要记录图片每一个像素点的颜色信息,因此需要预先建立一个名为img_ndarry
的,大小为(image_height, image_width)的二维数组,此处使用numpy进行实现,因此使用的是np.full()
函数,同时使用'0000000'
进行填充,经过实测,这里预先填充的字符串长度决定了该数组的可以存放的字符串长度,因此为保存颜色的RGB的HEX信息,需要提前存入长度为7的字符串(6位颜色值外加一位’#')。
但是在写下这段话的时候我突然意识到其实#是可以不进行存储的,因为没有必要,并且还可以减小空间开销。不过既然程序已经写完,就这样不进行修改了,如果小可爱看到这里的话可以尝试删掉存入程序的#号。
由于图片读入并且转换后,颜色通道为RGB,而保存在数组中的颜色通道信息希望为HEX,因此需要使用如下的方程进行转换。
1 2 3 4 5 6 7 def rgb2hex (rgb ): hex_color = '#' for i in rgb: num = int (i) hex_color += str (hex (num))[-2 :].replace('x' , '0' ).upper() return hex_color
首先初始化数组,然后遍历每一个像素点,找到第一个没有标记的像素点,记录下当前像素点的颜色信息,并且将数组的对应位置记录下颜色的信息,接着在这个像素点周围开始寻找相同颜色的连通区域。
在寻找连通区域的过程中,由于其区域是不断扩大的,此时需要记录下连通区域的边界位置,即当前连通区域的上下左右的边界。
寻找边界的时候,可以使用寻找最大最小值的方式进行判定,比如将最下限设置为最高点,最上限设置为最低点,只要有新的上限大于原来的上限,则让新的上限成为上限並且保存。
决定好边界之后就要交给滑动窗口进行处理了。
处理完当前颜色后,继续处理下一个颜色。仍旧从头开始遍历每一个像素点,当当前像素点的颜色与上一个颜色不同,并且当前像素点对应位置的数组中也没有被填充颜色的时候,则代表当前像素点所对应的颜色及其连通区域尚未被遍历,因此重复寻找连通域,然后使用滑动窗口处理这一步骤。
滑动窗口方法 在使用滑动窗口方式处理时,面临的最大问题便是如何处理边界的问题。
面对边界问题,需要考虑的一共有如下的四种情况。
当方块的初始长度大边界快的长度该如何处理,如下图所示,该图像中的黄色区域占14像素,而根据d = ⌈ p i x e l n u m b e r ⌉ d=\lceil {\sqrt{pixel\ number}}\ \rceil d = ⌈ p i x e l n u mb er ⌉ 公式可以判断,该窗口的边长应为d = ⌈ 14 ⌉ = 4 d=\lceil {\sqrt{14}}\ \rceil = 4 d = ⌈ 14 ⌉ = 4
而该黄色区域的上下高度最大为3,因此可知框的窗口位置要大于其像素的连通区域,此时窗口不能够从黄色区域的最上方开始,而应该从黄色区域的最下方开始。
不过这样一来就涉及到需要比较繁杂的代码进行实现,而因为我比较懒惰,不想要进行思考而写出很多复杂的代码去解决这一问题。既然无法解决问题,就解决掉提出问题的人
,也就是解决掉我自己。
既然无法决定固定大小的窗口的的起始位置,那么就直接丢掉固定窗口这一概念好了。
首先检测区域的范围与窗口初始大小的关系,如果区域足够覆盖窗口初始大小(经主观判断,大部分情况属于能够正常覆盖),则使用窗口从区域的最左上角开始滑动,找到满足条件的区域则记录面积和窗口的上下左右四个边界,并且当窗口增长到足够大并且能够遍历所有的像素点之后停止,此时找到最小的窗口并且将其对应的四个界限返回。
此时即可回到主程序进行接下来的处理。
最后的处理 根据要求,最后输出的内容需要是检测出的方块框柱的原图部分,因此需要使用如下方法进行处理。
将上一步中获取到的边界值应用于一张原图,将改变接之外的部分的颜色值变换为RGB(0, 0, 0)
,方块之内的部分需要保留其原本的颜色。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def save_image (self, path, point ): """ 按照要求处理图片,然后进行保存 :param path: 图片保存路径 :param point: 上下左右的限定范围 :return: """ data_copy = self.source_file[point[0 ]:point[1 ], point[2 ]:point[3 ], :] for row in range (point[0 ], point[1 ]): for col in range (point[2 ], point[3 ]): if self.img_ndarray[row, col] == self.last_color: continue else : data_copy[row - point[0 ]][col - point[2 ]] = (0 , 0 , 0 ) cv2.imwrite(path, data_copy[:, :, (2 , 1 , 0 )])
如上述程序所述,首先需要根据边框画出原图的部分,然后根据img_ndarry
中保存的颜色值与当前颜色值进行比较,与当前颜色符合的则保持原样,与当前颜色不同的则设置为0。
实现代码 该程序的全部代码如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 import mathimport cv2import numpy as npimport osclass ColorProcessor (object ): def __init__ (self, file_path, source_file_path ): self.source_file = self.open_image(source_file_path) self.data = self.open_image(file_path) self.image_height = self.data.shape[0 ] self.image_width = self.data.shape[1 ] self.img_ndarray = np.full((self.image_height, self.image_width), '0000000' ) self.last_color = "#" self.color_set = set () self.most_left = self.image_width self.most_right = 0 self.most_top = self.image_height self.most_bottom = 0 self.color_count = 0 @staticmethod def open_image (path ): """ 打开图片 Return: 高(row), 宽(col), 颜色([rgb]) """ bgr = cv2.imread(path) rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) return rgb @staticmethod def rgb2hex (rgb ): hex_color = '#' for i in rgb: num = int (i) hex_color += str (hex (num))[-2 :].replace('x' , '0' ).upper() return hex_color def traversal (self, i ): """ 遍历数组 :return: """ counter = 0 for row in range (self.image_height): for col in range (self.image_width): if self.img_ndarray[row, col] != '0000000' : pass else : self.last_color = self.rgb2hex(self.data[row, col]) self.img_ndarray[row, col] = self.last_color self.get_connected(row, col) point = self.move_rectangle() print ("point: \n" , point) if point: path = "./Process_result1/{}_{}.png" .format (i, counter) print ("last_color: {}" .format (self.last_color)) self.save_image(path, point) counter += 1 def save_image (self, path, point ): """ 按照要求处理图片,然后进行保存 :param path: 图片保存路径 :param point: 上下左右的限定范围 :return: """ data_copy = self.source_file[point[0 ]:point[1 ], point[2 ]:point[3 ], :] for row in range (point[0 ], point[1 ]): for col in range (point[2 ], point[3 ]): if self.img_ndarray[row, col] == self.last_color: continue else : data_copy[row - point[0 ]][col - point[2 ]] = (0 , 0 , 0 ) cv2.imwrite(path, data_copy[:, :, (2 , 1 , 0 )]) def move_rectangle (self ): """ 使用矩形在范围内移动,并且逐渐扩大矩形以找到最大结果 :return: 返回 上下左右 """ min_sqa = math.ceil(self.color_count * 0.95 ) print ("最小像素数: " , min_sqa) min_rec = math.ceil(math.sqrt(min_sqa)) width = self.most_right - self.most_left height = self.most_bottom - self.most_top max_rec = max (width, height) count = 0 print ("mo_le: {}, mo_ri: {}, mo_to: {}, mo_bo: {}" .format (self.most_left, self.most_right, self.most_top, self.most_bottom)) print ("min_rec: {}, max_rec: {}" .format (min_rec, max_rec)) for d in range (min_rec, max_rec + 1 ): print ("width: {}, height: {}, d: {}" .format (width, height, d)) if d > width: for y in range (height - d): for row in range (self.most_top + y, self.most_top + y + d + 1 ): for col in range (self.most_left, self.most_right + 1 ): if self.img_ndarray[row, col] == self.last_color: count += 1 if count >= min_sqa: return [self.most_top + y, self.most_top + y + d, self.most_left if self.most_left + d <= self.image_width else self.most_right - d, self.most_left + d if self.most_left + d <= self.image_width else self.most_right] elif d > height: for x in range (width - d): for row in range (self.most_top, self.most_bottom + 1 ): for col in range (self.most_left + x, self.most_left + x + d + 1 ): if self.img_ndarray[row, col] == self.last_color: count += 1 if count >= min_sqa: return [self.most_top if self.most_top + d <= self.image_height else self.most_bottom - d, self.most_top + d if self.most_top + d <= self.image_height else self.most_bottom, self.most_left + x, self.most_left + x + d] else : for x in range (width - d + 1 ): for y in range (height - d + 1 ): for row in range (self.most_top + y, self.most_top + y + d + 1 ): for col in range (self.most_left + x, self.most_left + x + d + 1 ): if self.img_ndarray[row, col] == self.last_color: count += 1 print ("count: {}, min_sqa: {}" .format (count, min_sqa)) if count >= min_sqa: return [self.most_top + y, self.most_top + y + d, self.most_left + x, self.most_left + x + d] def get_connected (self, _row, _col ): """ 在图片中从当前点寻找连通域,并且在ndarray中标注颜色 :param _row: 坐标x :param _col: 坐标y :return: """ self.color_set = set () self.color_set.add((_row, _col)) self.most_left = self.image_width self.most_right = 0 self.most_top = self.image_height self.most_bottom = 0 self.color_count = 0 while 0 != len (self.color_set): [row, col] = self.color_set.pop() self.color_count += 1 if row - 1 >= 0 : self.set_color(row - 1 , col) if row + 1 <= self.image_height - 1 : self.set_color(row + 1 , col) if col - 1 >= 0 : self.set_color(row, col - 1 ) if col + 1 <= self.image_width - 1 : self.set_color(row, col + 1 ) def set_color (self, row, col ): if self.rgb2hex(self.data[row, col]) == self.last_color: if self.img_ndarray[row, col] != self.last_color: self.img_ndarray[row, col] = self.last_color self.color_set.add((row, col)) if row > self.most_bottom: self.most_bottom = row if row < self.most_top: self.most_top = row if col < self.most_left: self.most_left = col if col > self.most_right: self.most_right = col if __name__ == '__main__' : for i in range (37 ): print ("第{}/{}个正在运行" .format (i + 1 , 37 )) p = ColorProcessor('./SLIC_result/{}.png' .format (i), './result/{}.png' .format (i)) p.traversal(i)
参考内容 滑动窗口的概念: https://baike.baidu.com/item/滑动窗口
什么是「滑动窗口算法」(sliding window algorithm),有哪些应用场景?: https://www.zhihu.com/question/314669016
总结 以上便是第一次教授布置的演習課題的全部内容,虽然是分为三次进行布置的,但是综合起来我认为可以成为一个问题,因此就放在一篇文章中了。
本篇内容只是为了抹茶面对这一个课题的击破过程,如果能够为小可爱带来一丝丝灵感的话那真的是非常棒了呢。
另外:好久没有更新(水)技术性的文章了,我差点都忘记自己是计算机专业的了(笑)