日常自写的小脚本 一

7 篇文章 0 订阅
6 篇文章 1 订阅

一、使用cv为批量图片自定义添加文字/水印logo

需求:需要大量带有各式水印、二维码、自定义文字的图片,水印、二维码 大小不固定、位置不固定、模糊程度不固定
功能:自定义为图片添加 图片logo/文字
    1、添加水印
        ① 随机选取水印logo、二维码 添加的位置可以随机插入【“四角”,“去心域”】,也可以指定固定位置插入水印logo
        ② 可以自定义logo、二维码插入 “四角”、“去心域”的大小占比
        ③ logo、二维码水印大小、模糊程度自定义
        ④ 对于“黑底”背景的logo,插入logo时可以去除其“黑色的”背景(通过‘与运算’),且可以通过logo插入位置颜色更改logo的颜色。

    2、添加文本
        ① 插入一行/多行或一列/多列,支持换行换列
        ② 文字大小,颜色,位置,模糊程度,字间距,插入的方向【横 竖】自定义
        ③ 可随机插入文本,可以自定义插入时 “四角”、“去心域”的大小占比
        ④ 可以选择插入文本的颜色是否跟随插入位置颜色的变化而变化

     1、代码 

tip:
    用于生成图片的小脚本,里面有很多臃肿重复的代码,有兴趣的欢迎大家帮忙优化优化
{
    "source_dir": "./source_datas",
    "wm_dir": "./watermasks",
    "report_dir": "./report_datas",
    "type_": "img",
    "alpha": 1.0,
    "logo_height": 70,
    "insert_percent": 0.2,
    "threshold": 20,
    "position": [
        20,
        20
    ],
    "random": false,
    "insert_type": "move_heart",
    "affinity": true,
    "color_increment": 25,
    "text_path": "watermasks/comments.txt",
    "font_size": 40,
    "text_area_size":null,
    "font_color": [
        255,
        235,
        100
    ],
    "font_method": "line",
    "font_offset_h": 0,
    "font_offset_w": 0,
    "font_space_row": 0,
    "font_space_line": 3
}
  •     也可以使用结构更加清晰的 yaml 文件 

            参考博客园的博主:yaml文件解析详解

  •     代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
import cv2
import os
import numpy as np
from pandas import Series
import time
import json
import warnings
warnings.filterwarnings('ignore')
from tqdm import tqdm
from PIL import ImageFont
from PIL import Image
from PIL import ImageDraw
 
