Spaces:
Running
Running
""" | |
本文件存放一些自制的简单的图像处理函数 | |
""" | |
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): | |
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 | |
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) | |