TheEeeeLin's picture
update files
d5d20be verified
raw
history blame
19.6 kB
"""
本文件存放一些自制的简单的图像处理函数
"""
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)