class AddLogoText():
 
    def __init__(self, source_imgs_dir,
                 wm_imgs_dir=None,
                 text_path=None,
                 report_dir=None,
                 alpha=0.3,
                 size_height=70,
                 percent=0.2,
                 threshold=20,
                 position=None,
                 random=True,
                 insert_type='move_heart',
                 type_='img',
                 affinity = False,
                 color_increment = 40,
                 fonts_dir = None,
                 font_size = 20,
                 text_area_size = None,
                 font_color = (255,255,0),
                 font_method = 'row',
                 font_offset_h = 0,
                 font_offset_w = 0,
                 font_space_row = 0,
                 font_space_line = 0,
                 *args, **kwargs):
 
        # TODO a:插入图片相关参数
        self.source_imgs_dir = source_imgs_dir  # 原图片的目录路径,str
        self.wm_imgs_dir = wm_imgs_dir  # logo或者二维码的目录路径,logo/二维码图片不可是灰度图,str
        self.report_dir = report_dir  # 图片合成后输入的文件夹
        assert report_dir is not None
        self.alpha = alpha  # logo 插入的模糊度,1.0 表示完全不模糊
        assert 0.0 <= self.alpha <= 1.0
        self.position = position  # logo、二维码插入的像素位置元祖,使用时需要将 random 置为 False,tuple
        self.random = random  # 是否随机插入,开启时position无效 ,bool
        if not random:
            assert position is not None
        self.insert_type = insert_type  # random = True时有效,随机插入的类型;'move_heart' 随机插入至原图片去心域;'four_corn' 随机插入至原图四个角的位置
        assert insert_type in ['move_heart', 'four_corn']
        self.type_ = type_  # "img" :在图片中 插入 logo , "text":在图片中插入文本
        assert type_ in ['img', 'text']
        self.affinity = affinity  # 插入logo时是否使用"与"运算 bool
        self.color_increment = color_increment  # 指的是如果 logo/text 开启 affinity时,使用 “ 插入位置平均色阶 + affinity_increment ” 的值作为 logo/text 改变的颜色,int
        self.size_height = size_height  # logo/二维码 resize 插入原图片时的高度的像素大小,int
        self.percent = percent  # logo/二维码插入图片时 不能越界【左上 左下 右上 右下】 percent占比的位置 【仅仅在 随机插入 即random=True 有效 】,float
        self.threshold = threshold # logo 插入原图片 "与" 运算二值化的阈值
        assert 0.0 < self.percent < 0.5
 
        # TODO b:插入文本时相关参数
        self.text_path = [os.path.join(text_path,i) for i in os.listdir(text_path) if i.endswith('.txt')][0]  # 图片写文字的文件路径 str
        if type_ == 'text':
            assert fonts_dir is not None
        self.fonts_dir = fonts_dir # 插入字体时,字体的 ttf 文件保存路径
        self.font_size = font_size
        self.text_area_size = text_area_size # 给插入文本时文本域的大小,int
        self.font_color = font_color
        self.font_method = font_method # 文字写入的方式,row 横向 line 纵向 str
        assert self.font_method in ['row','line']
        self.font_offset_h = font_offset_h # 文字插入位置高度的偏移量
        self.font_offset_w = font_offset_w # 文字插入位置宽度的偏移量
        assert self.font_offset_w >= 0 and self.font_offset_h >= 0
        self.font_space_row = font_space_row # 文字 与 文字之间间距(水平方向)
        self.font_space_line = font_space_line # 文字 与 文字之间间距(垂直方向)
 
    def bitwiseAnd(self,main_img,logo,position,threshold=20,color_increment=40):
        """
        进行logo融入图片的时候进行 ‘与’ 运算,去除除了logo以外其他内容
        tip:logo的部分灰度图的色阶 需要高于其他非logo部分的色阶
        :param main_img: 主图片 bgr array
        :param logo: logo bgr array
        :param position: logo插入图片的位置
        :param color: affinity 为 False 时,将logo更改的颜色
        :param threshold: logo 进行‘与运算’时二值化的阈值
        :param affinity: 是否根据logo所插入的图片的周围的颜色改变logo的颜色与之相似
        :return: logo 插入后的 roi 区域 ,logo插入的地址
        """
        height_logo, weight_logo, logo_channels = logo.shape
        row,col = position
        main_roi = main_img[row:row + height_logo, col:col + weight_logo]
 
        # 取 roi 区域 bgr 平均色阶
        roi_m_color = np.mean(np.mean(main_roi, axis=0), axis=0).astype('uint8')
 
        # 将logo二值化
        logo2gray = cv2.cvtColor(logo, cv2.COLOR_BGR2GRAY)
        ret_tmp1, bin_tmp1 = cv2.threshold(logo2gray, threshold, 255, cv2.THRESH_BINARY)
 
        # 将二值化的 logo 进行反转
        bin_tmp2 = cv2.bitwise_not(bin_tmp1)
 
        # 构建 Mask 与 Mask_inv; Mask:logo颜色为纯黑,其他部分为纯白,Mask_inv 与之恰恰相反
        if bin_tmp1[0, 0] == 0 :
            Mask = bin_tmp2
            Mask_inv = bin_tmp1
        else:
            Mask = bin_tmp1
            Mask_inv = bin_tmp2
 
        # 进行 ‘与’ 计算
        main_img_black = cv2.bitwise_and(main_roi, main_roi, mask=Mask)
        logo_only = cv2.bitwise_and(logo, logo, mask=Mask_inv)
 
        # 是否进行根据 主图片 插入位置的颜色更改logo的颜色
        logo_only_gray = cv2.cvtColor(logo_only, cv2.COLOR_BGR2GRAY)
        row_tmp1, col_tmp1 = logo_only_gray.shape
 
        for i in range(row_tmp1):
            for j in range(col_tmp1):
                if logo_only_gray[i, j] == 0:
                    pass
                else:
                    logo_only[i, j, :] = (roi_m_color + color_increment).astype('uint8')
 
        # for i in range(row_tmp1):
        #     for j in range(col_tmp1):
        #         if logo_only_gray[i, j] == 0:
        #             pass
        #         else:
        #             logo_only[i, j, :] = np.array((220,220,245)).astype('uint8')
 
        roi_all = cv2.add(main_img_black, logo_only)
 
        # main_img[row:row + height_logo, col:col + weight_logo] = roi_all
        # cv2.imshow('main_img', main_img)
        # cv2.waitKey(0)
        # cv2.destroyAllWindows()
        return roi_all,position
 
    def cv_read_image(self, fpath):
        """
        cv 加载图片【路径可以是中文】,返回图片的数组,如果图片为 灰度图,flag = False,否则返回 true
        :return: cv_img,flag   bgr数组,flag 判断 读取图片是否成功
        """
 
        # 读取带有中文路径的图片
        def cn_imread(filePath):
            try:
                cv_img = cv2.imdecode(np.fromfile(filePath, dtype=np.uint8),cv2.IMREAD_COLOR)
                """ 
                cv2.imdecode 参数与 cv2.imread 打开的图片相似,如果不需要第四通道,选择 cv2.IMREAD_COLOR,读取的格式是 bgr 数组
                
                cv2.IMREAD_COLOR:加载彩色图片,这个是默认参数,可以直接写1。
                cv2.IMREAD_GRAYSCALE:以灰度模式加载图片,可以直接写0。
                cv2.IMREAD_UNCHANGED:包括alpha,可以直接写-1
                """
            except Exception as e:
                print('当前图片opencv无法读取,原因为:\n{}'.format(e))
                cv_img = None
            return cv_img
 
        flag = True
        img_bgr = cn_imread(fpath)
        # img_bgr = cv2.imread(fpath)
 
        if img_bgr is None:
            flag = False
            return None,flag
        # Tip: 代码多余,即使是灰色图,默认情况下他也是3通道进行读取的
        dim = img_bgr.ndim
        if dim == 2:
            flag = False
        return img_bgr, flag
 
    def load_images(self, source_dir):
        """
        给定文件夹,加载数据
        :return:
        """
        assert source_dir is not None
 
        father_dir_paths = []
        file_paths = []
        for root, dirs, files in os.walk(source_dir):
            for file in files:
                # 获取文件所属目录
                father_dir_paths.append(root)
                # 获取文件路径
                file_paths.append(os.path.join(root, file))
        return file_paths, father_dir_paths
 
    def time_stamp_str(self, str_):
        """
        为了保证字符串的唯一性,给字符串末尾添加时间戳
        :param str_:
        :return:
        """
        stamp_ = str(time.time()).replace('.', '')
        str_ = str(str_) + stamp_
        return str_
 
    def cv_save_img(self, array, img_name, report_dir):
        """
        用 cv2 将 三通道 BGR array 保证为 img_name,保存在 report_dir中
        :param array: bgr 图片的数组
        :param img_name: 图片的名字,CV2文件中不可以有中文
        :param report_dir: 图片保存到的目录中, CV2 路径中不可以有中文
        :return:
        """
 
        # 保存带有中文路径的图片
        def cn_imwrite(file_path, img_array):
            # 这个方法需要特别注意,img_array一定是一个BGR格式的uint8 ndarray
            cv2.imencode('.jpg', img_array)[1].tofile(file_path)
 
        assert report_dir is not None
        # 判断目录是否存在
        if not os.path.exists(report_dir):
            os.makedirs(report_dir)
 
        img_path = os.path.join(report_dir, img_name)
        cn_imwrite(img_path, array)
 
    def pil_read_img(self,fpath):
        """
        给定 fpath,利用 PIL 读取 图片,并转化为 RGB 三通道
        :param fpath:
        :return: s_img,flag  flag 表示 Image open 是否成功
        """
        flag = True
        try:
            # 加载 img 源图
            s_img = Image.open(fpath)  # 打开图片 <PIL.PngImagePlugin.PngImageFile image mode=RGBA size=1318x567 at 0x24332BD0708>
        except Exception as e:
            flag = False
            s_img = None     # Image open 读取文件失败,置为None
            return s_img,flag
 
        if s_img.mode != 'RGB':
            s_img = s_img.convert('RGB')
        """
            Image.open 与 cv imread 不同,可以读取中文路径
        """
        return s_img,flag
 
    # TODO 核心函数 Img
    def add_logo_single_img(self, source_img_fpath,
                            wm_img_fpath,
                            alpha=0.6,
                            size_height=100,
                            percent=0.3,
                            position=None,
                            random=True,
                            insert_type='move_heart',
                            affinity=False,
                            color_increment=40,
                            threshold=20):
 
        """
        将一张图片 与 loge 通过cv2.addWeighted 的方式进行融合,本函数不处理原图为灰度图的情况
        tip:1、二维码插入浅色的位置会随着深色的位置一起淡化或加深 2、logo 插入图片可以开启 “与” 运算
        :param source_img_path: 原图片的文件路径,str
        :param wm_img_path: logo或者二维码的文件路径,logo/二维码图片不可是灰度图,str
        :param alpha: logo 加入图片的模糊状态,最大 1.0,即不做模糊处理,float
        :param size_height: logo/二维码 resize 插入原图片时的高度的像素大小,int
        :param percent: logo/二维码插入图片时 不能越界【左上 左下 右上 右下】 percent占比的位置 【仅仅在 随机插入 即random=True 有效 】,float
        :param position: logo 插入的 左上角的像素位置【random=True 无效】,tuple
        :param random: 插入位置是否随机,bool
        :param insert_type: # random = True时有效,随机插入的类型;'move_heart' 随机插入至原图片去心域;'four_corn'
        :param affinity  # 插入logo时是否使用"与"运算 bool
        :param threshold # logo 进行‘与运算’时二值化的阈值
        :return: s_img,position; s_img:插入logo后图片的 bgr数组,position 插入logo的 左上角定点的 位置
        """
 
        if not random:
            assert position is not None
        # 加载 img logo图
        s_img, s_flag = self.cv_read_image(source_img_fpath)
        wm_img, wm_flag = self.cv_read_image(wm_img_fpath)
 
        # bgr 通道执行
        if s_flag and wm_flag:
            s_img_h, s_img_w = s_img.shape[:2]
            tmp1_h, tmp1_w = wm_img.shape[:2]
            # 将 logo/二维码 resize
            wm_logo = cv2.resize(wm_img, dsize=(int(tmp1_w / (tmp1_h * 1.0 / size_height)), size_height),
                                 interpolation=cv2.INTER_LINEAR)
            wm_logo_h, wm_logo_w = wm_logo.shape[:2]
            # print(s_img_h,s_img_w,wm_logo_h,wm_logo_w)
 
            # 判断 logo、二维码的尺寸小于图片尺寸
            if (wm_logo_h < s_img_h) and (wm_logo_w < s_img_w):
 
                # 如果是随机插入图片,需要生成随机插入点 , 否则 使用position的地址作为图片的插入点
                if random:
                    if insert_type == 'four_corn':
                        # TODO 生成随机的 '四角'的顶点
                        percent_h_top = int(s_img_h * 1.0 * percent)
                        percent_h_tail = int(s_img_h * 1.0 * (1 - percent))
                        percent_w_l = int(s_img_w * 1.0 * percent)
                        percent_w_r = int(s_img_w * 1.0 * (1 - percent))
 
                        if np.random.uniform() < 0.5:  # 如果随机数小于 0.5 判断顶点在上方,否则在下方
                            ran_h = np.random.randint(low=0, high=percent_h_top)
                        else:
                            ran_h = np.random.randint(low=percent_h_tail, high=s_img_h)
                        if np.random.uniform() < 0.5:  # 如果随机数小于 0.5 判断顶点在左方,否则在右方
                            ran_w = np.random.randint(low=0, high=percent_w_l)
                        else:
                            ran_w = np.random.randint(low=percent_w_r, high=s_img_w)
                        position = (ran_h, ran_w)
                    elif insert_type == 'move_heart':
                        # TODO 生成随机的 去除'心域'的顶点
                        percent_h_top = int(s_img_h * 1.0 * percent)
                        percent_h_tail = int(s_img_h * 1.0 * (1 - percent))
                        percent_w_l = int(s_img_w * 1.0 * percent)
                        percent_w_r = int(s_img_w * 1.0 * (1 - percent))
 
                        ran_h = np.random.randint(low=0, high=s_img_h)
                        if percent_h_tail > ran_h >= percent_h_top:  # 当 高度h 落在中间的 区域时,去除 '心域'
                            if np.random.uniform() < 0.5:  # 如果改值小于 0.5,选择 “左边”,否则选择 “右边”
                                ran_w = np.random.randint(low=0, high=percent_w_l)
                            else:
                                ran_w = np.random.randint(low=percent_w_r, high=s_img_w)
                        else:
                            ran_w = np.random.randint(low=0, high=s_img_w)
                        position = (ran_h, ran_w)
 
                # 判断 position 插入后是否越界,如果越界,将logo、二维码左移/上移 至极限位置
                start_h, start_w = position
                if start_h + wm_logo_h > s_img_h:
                    start_h = s_img_h - wm_logo_h
                if start_w + wm_logo_w > s_img_w:
                    start_w = s_img_w - wm_logo_w
                position = (start_h, start_w)
 
                # 将图片的数据 深拷贝一份,用于 cv2.addWeighted
                s_img_copy = s_img.copy()
 
                # 将loge、二维码数据嵌入
                if not affinity:
                    # s_img_copy[start_h:start_h + wm_logo_h, start_w:start_w + wm_logo_w, ...] = wm_logo
                    roi_copy = wm_logo
                else:  # 提取插入原图周边的颜色更改logo的颜色
                    roi_copy,_ = self.bitwiseAnd(s_img_copy,wm_logo,position=position,
                                                 threshold=threshold,color_increment=color_increment)
                # 图片融合
                roi_main = s_img[start_h:start_h + wm_logo_h, start_w:start_w + wm_logo_w, ...]
                roi_all = cv2.addWeighted(roi_main, 1.0 - alpha, roi_copy, alpha, gamma=0)
                s_img[start_h:start_h + wm_logo_h, start_w:start_w + wm_logo_w, ...] = roi_all
 
                return s_img, position
 
            else:
                print('当前logo/二维码尺寸为h:{} w:{},原图 {} 尺寸太小,不做处理'.format(wm_logo_h, wm_logo_w,source_img_fpath))
                return None, False
 
        else:  # 原图读取失败
            print('CV读取原图 {} 失败,跳过处理'.format(source_img_fpath))
            return None, False
 
    def add_logo_allImgs(self):
        """
        通过 cv2.addWeighted 的方式将 所有的源图片 与 随机选择的logo 进行融合
        :return:
        """
        source_img_paths, _ = self.load_images(self.source_imgs_dir)
        wm_img_paths = [i for i in self.load_images(self.wm_imgs_dir)[0] if i.endswith('.jpg') or i.endswith('.png')]
 
        print('logo 嵌入中,一共{}张源图片,{}张logo图片'.format(len(source_img_paths), len(wm_img_paths)))
        for idx, img in tqdm(enumerate(source_img_paths)):
            # 随机取一个 二维码/loge图片
            wm_img_fpath = np.random.choice(wm_img_paths)
            img_bgr, _ = self.add_logo_single_img(source_img_fpath=img,
                                                  wm_img_fpath=wm_img_fpath,
                                                  alpha=self.alpha,
                                                  size_height=self.size_height,
                                                  percent=self.percent,
                                                  position=self.position,
                                                  random=self.random,
                                                  insert_type=self.insert_type,
                                                  affinity=self.affinity,
                                                  color_increment=self.color_increment,
                                                  threshold=self.threshold)
            if img_bgr is not None:
                img_name = self.time_stamp_str(str(idx + 1) + '_') + '.jpg'
                # 保存图片
                self.cv_save_img(img_bgr, img_name, self.report_dir)
 
    def load_comments(self, comments_path):
 
        """
        加载文件【①去除空行数据 ②去除首尾空格】至Series中,保存每段文本所对应的行号
        :param comments_path: 文件的路径
        :return: Series
        """
        assert comments_path is not None
 
        # 将数据保存至 Series 中
        comments_ser = Series()
        try:
            with open(comments_path, 'r', encoding='utf-8-sig') as w:
                for row, line in enumerate(w.readlines()):
                    # 对每一行的数据前后去除空格
                    line = line.strip()
                    # 判断去除空格后的数据是否有内容,只要有数据的内容【去除空行数据】
                    if line and not line.startswith('//'):  # 设置已 // 开头的数据为注释
                        # 添加行号
                        row_index = row + 1
                        comments_ser.loc[row_index] = line
 
        except Exception as e:
            with open(comments_path, 'r', encoding='gbk') as w:
                for row, line in enumerate(w.readlines()):
                    # 对每一行的数据前后去除空格
                    line = line.strip()
                    # 判断去除空格后的数据是否有内容,只要有数据的内容【去除空行数据】
                    if line and not line.startswith('//'):  # 设置已 // 开头的数据为注释
                        # 添加行号
                        row_index = row + 1
                        comments_ser.loc[row_index] = line
        if comments_ser.size == 0:
            raise Exception('comments.txt 不能为空!!!!')
        return comments_ser
 
    # TODO 核心函数 Text
    def add_text_single_img(self,source_img_fpath,
                            comment,
                            alpha=0.5,
                            font_size=20,
                            font_color = (240, 240, 240),
                            percent=0.3,
                            position=None,
                            random=True,
                            insert_type='move_heart',
                            affinity=True,
                            color_increment=40,
                            add_font_type=None,
                            font_method='row',
                            font_offset_h=0,
                            font_offset_w=0,
                            font_space_row=0,
                            font_space_line=0,
                            *args,**kwargs):
        """
        在 一张图片 嵌入 text【目前只能插入一行或者一列文本,不支持换行】
        :param source_img_fpath: 这一张图片的文件路径 str
        :param comment: 需要嵌入 text 的文本数据 str
        :param alpha: 嵌入 text 文本的模糊度 float
        :param font_size: 嵌入 text 文本字体的大小 int
        :param font_color: 嵌入 text 文本字体的颜色 tuple
        :param percent: 插入文本时 不能越界【左上 左下 右上 右下】 percent占比的位置 【仅仅在 随机插入 即random=True 有效 】,float
        :param position: 、插入文本时第一个文本左上角的像素位置【random=True 无效】,tuple
        :param random: 插入位置是否随机,bool
        :param insert_type:  random = True时有效,随机插入的类型;'move_heart' 随机插入至原图片去心域;'four_corn' ,str
        :param affinity:  是否提取插入位置颜色附近的颜色作为插入文本字体的颜色,该参数为 True时,font_color 失去作用,bool
        :param color_increment: 取相对于近似背景色阶的增量,int
        :param add_font_type:选择插入文本的 ttf 文件,如果不给定选择系统默认的ttf字体
        :param font_method: # 文字写入的方式,row 横向 line 纵向 str
        :param font_offset_h: # 文字插入位置高度的偏移量
        :param font_offset_w: # 文字插入位置宽度的偏移量
        :param font_space_row: # 文字 与 文字之间间距(水平方向)
        :param font_space_line: # 文字 与 文字之间间距(垂直方向)
        :param args:
        :param kwargs:
        :return: s_img,position; s_img:Image 对象,position 插入文本时的 第一个文字左上角定点的 位置
        """
 
        def affinity_color(roi,color_increment):
            """
            给定text插入位置三通道roi,计算平均色阶,返回 平均色阶 + increment 三通道颜色
            :param roi:
            :param color_increment:
            :return: color tuple
            """
            mean_color = np.mean(np.mean(roi,axis=0),axis=0)
            mean_color_gray = np.mean(mean_color).astype('uint8')
            if mean_color_gray >220:
                color = np.array((30,34,30))
            else:
                incre_tmp1 = mean_color / mean_color[2]
                list_tmp4 = []
                for idx,i in enumerate(mean_color):
                    color_incre = i + incre_tmp1[idx] * color_increment
                    if color_incre > 255:
                        color_incre = 255
                    list_tmp4.append(color_incre)
                color = np.array(list_tmp4)
            # print(color)
            color = color.astype('uint8')
            # print(type(color))
            return tuple(color)  # 必须步骤,color 只接受元祖

        def swap_row(s_img_w, s_img_h, position_begin, font_weights, f_h_1_max):
            """
            横向插入句子时,句子的宽度超过了图片的宽度,对字符进行换行操作
            :param s_img_w:
            :param position_begin:
            :param font_weights:
            :param f_h_1_max: 句子中最大的字符高度
            :return:
            """
            begin_h, begin_w = position_begin
            end_h = begin_h   #  分段文本最大的文字索引的高度
            end_w = s_img_w   #  分段文本最大的文字索引的宽度
            row_point = begin_w
            position_chars = []  # 定义所有字符插入的位置列表
            for row in font_weights:
                list_tmp2 = []
                for char_w in row:
                    if row_point + char_w > s_img_w:
                        end_h += f_h_1_max
                        row_point = begin_w
                    list_tmp2.append((end_h, row_point))
                    row_point = row_point + char_w
                position_chars.append(list_tmp2)
                end_h += f_h_1_max
                row_point = begin_w

            # 越界判断
            if end_h > s_img_h:
                out_value = end_h - s_img_h + f_h_1_max  # 计算越界的值
                begin_h = begin_h - out_value
                end_h = s_img_h
                position_chars = [[(j[0] - out_value, j[1]) for j in i] for i in position_chars]  # 将越界的插入位置上移

            position_begin = (begin_h, begin_w)
            return position_begin, end_h, end_w, position_chars

        def swap_line(s_img_h,s_img_w,position_begin,font_heights,f_w_1_max):
            """
            纵向插入句子时,句子的高度超过了图片的高度度,对字符进行换列操作
            :param s_img_h:
            :param s_img_w:
            :param position_begin:
            :param font_heights:
            :param f_w_1_max: 句子中最大的字符宽度
            :return:
            """
            begin_h, begin_w = position_begin
            end_w = begin_w
            end_h = s_img_h
            line_point = begin_h
            position_chars = []  # 定义所有字符插入的位置列表
            for line in font_heights:
                list_tmp3 = []
                for char_h in line:
                    if line_point + char_h > s_img_h:
                        end_w += f_w_1_max
                        line_point = begin_h
                    list_tmp3.append((line_point,end_w))
                    line_point = line_point + char_h
                position_chars.append(list_tmp3)
                end_w += f_w_1_max
                line_point = begin_h

            # 越界判断
            if end_w > s_img_w:
                out_value = end_w - s_img_w + f_w_1_max  # 计算越界的值
                begin_w = begin_w - out_value
                end_w = s_img_w
                position_chars = [[(j[0] , j[1] - out_value) for j in i] for i in position_chars]  # 将越界的插入位置上移

            position_begin = (begin_h, begin_w)
            return position_begin, end_h, end_w, position_chars

        def write_text(draw,
                       comment_sep,
                       position_chars,
                       font_obj,
                       font_color=(240, 240, 240)):

            """
            给桌布对象添加文字
            :param draw:
            :param comment_sep: 多段字符串
            :param position_chars:
            :param font_obj:
            :param font_color:
            :return:
            """
            for idx,row in enumerate(comment_sep):
                for jdx, char_ in enumerate(row):
                    # print("序号-值", idx, char_)
                    # print("坐标", position_chars[idx][0], position_chars[idx][1])
                    draw.text((position_chars[idx][jdx][1],position_chars[idx][jdx][0]), char_, font_color, font=font_obj)  # 设置位置坐标(x轴,y轴) 文字 颜色 字体
 
        if not random:
            assert position is not None
        # 加载 img 源图 【将其他模式均转化为 rgb 三通道】
        s_img,flag = self.pil_read_img(source_img_fpath)
        if s_img is None:
            print('Image open 文件 {} 失败,跳过处理'.format(source_img_fpath))
            return None,False
 
        s_img_rgb = np.asarray(s_img)
        s_img_h,s_img_w = s_img_rgb.shape[:2]
        # print(s_img_h,s_img_w)
        draw = ImageDraw.Draw(s_img)  # <PIL.ImageDraw.ImageDraw object at 0x0000028819323608>

        if not bool(add_font_type): # 如果没有给定字体文件,选择系统默认的字体
            add_font_type = "C:\\Windows\\Fonts\\SIMLI.TTF"  # 字体文件   行楷 STXINGKA.TTF华文行楷   simkai.ttf 楷体  SIMLI.TTF隶书  minijianhuangcao.ttf  迷你狂草    kongxincaoti.ttf空心草
 
        # 设置字体,如果没有,也可以不设置
        font_obj = ImageFont.truetype(add_font_type, font_size)

        # 提取所有文字的 x轴 y轴 信息
        # print(comment)
        # 根据 </sep> 分割段落
        comment_sep = comment.split('</sep>')  # 对 字符串 进行 分段,分隔符为 </sep>
        # print(comment_sep)
        font_info_list = [[font_obj.getsize(char) for char in comment] for comment in comment_sep]
        # 将
        font_weights,font_heights = [],[]
        for i_tmp1 in font_info_list:
            font_weights_1,font_heights_1 = zip(*i_tmp1)
            font_weights.append(font_weights_1)
            font_heights.append(font_heights_1)

        # print(font_weights)
        # print(font_heights)
        font_weights = [[j + font_space_row for j in i] for i in font_weights]   # 为每一个文字的水平方向添加字间距
        font_heights = [[j + font_space_line for j in i] for i in font_heights]  # 为每一个文字的垂直方向添加字间距
        # print(font_weights)
        # print(font_heights)

        f_w_1_max = max([max(i) for i in font_weights])     # 获取最宽字符的宽度【包含了 font_space_row】
        f_h_1_max = max([max(i) for i in font_heights])       # 获取最高字符的高度【包含了 font_space_line】
        f_w = max([sum(i) for i in font_weights])  # 横向插入分段句子的最宽的宽度
        f_h = max([sum(i) for i in font_heights])  # 纵向插入分段句子的最高的高度
        # print(f_w_1_max)
        # print(f_h_1_max)
        # print(f_w)
        # print(f_h)

        # 生成随机坐标
        if random:
            if insert_type == 'four_corn':
                # TODO 生成随机的 '四角'的顶点
                percent_h_top = int(s_img_h * 1.0 * percent)
                percent_h_tail = int(s_img_h * 1.0 * (1 - percent))
                percent_w_l = int(s_img_w * 1.0 * percent)
                percent_w_r = int(s_img_w * 1.0 * (1 - percent))
 
                if np.random.uniform() < 0.5:  # 如果随机数小于 0.5 判断顶点在上方,否则在下方
                    ran_h = np.random.randint(low=0, high=percent_h_top)
                else:
                    ran_h = np.random.randint(low=percent_h_tail, high=s_img_h)
                if np.random.uniform() < 0.5:  # 如果随机数小于 0.5 判断顶点在左方,否则在右方
                    ran_w = np.random.randint(low=0, high=percent_w_l)
                else:
                    ran_w = np.random.randint(low=percent_w_r, high=s_img_w)
                position = (ran_h, ran_w)
            elif insert_type == 'move_heart':
                # TODO 生成随机的 去除'心域'的顶点
                percent_h_top = int(s_img_h * 1.0 * percent)
                percent_h_tail = int(s_img_h * 1.0 * (1 - percent))
                percent_w_l = int(s_img_w * 1.0 * percent)
                percent_w_r = int(s_img_w * 1.0 * (1 - percent))
 
                ran_h = np.random.randint(low=0, high=s_img_h)
                if percent_h_tail > ran_h >= percent_h_top:  # 当 高度h 落在中间的 区域时,去除 '心域'
                    if np.random.uniform() < 0.5:  # 如果改值小于 0.5,选择 “左边”,否则选择 “右边”
                        ran_w = np.random.randint(low=0, high=percent_w_l)
                    else:
                        ran_w = np.random.randint(low=percent_w_r, high=s_img_w)
                else:
                    ran_w = np.random.randint(low=0, high=s_img_w)
                position = (ran_h, ran_w)
 
        # 字体的插入模式 'row' 横向插入  'line' 纵向插入
        position_begin = (position[0] +font_offset_h,position[1]+font_offset_w)
        position_begin_h,position_begin_w = position_begin
        sep_num = len(comment_sep)
        if font_method == 'row':
            if  f_w <= s_img_w:
                # 越界处理
                if position_begin_h + f_h_1_max * sep_num > s_img_h:
                    position_begin_h = s_img_h - f_h_1_max * sep_num
                if position_begin_w + f_w > s_img_w:
                    position_begin_w = s_img_w - f_w
                position_begin = (position_begin_h,position_begin_w)   # 真正起始插入字的索引位置
                position_end_h = position_begin_h + f_h_1_max * sep_num   # 真正终止插入字的纵向索引位置
                position_end_w = position_begin_w + f_w         # 真正终止插入字的横向向索引位置

                # 计算所有字符插入的位置列表
                point_tmp1 = position_begin_w
                point_tmp3 = position_begin_h
                position_chars = []
                for row in font_weights:
                    list_tmp1 = []
                    for char_w in row:
                        list_tmp1.append((point_tmp3,point_tmp1))
                        point_tmp1 += char_w
                    position_chars.append(list_tmp1)
                    point_tmp3 += f_h_1_max
                    point_tmp1 = position_begin_w

            else:
                position_begin_w = 0   # 由于文本超过了图片的宽度,因此将 position_begin_w 置为 0
                position_begin = (position_begin_h,position_begin_w)
                position_begin,position_end_h,position_end_w,position_chars = swap_row(s_img_w = s_img_w,
                                                                                        s_img_h = s_img_h,
                                                                                        position_begin = position_begin,
                                                                                        font_weights=font_weights,
                                                                                        f_h_1_max = f_h_1_max)
            # print(position_chars)
            # 是否进行颜色提取转化
            roi_rgb_source = s_img_rgb[position_begin[0]:position_end_h, position_begin[1]:position_end_w ,...] # 取 文字插入区域 ROI
            if affinity:
                font_color = affinity_color(roi_rgb_source,color_increment)

            # 插入文本
            write_text(draw=draw,
                           comment_sep=comment_sep,
                           position_chars=position_chars,
                           font_obj=font_obj,
                           font_color=font_color)

            # cv2.addWeighted alpha 加权文本
            s_img_array_text = np.asarray(s_img)[..., :3] # 取 RGB 三通道的维度

            s_img_bgr_text = s_img_array_text[...,::-1]  # 转化为 cv 的 bgr 格式
            s_img_bgr = s_img_rgb[...,::-1]

            s_img_bgr_text = cv2.addWeighted(s_img_bgr,1-alpha,s_img_bgr_text,alpha,gamma=0)
            return s_img_bgr_text, position

        elif font_method == "line":
            if  f_h <= s_img_h:
                
                # 越界处理
                if position_begin_w + f_w_1_max * sep_num > s_img_w:
                    position_begin_w = s_img_w - f_w_1_max * sep_num
                if position_begin_h + f_h > s_img_h:
                    position_begin_h = s_img_h - f_h
                position_begin = (position_begin_h, position_begin_w)  # 真正起始插入字的索引位置
                position_end_w = position_begin_w + f_w_1_max * sep_num  # 真正终止插入字的纵向索引位置
                position_end_h = position_begin_h + f_h  # 真正终止插入字的横向向索引位置

                # 计算所有字符插入的位置列表
                point_tmp2 = position_begin_w
                point_tmp4 = position_begin_h
                position_chars = []
                for line in font_heights:
                    list_tmp1 = []
                    for char_h in line:
                        list_tmp1.append((point_tmp4, point_tmp2))
                        point_tmp4 += char_h
                    position_chars.append(list_tmp1)
                    point_tmp2 += f_w_1_max
                    point_tmp4 = position_begin_h
            else:
                position_begin_h = 0   # 由于文本超过了图片的高度,因此将 position_begin_h 置为 0
                position_begin = (position_begin_h,position_begin_w)
                position_begin,position_end_h,position_end_w,position_chars = swap_line(s_img_w = s_img_w,
                                                                                        s_img_h = s_img_h,
                                                                                        position_begin = position_begin,
                                                                                        font_heights=font_heights,
                                                                                        f_w_1_max = f_w_1_max)
            # 是否进行颜色提取转化
            roi_rgb_source = s_img_rgb[position_begin[0]:position_end_h, position_begin[1]:position_end_w ,...] # 取 文字插入区域 ROI
            if affinity:
                font_color = affinity_color(roi_rgb_source,color_increment)

            # 插入文本
            write_text(draw=draw,
                           comment_sep=comment_sep,
                           position_chars=position_chars,
                           font_obj=font_obj,
                           font_color=font_color)

            # cv2.addWeighted alpha 加权文本
            s_img_array_text = np.asarray(s_img)[..., :3] # 取 RGB 三通道的维度

            s_img_bgr_text = s_img_array_text[...,::-1]  # 转化为 cv 的 bgr 格式
            s_img_bgr = s_img_rgb[...,::-1]

            s_img_bgr_text = cv2.addWeighted(s_img_bgr,1-alpha,s_img_bgr_text,alpha,gamma=0)
            return s_img_bgr_text,position
        else:
            print('未知问题,跳过')
            return None,position

    def add_text_allImgs(self):
        """
        给所有的图片添加文本
        :return:
        """
        assert self.text_path is not None
        assert self.report_dir is not None
        # 加载 ttf 字体文件
        fonts_type_list,_ = self.load_images(self.fonts_dir)
        fonts_type_list = [i for i in fonts_type_list if i.endswith('.ttf') or i.endswith('.TTF')]

        source_img_paths, _ = self.load_images(self.source_imgs_dir)
        comments_ser = self.load_comments(self.text_path)
        index = comments_ser.index
        print('文本 嵌入中,一共 {} 张源图片,{}个随机嵌入文本'.format(len(source_img_paths), len(comments_ser)))
        for idx, img_path in tqdm(enumerate(source_img_paths)):
            row = comments_ser[np.random.choice(index)]
            add_font_type = np.random.choice(fonts_type_list)
            img_bgr_array,_ = self.add_text_single_img(source_img_fpath=img_path,
                                     comment=row,
                                     alpha=self.alpha,
                                     font_size=self.font_size,
                                     font_color = self.font_color,
                                     percent=self.percent,
                                     position=self.position,
                                     random=self.random,
                                     insert_type=self.insert_type,
                                     affinity=self.affinity,
                                     color_increment=self.color_increment,
                                     add_font_type=add_font_type,
                                     font_method=self.font_method,
                                     font_offset_h=self.font_offset_h,
                                     font_offset_w=self.font_offset_w,
                                     font_space_row=self.font_space_row,
                                     font_space_line=self.font_space_line,
                                     )

            # 将 Image 的对象 img 保存
            if img_bgr_array is not None:
                img_name = self.time_stamp_str(str(idx + 1) + '_') + '.jpg'
                # 保存图片
                self.cv_save_img(img_bgr_array, img_name, self.report_dir)
 
    def run(self):
        if self.type_ == 'img':
            print('当前模式为嵌入logo的模式!')
            print('\n参数如下:\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}'\
                  '\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n'.format(
                "source_dir",self.source_imgs_dir,
                "wm_dir",self.wm_imgs_dir,
                "report_dir",self.report_dir,
                "type_",self.type_,
                "alpha",self.alpha,
                "position",self.position,
                "random",self.random,
                "insert_type",self.insert_type,
                "affinity",self.affinity,
                "color_increment",self.color_increment,
                "shuiyin_height",self.size_height,
                "logo_height",self.size_height,
                "threshold",self.threshold,
                "percent",self.percent
            ))
 
            self.add_logo_allImgs()
        elif self.type_ == "text":
            print('当前模式为嵌入文本的模式!')
            print('\n参数如下:\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}'\
                  '\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n'.format(
                "source_dir", self.source_imgs_dir,
                "text_path", self.text_path,
                "report_dir", self.report_dir,
                "type_", self.type_,
                "alpha", self.alpha,
                "position", self.position,
                "random", self.random,
                "insert_type", self.insert_type,
                "affinity", self.affinity,
                "color_increment", self.color_increment,
                "percent", self.percent,
                "fonts_dir",self.fonts_dir,
                "font_size",self.font_size,
                "text_area_text",self.text_area_size,
                "font_color",self.font_color,
                "font_method",self.font_method,
                "font_offset_h",self.font_offset_h,
                "font_offset_w" ,self.font_offset_w,
                "font_space_row" ,self.font_space_row,
                "font_space_line",self.font_space_line,
            ))
 
            self.add_text_allImgs()
 
 
