需求
前段时间我的宝贝女朋友生日,做了几张马赛克照片来记录一下恋爱中的美好瞬间,就是使用数据集图片来替换调原图中的像素点,生成的图片与原图相比,类似于打了马赛克,但是放大之后会显示数据集中的图片
代码中所使用的照片集资源链接:https://download.csdn.net/download/Huang_X_H/89858939
实现效果
原图
以邓紫棋的照片为例
生成的图片(截图)
实际生成的图片从几十兆到上百兆不等,取决于原图以及像素大小设置,该图片为生成的图片的截图
生成的图片(放大)
此图是生成的图片的眼睛部分放大后的截图,放大之后,可以看出,实际上是若干张刘亦菲的图片组成
代码实现
代码结构
project
----photo_combination
----input_files
----background_files
----LiuYiFei
----LiuYiFei_001.jpg
----LiuYiFei_002.jpg
----...
----template_files
----DengZiQi_001.jpg
----output_files
----DengZiQi_001.jpg
----DengZiQi_001_temp.jpg
----DengZiQi_001_tem2.jpg
----photo_combination.py
photo_combination.py
导入相关的库
import os
import random
import shutil
import time
from abc import ABCMeta, abstractmethod
from collections import Counter
from datetime import datetime
from pathlib import Path
import cv2
import numpy as np
from scipy import stats
抽象工具类
此处有抽象类有两个原因;1.原本想着先使用灰度图进行测试,然后再改为使用彩图;2.可以调整图片匹配像素的方法;可以自行合并为一个类
class PhotoCombinationUtil(metaclass=ABCMeta):
def __init__(self):
self.template_file_path = ""
self.background_files_path = ""
self.output_file_path = None
self.template_file_type = None
self.template_file_extension = None
self.output_file_width = None
self.output_file_height = None
self.combination_item_size = 1
self.background_files_size = 10
self.flag_initialed = False
def get_file_type(self, file_path):
"""
获取文件类型,图片还是视频
:param file_path:
:return:
"""
if not os.path.exists(file_path):
return "unknown", None
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff']
video_extensions = ['.mp4', '.avi', '.mkv', '.mov', '.flv', '.wmv']
_, file_extension = os.path.splitext(file_path)
if file_extension.lower() in image_extensions:
return "image", file_extension
elif file_extension.lower() in video_extensions:
return "video", file_extension
else:
return "unknown", None
def initial_params(self, template_file_path, background_files_path, output_file_path=None,
combination_item_size:int=1, background_files_size:int=10):
"""
初始化目录等基本信息
:return:
"""
self.template_file_path = template_file_path
self.background_files_path = background_files_path
self.output_file_path = output_file_path
self.combination_item_size = combination_item_size
self.background_files_size = background_files_size
self.template_file_type, self.template_file_extension = self.get_file_type(self.template_file_path)
if self.template_file_type == "image":
if self.output_file_path is None or self.output_file_path == "" or not os.path.exists(os.path.dirname(self.output_file_path)) or os.path.exists(self.output_file_path):
if not os.path.exists("photo_combination"):
os.mkdir("photo_combination")
if not os.path.exists(os.path.join("photo_combination", "output_files")):
os.mkdir(os.path.join("photo_combination", "output_files"))
now = datetime.now()
self.output_file_path = os.path.join("photo_combination", "output_files",
f"output_{now.strftime('%Y%m%d%H%M%S')}{self.template_file_extension}")
if self.output_file_width is None or self.output_file_height is None:
template_image = cv2.imread(self.template_file_path)
self.output_file_width, self.output_file_height = template_image.shape[:2]
self.flag_initialed = True
else:
print(f"文件路径:{self.template_file_path},非图片文件")
@abstractmethod
def combination(self):
"""
组合图片
:return:
"""
pass
具体工具类
class ColorPhotoCombinationUtil(PhotoCombinationUtil):
def __init__(self):
super().__init__()
def combination(self):
start_time = time.time()
print("开始合并图片")
# 读取图像,参数1表示以彩色模式读取(默认)
original_image = cv2.imread(self.template_file_path, cv2.IMREAD_COLOR)
# 获取图片中所有的坐标的灰度值
grid_value = []
height, width = original_image.shape[:2]
# 确保图像尺寸是指定分割像素的倍数,如果不是,就裁剪掉多余的部分
if height % self.combination_item_size != 0:
original_image = original_image[:-(height % self.combination_item_size), :]
if width % self.combination_item_size != 0:
original_image = original_image[:, :-(width % self.combination_item_size)]
output_file_name, file_extension = os.path.splitext(os.path.basename(self.output_file_path))
image_temp_path = os.path.join(os.path.dirname(self.output_file_path), f"{output_file_name}-temp{file_extension}")
cv2.imwrite(image_temp_path, original_image)
temp2_image = cv2.resize(original_image, (int(width / self.combination_item_size), int(height / self.combination_item_size)))
image_temp2_path = os.path.join(os.path.dirname(self.output_file_path), f"{output_file_name}-temp2{file_extension}")
cv2.imwrite(image_temp2_path, temp2_image)
height, width = original_image.shape[:2]
# 遍历图像,每次处理一个子区域
print(f"开始获取原图的色彩数据")
for y in range(0, height, self.combination_item_size):
y_line_values = []
for x in range(0, width, self.combination_item_size):
# 提取子区域
subregion = original_image[y:y + self.combination_item_size, x:x + self.combination_item_size]
b_subregion = subregion[:, :, 0]
g_subregion = subregion[:, :, 1]
r_subregion = subregion[:, :, 2]
# 计算子区域的众数
mode_value = [stats.mode(b_subregion, axis=None)[0],
stats.mode(g_subregion, axis=None)[0],
stats.mode(r_subregion, axis=None)[0]]
# 计算子区域的中位数
median_value = [int(np.median(b_subregion)),
int(np.median(g_subregion)),
int(np.median(r_subregion))]
# 计算子区域的平均数
mean_value = [int(np.mean(b_subregion)),
int(np.mean(g_subregion)),
int(np.mean(r_subregion))]
mode_value = "-".join([str(num) for num in mode_value])
median_value = "-".join([str(num) for num in median_value])
mean_value = "-".join([str(num) for num in mean_value])
item_value = {
"mode_value": mode_value,
"median_value": median_value,
"mean_value": mean_value,
}
y_line_values.append(item_value)
grid_value.append(y_line_values)
if y % 10 == 0:
temp_percent = int(100 * (y + 1) / height)
print(f"\r获取原图的色彩数据,已完成{temp_percent}%,耗时:{(time.time() - start_time):.2f} 秒", end="")
print(f"\r已获取原图的色彩数据,耗时:{(time.time() - start_time):.2f} 秒")
# 创建以众数、中位数、平均数为key的字典
value_as_key_dict = {}
# 获取背景图数据集所有图片的色彩数据
background_files_temp_size_path = f"{self.background_files_path}_temp_{self.background_files_size}"
if not os.path.exists(background_files_temp_size_path):
os.mkdir(background_files_temp_size_path)
for root, dirs, files in os.walk(self.background_files_path):
for file in files:
item_image = cv2.imread(os.path.join(self.background_files_path, file), cv2.IMREAD_COLOR)
item_height, item_width = item_image.shape[:2]
if not item_height == item_width:
size = min(item_height, item_width)
# 计算裁剪区域的起始点
start_y = (item_height - size) // 2
start_x = (item_width - size) // 2
# 裁减背景图为正方形
item_image = item_image[start_y:start_y + size, start_x:start_x + size]
item_image = cv2.resize(item_image,
(self.background_files_size, self.background_files_size))
# 保存处理后的图像
cv2.imwrite(os.path.join(background_files_temp_size_path, file), item_image)
# 获取每张背景图色彩值的众数、中位数、平均数
self.get_background_image_data(item_image, file, value_as_key_dict)
else:
for root, dirs, files in os.walk(background_files_temp_size_path):
for file in files:
item_image = cv2.imread(os.path.join(background_files_temp_size_path, file), cv2.IMREAD_COLOR)
# 获取每张背景图色彩值的众数、中位数、平均数
self.get_background_image_data(item_image, file, value_as_key_dict)
print(f"已获取背景图数据集所有图片的色彩数据,耗时:{(time.time() - start_time):.2f} 秒")
# 根据模板图片的色彩映射字典
print(f"开始根据模板图片的色彩映射字典")
for y, y_line in enumerate(grid_value):
for x, item_value in enumerate(y_line):
match_background_images = self.find_match_background_image(item_value, value_as_key_dict)
item_value["file"] = random.choice(match_background_images)
if y % 10 == 0:
temp_percent = int(100 * (y + 1) / len(grid_value))
print(f"\r根据模板图片的色彩映射字典,已完成{temp_percent}%,耗时:{(time.time() - start_time):.2f} 秒", end="")
print(f"\r已根据模板图片的色彩映射字典,耗时:{(time.time() - start_time):.2f} 秒")
# 根据原图坐标组合图片
result_image = np.zeros((len(grid_value) * self.background_files_size, len(grid_value[0]) * self.background_files_size, 3), dtype=np.uint8)
print(f"开始根据原图坐标组合图片")
for y, y_line in enumerate(grid_value):
for x, item_value in enumerate(y_line):
# 计算当前图片在大图中的位置
y_start = y * self.background_files_size
y_end = y_start + self.background_files_size
x_start = x * self.background_files_size
x_end = x_start + self.background_files_size
item_file = item_value["file"]
item_image = cv2.imread(os.path.join(background_files_temp_size_path, item_file), cv2.IMREAD_COLOR)
# 将当前图片放入大图中
result_image[y_start:y_end, x_start:x_end] = item_image
if y % 10 == 0:
temp_percent = int(100 * (y + 1) / len(grid_value))
print(f"\r根据原图坐标组合图片,已完成{temp_percent}%,耗时:{(time.time() - start_time):.2f} 秒", end="")
print(f"\r已根据原图坐标组合图片,耗时:{(time.time() - start_time):.2f} 秒")
# 保存合并后的图片
cv2.imwrite(self.output_file_path, result_image)
print(f"输出图片,{self.output_file_path},耗时:{(time.time() - start_time):.2f} 秒")
pass
def find_closest_number(self, input_list, input_value):
if not input_list:
return [] # 如果列表为空,返回 None 或其他适当的值
closest_values = []
min_differences = float('inf')
for numbers in input_list:
b_number, g_number, r_number = numbers.split("-")
b_value, g_value, r_value = input_value.split("-")
differences = abs(int(b_number) - int(b_value)) + abs(int(g_number) - int(g_value)) + abs(int(r_number) - int(r_value))
if differences < min_differences:
closest_values = [numbers]
min_differences = differences
elif differences == min_differences:
closest_values.append(numbers)
return closest_values
def get_background_image_data(self, background_image, file_name, value_as_key_dict):
b_subregion = background_image[:, :, 0]
g_subregion = background_image[:, :, 1]
r_subregion = background_image[:, :, 2]
# 获取每张背景图灰度值的众数、中位数、平均数
# 计算众数
mode_value = [stats.mode(b_subregion, axis=None)[0],
stats.mode(g_subregion, axis=None)[0],
stats.mode(r_subregion, axis=None)[0]]
# 计算中位数
median_value = [int(np.median(b_subregion)),
int(np.median(g_subregion)),
int(np.median(r_subregion))]
# 计算平均数
mean_value = [int(np.mean(b_subregion)),
int(np.mean(g_subregion)),
int(np.mean(r_subregion))]
mode_value = "-".join([str(num) for num in mode_value])
median_value = "-".join([str(num) for num in median_value])
mean_value = "-".join([str(num) for num in mean_value])
# value_as_key_dict = {
# mean_value:{
# median_value:{
# mode_value:[file_name]
# }
# }
# }
if mean_value in value_as_key_dict:
if median_value in value_as_key_dict[mean_value]:
if mode_value in value_as_key_dict[mean_value][median_value]:
value_as_key_dict[mean_value][median_value][mode_value].append(file_name)
else:
value_as_key_dict[mean_value][median_value][mode_value] = [file_name]
else:
value_as_key_dict[mean_value][median_value] = {
mode_value: [file_name]
}
else:
value_as_key_dict[mean_value] = {
median_value: {
mode_value: [file_name]
}
}
def find_match_background_image(self, input_values, value_dict):
# value_as_key_dict = {
# mean_value:{
# median_value:{
# mode_value:[file_name]
# }
# }
# }
mode_value, median_value, mean_value = input_values["mode_value"], input_values["median_value"], input_values[
"mean_value"]
if mean_value not in value_dict:
most_closet_values = self.find_closest_number(value_dict.keys(), mean_value)
mean_value = random.choice(most_closet_values)
if median_value not in value_dict[mean_value]:
most_closet_values = self.find_closest_number(value_dict[mean_value].keys(), median_value)
median_value = random.choice(most_closet_values)
if mode_value not in value_dict[mean_value][median_value]:
most_closet_values = self.find_closest_number(value_dict[mean_value][median_value].keys(),
mode_value)
mode_value = random.choice(most_closet_values)
return value_dict[mean_value][median_value][mode_value]
程序入口
def main():
combination_item_size = 3
background_files_size = 50
template_file_path = "photo_combination/input_files/template_files/DengZiQi_18.jpg"
background_files_path = "photo_combination/input_files/background_files/LiuYiFei"
output_file_path = f"photo_combination/output_files/DengZiQi-{combination_item_size}-{background_files_size}.jpg"
photo_combination_util = ColorPhotoCombinationUtil()
photo_combination_util.initial_params(template_file_path=template_file_path,
background_files_path=background_files_path,
output_file_path=output_file_path,
combination_item_size=combination_item_size,
background_files_size=background_files_size)
photo_combination_util.combination()
if __name__ == '__main__':
main()
结束语
替换为自己的照片素材之后,最后输出的照片效果还是很令女朋友满意的,就是图片太大了没办法直接通过wechat传输;