""" 本文件存放一些自制的简单的图像处理函数 """ from PIL import Image import cv2 import numpy as np import math import warnings import csv import glob def cover_mask(image_path, mask_path, alpha=0.85, rate=0.1, if_save=True): """ 在图片右下角盖上水印 :param image_path: :param mask_path: 水印路径,以PNG方式读取 :param alpha: 不透明度,默认为0.85 :param rate: 水印比例,越小水印也越小,默认为0.1 :param if_save: 是否将裁剪后的图片保存,如果为True,则保存并返回新图路径,否则不保存,返回截取后的图片对象 :return: 新的图片路径 """ # 生成新的图片路径,我们默认图片后缀存在且必然包含“.” path_len = len(image_path) index = 0 for index in range(path_len - 1, -1, -1): if image_path[index] == ".": break if 3 >= path_len - index >= 6: raise TypeError("输入的图片格式有误!") new_path = image_path[0:index] + "_with_mask" + image_path[index:path_len] # 以png方式读取水印图 mask = Image.open(mask_path).convert('RGBA') mask_h, mask_w = mask.size # 以png的方式读取原图 im = Image.open(image_path).convert('RGBA') # 我采取的策略是,先拷贝一张原图im为base作为基底,然后在im上利用paste函数添加水印 # 此时的水印是完全不透明的,我需要利用blend函数内置参数alpha进行不透明度调整 base = im.copy() # layer = Image.new('RGBA', im.size, (0, 0, 0, )) # tmp = Image.new('RGBA', im.size, (0, 0, 0, 0)) h, w = im.size # 根据原图大小缩放水印图 mask = mask.resize((int(rate*math.sqrt(w*h*mask_h/mask_w)), int(rate*math.sqrt(w*h*mask_w/mask_h))), Image.ANTIALIAS) mh, mw = mask.size r, g, b, a = mask.split() im.paste(mask, (h-mh, w-mw), mask=a) # im.show() out = Image.blend(base, im, alpha=alpha).convert('RGB') # out = Image.alpha_composite(im, layer).convert('RGB') if if_save: out.save(new_path) return new_path else: return out def check_image(image) ->np.ndarray: """ 判断某一对象是否为图像/矩阵类型,最终返回图像/矩阵 """ if not isinstance(image, np.ndarray): image = cv2.imread(image, cv2.IMREAD_UNCHANGED) return image def get_box(image) -> list: """ 这是一个简单的扣图后图像定位函数,不考虑噪点影响 我们使用遍历的方法,碰到非透明点以后立即返回位置坐标 :param image:图像信息,可以是图片路径,也可以是已经读取后的图像 如果传入的是图片路径,我会首先通过读取图片、二值化,然后再进行图像处理 如果传入的是图像,直接处理,不会二值化 :return: 回传一个列表,分别是图像的上下(y)左右(x)自个值 """ image = check_image(image) height, width, _ = image.shape try: b, g, r, a = cv2.split(image) # 二值化处理 a = (a > 127).astype(np.int_) except ValueError: # 说明传入的是无透明图层的图像,直接返回图像尺寸 warnings.warn("你传入了一张非四通道格式的图片!") return [0, height, 0, width] flag1, flag2 = 0, 0 box = [0, 0, 0, 0] # 上下左右 # 采用两面夹击战术,使用flag1和2确定两面的裁剪程度 # 先得到上下 for i in range(height): for j in range(width): if flag1 == 0 and a[i][j] != 0: flag1 = 1 box[0] = i if flag2 == 0 and a[height - i -1][j] != 0: flag2 = 1 box[1] = height - i - 1 if flag2 * flag1 == 1: break # 再得到左右 flag1, flag2 = 0, 0 for j in range(width): for i in range(height): if flag1 == 0 and a[i][j] != 0: flag1 = 1 box[2] = j if flag2 == 0 and a[i][width - j - 1] != 0: flag2 = 1 box[3] = width - j - 1 if flag2 * flag1 == 1: break return box def filtering(img, f, x, y, x_max, y_max, x_min, y_min, area=0, noise_size=50) ->tuple: """ filtering将使用递归的方法得到一个连续图像(这个连续矩阵必须得是单通道的)的范围(坐标) :param img: 传入的矩阵 :param f: 和img相同尺寸的全零矩阵,用于标记递归递归过的点 :param x: 当前递归到的x轴坐标 :param y: 当前递归到的y轴坐标 :param x_max: 递归过程中x轴坐标的最大值 :param y_max: 递归过程中y轴坐标的最大值 :param x_min: 递归过程中x轴坐标的最小值 :param y_min: 递归过程中y轴坐标的最小值 :param area: 当前递归区域面积大小 :param noise_size: 最大递归区域面积大小,当area大于noise_size时,函数返回(0, 1) :return: 分两种情况,当area大于noise_size时,函数返回(0, 1),当area小于等于noise_size时,函数返回(box, 0) 其中box是连续图像的坐标和像素点面积(上下左右,面积) 理论上来讲,我们可以用这个函数递归出任一图像的形状和坐标,但是从计算机内存、计算速度上考虑,这并不是一个好的选择 所以这个函数一般用于判断和过滤噪点 """ dire_dir = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (1, -1), (-1, -1), (-1, 1)] height, width = img.shape f[x][y] = 1 for dire in dire_dir: delta_x, delta_y = dire tmp_x, tmp_y = (x + delta_x, y + delta_y) if height > tmp_x >= 0 and width > tmp_y >= 0: if img[tmp_x][tmp_y] != 0 and f[tmp_x][tmp_y] == 0: f[tmp_x][tmp_y] = 1 # cv2.imshow("test", f) # cv2.waitKey(3) area += 1 if area > noise_size: return 0, 1 else: x_max = tmp_x if tmp_x > x_max else x_max x_min = tmp_x if tmp_x < x_min else x_min y_max = tmp_y if tmp_y > y_max else y_max y_min = tmp_y if tmp_y < y_min else y_min box, flag = filtering(img, f, tmp_x, tmp_y, x_max, y_max, x_min, y_min, area=area, noise_size=noise_size) if flag == 1: return 0, 1 else: (x_max, x_min, y_max, y_min, area) = box return [x_min, x_max, y_min, y_max, area], 0 def get_box_pro(image: np.ndarray, model: int = 1, correction_factor=None, thresh: int = 127): """ 本函数能够实现输入一张四通道图像,返回图像中最大连续非透明面积的区域的矩形坐标 本函数将采用opencv内置函数来解析整个图像的mask,并提供一些参数,用于读取图像的位置信息 Args: image: 四通道矩阵图像 model: 返回值模式 correction_factor: 提供一些边缘扩张接口,输入格式为list或者int:[up, down, left, right]。 举个例子,假设我们希望剪切出的矩形框左边能够偏左1个像素,则输入[0, 0, 1, 0]; 如果希望右边偏右1个像素,则输入[0, 0, 0, 1] 如果输入为int,则默认只会对左右两边做拓展,比如输入2,则和[0, 0, 2, 2]是等效的 thresh: 二值化阈值,为了保持一些羽化效果,thresh必须要小 Returns: model为1时,将会返回切割出的矩形框的四个坐标点信息 model为2时,将会返回矩形框四边相距于原图四边的距离 """ # ------------ 数据格式规范部分 -------------- # # 输入必须为四通道 if correction_factor is None: correction_factor = [0, 0, 0, 0] if not isinstance(image, np.ndarray) or len(cv2.split(image)) != 4: raise TypeError("输入的图像必须为四通道np.ndarray类型矩阵!") # correction_factor规范化 if isinstance(correction_factor, int): correction_factor = [0, 0, correction_factor, correction_factor] elif not isinstance(correction_factor, list): raise TypeError("correction_factor 必须为int或者list类型!") # ------------ 数据格式规范完毕 -------------- # # 分离mask _, _, _, mask = cv2.split(image) # mask二值化处理 _, mask = cv2.threshold(mask, thresh=thresh, maxval=255, type=0) contours, hierarchy = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) temp = np.ones(image.shape, np.uint8)*255 cv2.drawContours(temp, contours, -1, (0, 0, 255), -1) contours_area = [] for cnt in contours: contours_area.append(cv2.contourArea(cnt)) idx = contours_area.index(max(contours_area)) x, y, w, h = cv2.boundingRect(contours[idx]) # 框出图像 # ------------ 开始输出数据 -------------- # height, width, _ = image.shape y_up = y - correction_factor[0] if y - correction_factor[0] >= 0 else 0 y_down = y + h + correction_factor[1] if y + h + correction_factor[1] < height else height - 1 x_left = x - correction_factor[2] if x - correction_factor[2] >= 0 else 0 x_right = x + w + correction_factor[3] if x + w + correction_factor[3] < width else width - 1 if model == 1: # model=1,将会返回切割出的矩形框的四个坐标点信息 return [y_up, y_down, x_left, x_right] elif model == 2: # model=2, 将会返回矩形框四边相距于原图四边的距离 return [y_up, height - y_down, x_left, width - x_right] else: raise EOFError("请选择正确的模式!") def cut(image_path:str, box:list, if_save=True): """ 根据box,裁剪对应的图片区域后保存 :param image_path: 原图路径 :param box: 坐标列表,上下左右 :param if_save:是否将裁剪后的图片保存,如果为True,则保存并返回新图路径,否则不保存,返回截取后的图片对象 :return: 新图路径或者是新图对象 """ index = 0 path_len = len(image_path) up, down, left, right = box image = cv2.imread(image_path, cv2.IMREAD_UNCHANGED) new_image = image[up: down, left: right] if if_save: for index in range(path_len - 1, -1, -1): if image_path[index] == ".": break if 3 >= path_len - index >= 6: raise TypeError("输入的图片格式有误!") new_path = image_path[0:index] + "_cut" + image_path[index:path_len] cv2.imwrite(new_path, new_image, [cv2.IMWRITE_PNG_COMPRESSION, 9]) return new_path else: return new_image def zoom_image_without_change_size(image:np.ndarray, zoom_rate, interpolation=cv2.INTER_NEAREST) ->np.ndarray: """ 在不改变原图大小的情况下,对图像进行放大,目前只支持从图像中心放大 :param image: 传入的图像对象 :param zoom_rate: 放大比例,单位为倍(初始为1倍) :param interpolation: 插值方式,与opencv的resize内置参数相对应,默认为最近邻插值 :return: 裁剪后的图像实例 """ height, width, _ = image.shape if zoom_rate < 1: # zoom_rate不能小于1 raise ValueError("zoom_rate不能小于1!") height_tmp = int(height * zoom_rate) width_tmp = int(width * zoom_rate) image_tmp = cv2.resize(image, (height_tmp, width_tmp), interpolation=interpolation) # 定位一下被裁剪的位置,实际上是裁剪框的左上角的点的坐标 delta_x = (width_tmp - width) // 2 # 横向 delta_y = (height_tmp - height) // 2 # 纵向 return image_tmp[delta_y : delta_y + height, delta_x : delta_x + width] def filedir2csv(scan_filedir, csv_filedir): file_list = glob.glob(scan_filedir+"/*") with open(csv_filedir, "w") as csv_file: writter = csv.writer(csv_file) for file_dir in file_list: writter.writerow([file_dir]) print("filedir2csv success!") def full_ties(image_pre:np.ndarray): height, width = image_pre.shape # 先膨胀 kernel = np.ones((5, 5), dtype=np.uint8) dilate = cv2.dilate(image_pre, kernel, 1) # cv2.imshow("dilate", dilate) def FillHole(image): # 复制 image 图像 im_floodFill = image.copy() # Mask 用于 floodFill,官方要求长宽+2 mask = np.zeros((height + 2, width + 2), np.uint8) seedPoint = (0, 0) # floodFill函数中的seedPoint对应像素必须是背景 is_break = False for i in range(im_floodFill.shape[0]): for j in range(im_floodFill.shape[1]): if (im_floodFill[i][j] == 0): seedPoint = (i, j) is_break = True break if (is_break): break # 得到im_floodFill 255填充非孔洞值 cv2.floodFill(im_floodFill, mask, seedPoint, 255) # cv2.imshow("tmp1", im_floodFill) # 得到im_floodFill的逆im_floodFill_inv im_floodFill_inv = cv2.bitwise_not(im_floodFill) # cv2.imshow("tmp2", im_floodFill_inv) # 把image、im_floodFill_inv这两幅图像结合起来得到前景 im_out = image | im_floodFill_inv return im_out # 洪流算法填充 image_floodFill = FillHole(dilate) # 填充图和原图合并 image_final = image_floodFill | image_pre # 再腐蚀 kernel = np.ones((5, 5), np.uint8) erosion= cv2.erode(image_final, kernel, iterations=6) # cv2.imshow("erosion", erosion) # 添加高斯模糊 blur = cv2.GaussianBlur(erosion, (5, 5), 2.5) # cv2.imshow("blur", blur) # image_final = merge_image(image_pre, erosion) # 再与原图合并 image_final = image_pre | blur # cv2.imshow("final", image_final) return image_final def cut_BiggestAreas(image): # 裁剪出整张图轮廓最大的部分 def find_BiggestAreas(image_pre): # 定义一个三乘三的卷积核 kernel = np.ones((3, 3), dtype=np.uint8) # 将输入图片膨胀 # dilate = cv2.dilate(image_pre, kernel, 3) # cv2.imshow("dilate", dilate) # 将输入图片二值化 _, thresh = cv2.threshold(image_pre, 127, 255, cv2.THRESH_BINARY) # cv2.imshow("thresh", thresh) # 将二值化后的图片膨胀 dilate_afterThresh = cv2.dilate(thresh, kernel, 5) # cv2.imshow("thresh_afterThresh", dilate_afterThresh) # 找轮廓 contours_, hierarchy = cv2.findContours(dilate_afterThresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 识别出最大的轮廓 # 需要注意的是,在低版本的findContours当中返回的结果是tuple,不支持pop,所以需要将其转为pop contours = [x for x in contours_] area = map(cv2.contourArea, contours) area_list = list(area) area_max = max(area_list) post = area_list.index(area_max) # 将最大的区域保留,其余全部填黑 contours.pop(post) for i in range(len(contours)): cv2.drawContours(image_pre, contours, i, 0, cv2.FILLED) # cv2.imshow("cut", image_pre) return image_pre b, g, r, a = cv2.split(image) a_new = find_BiggestAreas(a) new_image = cv2.merge((b, g, r, a_new)) return new_image def locate_neck(image:np.ndarray, proportion): """ 根据输入的图片(四通道)和proportion(自上而下)的比例,定位到相应的y点,然后向内收缩,直到两边的像素点不透明 """ if image.shape[-1] != 4: raise TypeError("请输入一张png格式的四通道图片!") if proportion > 1 or proportion <=0: raise ValueError("proportion 必须在0~1之间!") _, _, _, a = cv2.split(image) height, width = a.shape _, a = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) y = int(height * proportion) x = 0 for x in range(width): if a[y][x] == 255: break left = (y, x) for x in range(width - 1, -1 , -1): if a[y][x] == 255: break right = (y, x) return left, right, right[1] - left[1] def get_cutbox_image(input_image): height, width = input_image.shape[0], input_image.shape[1] y_top, y_bottom, x_left, x_right = get_box_pro(input_image, model=2) result_image = input_image[y_top:height - y_bottom, x_left:width - x_right] return result_image def brightnessAdjustment(image: np.ndarray, bright_factor: int=0): """ 图像亮度调节 :param image: 输入的图像矩阵 :param bright_factor:亮度调节因子,可正可负,没有范围限制 当bright_factor ---> +无穷 时,图像全白 当bright_factor ---> -无穷 时,图像全黑 :return: 处理后的图片 """ res = np.uint8(np.clip(np.int16(image) + bright_factor, 0, 255)) return res def contrastAdjustment(image: np.ndarray, contrast_factor: int = 0): """ 图像对比度调节,实际上调节对比度的同时对亮度也有一定的影响 :param image: 输入的图像矩阵 :param contrast_factor:亮度调节因子,可正可负,范围在[-100, +100]之间 当contrast_factor=-100时,图像变为灰色 :return: 处理后的图片 """ contrast_factor = 1 + min(contrast_factor, 100) / 100 if contrast_factor > 0 else 1 + max(contrast_factor, -100) / 100 image_b = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) bright_ = image_b.mean() res = np.uint8(np.clip(contrast_factor * (np.int16(image) - bright_) + bright_, 0, 255)) return res class CV2Bytes(object): @staticmethod def byte_cv2(image_byte, flags=cv2.IMREAD_COLOR) ->np.ndarray: """ 将传入的字节流解码为图像, 当flags为 -1 的时候为无损解码 """ np_arr = np.frombuffer(image_byte,np.uint8) image = cv2.imdecode(np_arr, flags) return image @staticmethod def cv2_byte(image:np.ndarray, imageType:str=".jpg"): """ 将传入的图像解码为字节流 """ _, image_encode = cv2.imencode(imageType, image) image_byte = image_encode.tobytes() return image_byte def comb2images(src_white:np.ndarray, src_black:np.ndarray, mask:np.ndarray) -> np.ndarray: """输入两张图片,将这两张图片根据输入的mask进行叠加处理 这里并非简单的cv2.add(),因为也考虑了羽化部分,所以需要进行一些其他的处理操作 核心的算法为: dst = (mask * src_white + (1 - mask) * src_black).astype(np.uint8) Args: src_white (np.ndarray): 第一张图像,代表的是mask中的白色区域,三通道 src_black (np.ndarray): 第二张图像,代表的是mask中的黑色区域,三通道 mask (np.ndarray): mask.输入为单通道,后续会归一化并转为三通道 需要注意的是这三者的尺寸应该是一样的 Returns: np.ndarray: 返回的三通道图像 """ # 函数内部不检查相关参数是否一样,使用的时候需要注意一下 mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR).astype(np.float32) / 255 return (mask * src_white + (1 - mask) * src_black).astype(np.uint8)