if __name__ == '__main__':
    config = r"./config/config.json"
    with open(config,"r",encoding='utf-8-sig') as r:
        config_params = json.load(r)
    '''
        json 文件有以下特点
            ① json 可以传递 字符串格式的字典、数组,不可以传递 元祖
            ② json 最后一行末尾不可以有‘ ,’
            ③ json 文件总 False True 需要用 false true 来表示
            ④ json 中的字符串必须用英文状态下的 双引号
    '''
 
    # 嵌入图片相关参数
    source_dir = config_params['source_dir']
    wm_dir = config_params['wm_dir']
    report_dir = config_params['report_dir']
    type_ = config_params['type_']
    alpha = config_params["alpha"]
    logo_height = config_params["logo_height"]
    insert_percent = config_params["insert_percent"]
    threshold= config_params["threshold"]
    position = tuple(config_params['position'])
    random = config_params['random']
    insert_type = config_params['insert_type']
    affinity = config_params['affinity']  # 控制是否提取插入位置的色阶,若插入logo 则同时开启 “与运算”
    color_increment= config_params['color_increment']
 
    # 嵌入文本相关参数
    text_path = config_params['text_path_dir']
    fonts_dir = config_params['fonts_dir']
    font_size = config_params['font_size']
    text_area_size = config_params['text_area_size']
    font_color = tuple(config_params['font_color'])
    font_method = config_params['font_method']
    font_offset_h = config_params['font_offset_h']
    font_offset_w = config_params['font_offset_w']
    font_space_row = config_params['font_space_row']
    font_space_line = config_params['font_space_line']
 
    obj = AddLogoText(source_imgs_dir=source_dir,
                      wm_imgs_dir=wm_dir,
                      text_path=text_path,
                      report_dir=report_dir,
                      alpha=alpha,
                      size_height=logo_height,
                      percent=insert_percent,
                      threshold=threshold,
                      position=position,
                      random=random,
                      insert_type=insert_type,
                      type_=type_,
                      affinity=affinity,
                      color_increment=color_increment,
                      fonts_dir = fonts_dir,
                      font_size=font_size,
                      text_area_size = text_area_size,
                      font_color=font_color,
                      font_method= font_method,
                      font_offset_h=font_offset_h,
                      font_offset_w=font_offset_w,
                      font_space_row=font_space_row,
                      font_space_line=font_space_line,
                      )
 
    obj.run()

    2、效果 

    (1)插入 二维码、logo

  •                 二维码

                     二维码图片:

                                                  

 

                     原图:

                                

 

                      效果:

         LOGO参与 “与运算” 

          二维码未参与 “与运算” 

          二维码未参与 “与运算”  

          LOGO 参与 “与运算” 

  •              参数:
二维码:
source_dir:./datas/source_imgs
wm_dir:./datas/QR_logos
report_dir:./report_datas
type_:img
alpha:0.65
position:(20, 20)
random:False
insert_type:four_corn
affinity:False
color_increment:40
shuiyin_height:70
percent:0.2

logo:
source_dir:./datas/source_imgs
wm_dir:./datas/QR_logos
report_dir:./report_datas
type_:img
alpha:0.65
position:(20, 20)
random:False
insert_type:four_corn
affinity:True
color_increment:40
shuiyin_height:70
percent:0.2

    (2)插入文本数据

  •             文本数据
// </sep> 换行符,本行为注释

你好</sep>我是一位新人</sep>CSDN
  •              效果

                                

  •                参数 
source_dir:./source_datas
text_path:watermasks/comments.txt
report_dir:./report_datas
type_:text
alpha:1.0
position:(20, 20)
random:False
insert_type:four_corn
affinity:False
color_increment:100
percent:0.15
fonts_dir:./fonts
font_size:25
text_area_text:None
font_color:(225, 225, 230)
font_method:line
font_offset_h:0
font_offset_w:0
font_space_row:2
font_space_line:3

   3、遇到的问题

   (1)cv python 读取、写入中文路径解决

原因:python中用的 是utf-8编码,而opencv用的是gbk,要用numpy先读一下,另外写文件也是一个道理。
# 读取带有中文路径的图片
def cn_imread(filePath):
    try:
        cv_img = cv2.imdecode(np.fromfile(filePath, dtype=np.uint8), cv2.IMREAD_COLOR)
        """ 
        cv2.imdecode 参数与 cv2.imread 打开的图片相似,如果不需要第四通道,选择 cv2.IMREAD_COLOR,此时读取图片的格式为 bgr

        cv2.IMREAD_COLOR:加载彩色图片,这个是默认参数,可以直接写1。
        cv2.IMREAD_GRAYSCALE:以灰度模式加载图片,可以直接写0。
        cv2.IMREAD_UNCHANGED:包括alpha,可以直接写-1
        """
    except Exception as e:
        print('当前图片opencv无法读取,原因为:\n{}'.format(e))
        cv_img = None
    return cv_img

# 保存带有中文路径的图片
def cn_imwrite(file_path , img_array ): 
    # 这个方法需要特别注意,img_array一定是一个BGR格式的uint8 ndarray
    cv2.imencode('.jpg', img_array)[1].tofile(file_path)

   (2)用 opencv 读取 1通道的灰度图默认情况下获得的数组是 3通道 还是 1通道 ?

读取一张RGB图像时可以使用cv2.imread(im_path)完成,但是需要注意读取后的位数以及通道数。
如果用cv2.imread读取一张灰度图,默认得到的是3通道的numpy,此数组三个通道的值相同,但灰度图是1通道的,所以可以使用以下:
im = cv2.imread('path', -1)
这样通道数以及图片的位数都会保持不变。
快速察看图像信息:
    右击图片属性,摘要,点击详细属性,里面有位深度一项。如果是RGB图,位深度是24;如果是灰度和索引图,位深度是8,
    四通道RGBA的位深度为32。

              tip:
                   换句话来讲,上文插入logo/二维码代码中  “读取图片” 判断 是否是灰阶图是无用的代码,可以删掉,

                   这里不删了,作为学习的反面教材

   (3)Opencv numpy中uint8类型存储图像

               用opencv处理图像时,可以发现获得的矩阵类型都是uint8

import cv2 as cv
img=cv.imread(hello.png)
print(img)
array([[[...],
        [...],
        [...]]],dtype='uint8')

               uint8是专门用于存储各种图像的(包括RGB,灰度图像等),范围是从0–255
               这里要注意如何转化到uint8类型

注意一下两种方式:
1: numpy有np.uint8()函数,但是这个函数仅仅是对原数据和0xff相与(和最低2字节数据相与),这就容易导致如果原数据是大于255的,
   那么在直接使用np.uint8()后,比第八位更大的数据都被截断了,比如:
a=[2000,100,2]
np.uint8(a)
array([208, 100, 2], dtype=uint8)
2: 用cv2.normalize函数配合cv2.NORM_MINMAX,可以设置目标数组的最大值和最小值,然后让原数组等比例的放大或缩小到目标数组,
   如下面的例子中是将img的所有数字等比例的放大或缩小到0–255范围的数组中,
   cv2.normalize(img, out, 0, 255, cv2.NORM_MINMAX)
   然后改变数据类型
   np.array([out],dtype=‘uint8’)
  总结:
        要想将当前的数组作为图像类型来进行各种操作,就要转换到uint8类型,转换的方式推荐使用第二种,因为第一种在值大于
        255以后就容易丢失。

      (4)CV 打开 图片 三通道、PIL 打开 图片 三通道 遇到的各种问题

                  1、PIL 保存图像“cannot write mode P as JPEG”的解决方法

                        因为 jpg 模型保存需要 rgb 三通道,此时需要 利用 convert 将 其他模式转化为 RGB 模式

def pil_read_img(self, fpath):
    """
    给定 fpath,利用 PIL 读取 图片,并转化为 RGB 三通道
    :param fpath:
    :return: s_img,flag  flag 表示 Image open 是否成功
    """
    flag = True
    try:
        # 加载 img 源图
        s_img = Image.open(
            fpath)  # 打开图片 <PIL.PngImagePlugin.PngImageFile image mode=RGBA size=1318x567 at 0x24332BD0708>
    except Exception as e:
        flag = False
        s_img = None  # Image open 读取文件失败,置为None
        return s_img, flag

    if s_img.mode != 'RGB':
        s_img = s_img.convert('RGB')
    """
        Image.open 与 cv imread 不同,可以读取中文路径
    """
    return s_img, flag

s_img = pil_read_img('1.png')
s_img.save('1_new.jpg')

                 2、CV 打开文件为空值、报错 、中文路径问题

 自定义函数

def cv_read_image(fpath):
    """
    cv 加载图片【路径可以是中文】,返回图片的 BGR 三通道数组,如果文件为打开失败或cv不可打开的图片数据,flag = False,否则返回 true
    :return: cv_img,flag   bgr数组,flag 判断 读取图片是否成功
    """

    # 读取带有中文路径的图片
    def cn_imread(filePath):
        try:
            cv_img = cv2.imdecode(np.fromfile(filePath, dtype=np.uint8), cv2.IMREAD_COLOR)
            """ 
            cv2.imdecode 参数与 cv2.imread 打开的图片相似,如果不需要第四通道,选择 cv2.IMREAD_COLOR,读取的格式是 bgr 数组

            cv2.IMREAD_COLOR:加载彩色图片,这个是默认参数,可以直接写1。
            cv2.IMREAD_GRAYSCALE:以灰度模式加载图片,可以直接写0。
            cv2.IMREAD_UNCHANGED:包括alpha,可以直接写-1
            """
        except Exception as e:
            print('当前图片opencv无法读取,原因为:\n{}'.format(e))
            cv_img = None
        return cv_img

    flag = True
    img_bgr = cn_imread(fpath)

    if img_bgr is None:
        flag = False
        return None, flag
    return img_bgr, flag

         (5) RBG 通道 与 BGR 通道转化

① 使用 cv 或 PIL API进行转换
② 使用 array[...,::-1] 进行对 array 最后一个维度进行反转,即 bgr 三通道 与 rgb 三通道的转换

 

  4、 将 上诉脚本封装为 exe 文件,使用 pyinstaller 与  Enigma Virtual Box

             py文件打包为exe可执行文件(pyinstaller),解决exe包大且运行速度慢

 

  5、Reference

           OpenCV合并图片cv2.add、cv2.addWeighted、cv2.seamlessClone 效果对比、按位运算bitwise_and

 

二、对文件中每一行文本的指定词进行替换

需求:需要对大量文本中的特定词进行指定词汇库随机替换,采用贪婪匹配替换的方式
1、功能:自定义替换词典,自定义替换词的权重随机对文件中每一行文本的指定词进行替换,采用贪婪匹配的方法
2、注意点:
    ① 采用贪婪匹配
        例:词汇表中存在:中华人民、中华人民共和国,需要将 中华人民共和国 匹配到
    ② 替换后的词不会被二次替换
        例:若存在 可爱地:可爱的,的:地 两组替换词汇
            当文本中的 可爱地 替换为 可爱的 之后,该词中的 的 不允许再次被替换称 地。
            换句话来说该业务不适合用 re 正则进行快捷替换,因为正则替换会出现重复替换的情况【除非记录re每次替换的索引位置】。

1、代码

    (1) comments.txt 内容

中国人,中国人啊啊啊啊啊,中国人民,中华人民,中华人民共和国万岁
哈哈哈哈,我回家了,来拉拉阿拉啦啦啦
    杭州电子科技大学啊,杭州电子,清华大学清华夹,杭州电子科技大卡
非常可爱滴,的商正在开发中

    (2) vocab.csv 内容

// 必须严格遵守词汇表替换格式,否则可能出现错误,
// 格式如下:待替换词汇    替换词汇1,替换权重    替换词汇2,替换权重    ...     词汇的长度。
// 替换词汇与替换权重中间用英文逗号隔开,每一组替换值之间用空格、tab或多个空格隔开
// 保证词汇按照字符长短由高到底排序,被包含的词语放在包含词语的下面。简而言之,匹配模式遵循贪婪匹配

杭州电子科技大学	杭电,1	航電,1	行電,1	8 
中华人民共和国 中化人民共核国,1   China,2 PRC,1   7
清华大学    清华打血,1  清花大学,2  清大,3   清华d学,2  4
中国人民    Chinese,1   4
电子科	電z科,1	3
回家了 huijiale,1   3
可爱滴 可爱的,1   3
中华  中花,1   2
中国  仲国,1    2
的   底,1 	1

     (3)代码

                  Logger_Hanlder 自定义类代码地址:python logger 日志管理 自定义库

如果不想输出logger,也可以将所有有关logger的代码删除
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import re,os
from langconv import *
from pandas import DataFrame,Series,concat
from numpy import random
import warnings
warnings.filterwarnings('ignore')
from utils.utils import Logger_Hanlder,Directory_Hanlder

class CommentsReplace():
    def __init__(self,report_dir,vocab_path,comments_path,logger = None):
        if not report_dir:
            report_dir = '.'
        self.report_dir = report_dir
        self.vocab_path = vocab_path
        self.comments_path = comments_path
        self.logger = logger

    def load_vocab(self,vocab_path):
        """
        加载字符串替换的字典文件数据
        :param vocab_path:
        :return: vocab_df: DataFrame数据类型,index对应的是 csv文件中的行号, columns 是词汇的内容; replace_dict: dict数据类型, {待替换词汇1:{替换词汇1:替换权重,替换词汇2:替换权重,...},
                                                                                                                          待替换词汇2:{替换词汇1:替换权重,替换词汇2:替换权重,...}, ... }

            replace_dict:
            {'杭州电子科技大学': {'杭电': 1, '航電': 1, '行電': 1}, '中华人民共和国': {'中化人民共核国': 1, 'China': 2, 'PRC': 1},
            '清华大学': {'清华打血': 1, '清花大学': 2, '清大': 3, '清华d学': 2}, '中国人民': {'Chinese': 1}, '电子科': {'電z科': 1},
            '回家了': {'huijiale': 1}, '可爱滴': {'可爱的': 1}, '中华': {'中花': 1}, '中国': {'仲国': 1}, '的': {'底': 1}}

            vocab_df:
            word                char_1 char_2 char_3 char_4 char_5 char_6 char_7 char_8 char_9
        6   杭州电子科技大学      杭      州      电      子      科      技      大      学  <END>
        7    中华人民共和国      中      华      人      民      共      和      国  <END>    NaN
        8       清华大学      清      华      大      学  <END>    NaN    NaN    NaN    NaN
        9       中国人民      中      国      人      民  <END>    NaN    NaN    NaN    NaN
        10       电子科      电      子      科  <END>    NaN    NaN    NaN    NaN    NaN
        11       回家了      回      家      了  <END>    NaN    NaN    NaN    NaN    NaN
        12       可爱滴      可      爱      滴  <END>    NaN    NaN    NaN    NaN    NaN
        13        中华      中      华  <END>    NaN    NaN    NaN    NaN    NaN    NaN
        14        中国      中      国  <END>    NaN    NaN    NaN    NaN    NaN    NaN
        15         的      的  <END>    NaN    NaN    NaN    NaN    NaN    NaN    NaN
        """

        vocab_datas = []
        index = []
        try:
            with open(vocab_path,'r',encoding='utf-8-sig') as w:
                for row,line in enumerate(w.readlines()):
                    # 对每一行的数据前后去除空格
                    line = line.strip()
                    # 判断去除空格后的数据是否有内容,只要有数据的内容和非以 // 开头的文本数据
                    if line and not line.startswith('//'):
                        words_list = re.split('\s+', line)
                        #必须保证列表的长度 > 2,即替换词汇不能为空
                        if len(words_list) > 2:
                            # 添加行号
                            index.append(row + 1)
                            vocab_datas.append(words_list)

        except Exception as e:
            with open(vocab_path, 'r', encoding='gbk') as w:
                for row, line in enumerate(w.readlines()):
                    # 对每一行的数据前后去除空格
                    line = line.strip()
                    # 判断去除空格后的数据是否有内容,只要有数据的内容和非以 // 开头的文本数据
                    if line and not line.startswith('//'):
                        words_list = re.split('\s+', line)
                        # 必须保证列表的长度 > 1,即替换词汇不能为空
                        if len(words_list) > 2:
                            # 添加行号
                            index.append(row + 1)
                            vocab_datas.append(words_list)

        # 创建单词的dataframe  列内容:单词本身  待分解的单词

        # 取出最大的单词的单词长度
        words_max_len = int(vocab_datas[0][-1])
        columns =['word']
        vocab_df = DataFrame(columns=columns)

        # 构建 vocab 替换的 dataframe 数据
        replace_dict = {}
        for idx,word in enumerate(vocab_datas):
            # 提取替换词汇的信息
            word_tmp1 = word[1:-1]
            word_tmp2 = {} # 收集 {替换词汇:替换权重} 字典,同一代替换词汇保存至列表中
            for ky in word_tmp1:
                ky_list = ky.split(',')
                if len(ky_list) == 2 and ky_list[1] != '': # 必须保证 键值对存在,即 替换词汇与替换权重必须用英文逗号分隔且只给定一个不作数
                    word_tmp2[ky_list[0]] = float(ky_list[1]) # 字符串数字转化为浮点型
                else:
                    raise Exception('vocab 中存在不正确的格式')
            if len(word_tmp2) >= 1: # 必须保证 替换词汇与替换权重 至少存在一组
                word_tmp3 = word[0]
                replace_dict[word_tmp3] = word_tmp2
                vocab_df.loc[index[idx], :] = [word_tmp3]

        # print(vocab_df)
        # print(replace_dict)
        tmp1_df = vocab_df['word'].apply(lambda x:
                                            Series(list(x) + ['<END>'])
                                            )
        tmp1_df.columns = ['char_%d' %(i+1) for i in range(words_max_len + 1) ]
        vocab_df = concat([vocab_df,tmp1_df],axis=1)
        return vocab_df,replace_dict

    def load_comments(self,comments_path):
        """
        加载待替换的评论文件
        :param comments_path: 评论文件的路径
        :return: Series
        """
        # 将数据保存至 Series 中
        comments_ser = Series()
        try:
            with open(comments_path,'r',encoding='utf-8-sig') as w:
                for row,line in enumerate(w.readlines()):
                    # 对每一行的数据前后去除空格
                    line = line.strip()
                    # 判断去除空格后的数据是否有内容,只要有数据的内容
                    if line:
                        # 添加行号
                        row_index = row + 1
                        comments_ser.loc[row_index] = line

        except Exception as e:
            with open(comments_path, 'r', encoding='gbk') as w:
                for row, line in enumerate(w.readlines()):
                    # 对每一行的数据前后去除空格
                    line = line.strip()
                    # 判断去除空格后的数据是否有内容,只要有数据的内容
                    if line:
                        # 添加行号
                        row_index = row + 1
                        comments_ser.loc[row_index] = line

        return comments_ser

    def word2word(self,source_word,replace_vocab):
        """
        给定一个待替换词,根据给定的替换词汇表,根据每个替换选项的权重来进行随机选择一个替换词作为替换并返回
        :param source_word:
        :param replace_vocab: 格式 例子:{'杭州电子科技大学': {'杭电': 1, '航電': 1, '行電': 1}, '中华人民共和国': {'中化人民共核国': 1, 'China': 2, 'PRC': 1},
                                            '清华大学': {'清华打血': 1, '清花大学': 2, '清大': 3, '清华d学': 2}, '中国人民': {'Chinese': 1}, '电子科': {'電z科': 1},
                                            '回家了': {'huijiale': 1}, '可爱滴': {'可爱的': 1}, '中华': {'中花': 1}, '中国': {'仲国': 1}, '的': {'底': 1}}
        :return: 替换后的词
        """
        words_dict = replace_vocab[source_word]
        replace_words = list(words_dict.keys())
        replace_num = [float(words_dict[key]) for key in words_dict]
        sum_tmp1 = sum(replace_num)

        replace_weights = [i * 1.0/sum_tmp1 for i in replace_num]
        replace_word = random.choice(replace_words,p=replace_weights)
        return replace_word

    def traditional_2_simplified(self,sentence):
        """
            将繁体字转化简体字
        """
        sentence = Converter('zh-hans').convert(sentence)
        sentence.encode('utf-8')
        return sentence

    def replace_word(self,line_str,vocab_df,replace_dict):
        """
        处理一行句子,将句子中的单词按照单词 长短先后优先级 替换为其他单词
        :param line_str: 一行句子
        :param vocab_df: 用于方便判断句子需要替换的单词的 dataframe
        :param replace_dict: 需要替换为的单词的字典
        :return: 原始句子,需要替换的单词,替换后的结果
        """
        # 保存原始句子
        self.source_line_str = line_str
        self.line_replace_words = {}
        self.line_length = len(line_str)
        line_str = self.traditional_2_simplified(line_str) # 将文本统一转化为简体中文

        words_info = vocab_df.loc[:,'word']
        columns = vocab_df.columns
        chars_info = vocab_df.loc[:,columns[1:]]
        # print(chars_info)
        # print('='*100)

        # TODO 替换字符串的核心代码 >>>>>>>>>>>>>>>>>>
        _,col_num = chars_info.shape
        # 定义开始结束指针
        line_length = len(line_str)
        start = 0
        end = 0
        for i in range(line_length):      # 该for循环指的是所有字符均不匹配的情况下最大循环的次数

            # 判断起始索引是否已经越界,如果已经越界,break
            if start == line_length:
                break

            # char = line_str[start]
            # print('当前判断词的首字符为:',char)

            # 将原始词汇表复制一份出来
            tmp_chars_info = chars_info.copy(deep=True)
            tmp_replace_word = None  # 保存当前状态的词语的变量

            while True:
                bool1_index = tmp_chars_info.iloc[:,end-start] == line_str[end]
                # 获取待检测后的df
                discover1_df = tmp_chars_info[bool1_index]
                discover1_df_size = discover1_df.size

                # TODO 这种情况代表第一个字符就不匹配,break , end += 1
                if discover1_df_size == 0 and (end - start) == 0:
                    end += 1
                    break

                # TODO 第一个字符与词汇表中的匹配成功,做后续判断
                else:
                    # print('\ndiscover1_df:\n',discover1_df)
                    end += 1  # 更改下个字符的索引位置

                    # a TODO 判断下一个字符为 <END> 的标志,为了能够获取当前阶段匹配到的词
                    bool2_index = discover1_df.iloc[:,end-start] == '<END>'
                    discover2_df = discover1_df[bool2_index]

                        # a.1 保存当前状态的词语
                    if discover2_df.size > 0:
                        tmp_replace_word = ''.join(discover2_df.iloc[0,0:end-start])
                    if end == line_length: # 判断 end 是否已经 越界(超出 line_str的长度),超出,break,【此判断很重要】
                        break

                    # b TODO 判断下一个字符是否在替换词典中可以找的到,如果可以,继续循环去找,如果找不到,说明词汇不在替换字典中,break
                    bool3_index = discover1_df.iloc[:,end-start] == line_str[end]
                    discover3_df = discover1_df[bool3_index]
                    discover3_df_size = discover3_df.size
                    if discover3_df_size == 0:
                        # 下一个字符匹配失败
                        # TODO 非常重要,判断 end 指针是否会多匹配,需要将end指针置于上一状态寻找的 tmp_replace_word单词的终止位置。 如果一个词都有没有找到,即 tmp_replace_word 为 None,end指针的位置不做处理
                        if tmp_replace_word is not None:
                            end = start + len(tmp_replace_word)
                        break
                    else:
                        # TODO 很重要,将 discover1_df 赋值 给tmp_chars_info,下一次循环要用到上一轮迭代的 dataframe,而非整体的 dataframe
                        tmp_chars_info = discover1_df

            # 将需要替换的词汇保存下来,
            if tmp_replace_word is not None:
                self.line_replace_words[(start,end)] = tmp_replace_word # 考虑到字典重复的单词会被覆盖,所以用索引位置 (start,end)

            # TODO 该步骤很重要,只有扫描到匹配词的时候,将 start == 上一轮的end ; 若没有找到词语,必须索引 +1 扫描,即只能将 start += 1,将end == start,这是为了防止漏词
            if tmp_replace_word is not None:
                start = end   # 更新指针的数值
            else:
                start += 1
                end = start

        # print("\n当前语句已经全部扫描完毕,句子详细信息为:{}".format(line_str))
        # print(line_str[67:68])
        # print(line_str[0:0])
        # print('当前语句需要替换的词汇为:{}\n'.format(self.line_replace_words))

        if self.line_replace_words:
            point = 0  # 定义指针 point
            new_line_str = ''
            for v_index,k_word in self.line_replace_words.items():
                new_line_str = new_line_str + self.source_line_str[point:v_index[0]] + self.word2word(k_word,replace_dict)
                point  = v_index[1]
            # 判断 point 指针的位置是否处于 line_str 的末端【大部分情况都不会处于末端】,因此需要将末尾’遗忘‘的字符串添加上
            if point < self.line_length:
                new_line_str = new_line_str + self.source_line_str[point:]
        else: # self.line_replace_words 为空,即表示当前文本并没有匹配到单词需要替换,因此需要返回原来的字符串
            new_line_str = self.source_line_str

        if self.logger is not None:
            self.logger.info('\n1、原文本:\n{}\n2、需要替换的词汇:\n{}\n3、替换后的结果:\n{}\n'.format(self.source_line_str,
                                                                         self.line_replace_words,
                                                                         new_line_str))
        else:
            print('\n1、原文本:\n{}\n2、需要替换的词汇:\n{}\n3、替换后的结果:\n{}\n'.format(self.source_line_str,
                                                                                      self.line_replace_words,
                                                                                      new_line_str))
        return new_line_str,self.source_line_str

    def multi_line_replace(self,report_dir,lines_iter,vocab_df,replace_dict):
        """
        多行数据进行替换
        :param lines_iter:
        :return:
        """
        if not os.path.exists(report_dir):
            os.makedirs(report_dir)
        fpath = os.path.join(report_dir,'report_comments.txt')
        row_num = len(lines_iter)
        with open(fpath,'w',encoding='utf-8') as w:
            line_list = []
            for idx,line in enumerate(lines_iter):
                new_line,_ = self.replace_word(line,vocab_df,replace_dict)
                print('原文本有{}行哥样本,现在是第{}行'.format(row_num,idx + 1))
                line_list.append(new_line + '\n')
            random.shuffle(line_list) # 打乱操作
            line_list[-1] = line_list[-1].split('\n')[0]  # 去除最后一行的空格
            w.writelines(line_list)
        if self.logger is not None:
            self.logger.info('\n' + '='*100 + '\n')

    def run(self):
        # 加载词汇表 与 替换词汇字典
        vocab_df, replace_dict = self.load_vocab(vocab_path = self.vocab_path)
        # print(vocab_df)
        # print(replace_dict)
        # 加载待替换字符串集合
        comments_ser = self.load_comments(comments_path = self.comments_path)
        # 处理多行字符串并进行替换后保存
        self.multi_line_replace(report_dir = self.report_dir,
                                lines_iter = comments_ser,
                                vocab_df = vocab_df,
                                replace_dict = replace_dict)

if __name__ == '__main__':

    source_dir = './datas'
    files_path,_ = Directory_Hanlder.list_dir_all_files(source_dir)
    comments_path = [i for i in files_path if i.endswith('.txt')][0]  # 待替换字符串文件路径
    vocab_path = [i for i in files_path if i.endswith('.csv')][0]   # 词汇表文件路径
    report_dir = './report_datas'   # 替换后输出文件的目录
    logger_report_path = './logs'
    logger = Logger_Hanlder.setup_logging(report_path = logger_report_path)

    replace_obj = CommentsReplace(report_dir = report_dir,
                                  vocab_path= vocab_path,
                                  comments_path = comments_path,
                                  logger = logger)
    # 运行
    replace_obj.run()

结果:

2020-11-03 21:17:24,625 - logs:utils.utils.py - comments_replace.py[line:264] - INFO - 
1、原文本:
中国人,中国人啊啊啊啊啊,中国人民,中华人民,中华人民共和国万岁
2、需要替换的词汇:
{(0, 2): '中国', (4, 6): '中国', (13, 17): '中国人民', (18, 20): '中华', (23, 30): '中华人民共和国'}
3、替换后的结果:
仲国人,仲国人啊啊啊啊啊,Chinese,中花人民,PRC万岁

2020-11-03 21:17:24,634 - logs:utils.utils.py - comments_replace.py[line:264] - INFO - 
1、原文本:
哈哈哈哈,我回家了,来拉拉阿拉啦啦啦
2、需要替换的词汇:
{(6, 9): '回家了'}
3、替换后的结果:
哈哈哈哈,我huijiale,来拉拉阿拉啦啦啦

2020-11-03 21:17:24,668 - logs:utils.utils.py - comments_replace.py[line:264] - INFO - 
1、原文本:
杭州电子科技大学啊,杭州电子,清华大学清华夹,杭州电子科技大卡
2、需要替换的词汇:
{(0, 8): '杭州电子科技大学', (15, 19): '清华大学', (25, 28): '电子科'}
3、替换后的结果:
行電啊,杭州电子,清大清华夹,杭州電z科技大卡

2020-11-03 21:17:24,676 - logs:utils.utils.py - comments_replace.py[line:264] - INFO - 
1、原文本:
非常可爱滴,的商正在开发中
2、需要替换的词汇:
{(2, 5): '可爱滴', (6, 7): '的'}
3、替换后的结果:
非常可爱的,底商正在开发中

2020-11-03 21:17:24,677 - logs:utils.utils.py - comments_replace.py[line:288] - INFO - 
====================================================================================================

 

 

 

 

 

 

 

 

 

 

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值