照片生成心形工具【免费】【下载即可使用】

自己写的程序。效果如图:

用了1w张图片,生成了40分钟。小图片是250*250像素的,放大也不会失真。心形图片大小700M。需要用专门的软件打开,否则会慢。我是用的是gimp来查看。

程序页面:

可执行程序:

通过网盘分享的文件:爱心拼贴画生成器.exe

链接: https://pan.baidu.com/s/1lP_CmtUHbc2JOeDCxJQsjQ 提取码: 1234

通过网盘分享的文件:gimp-3.0.8-setup-1.exe

链接: https://pan.baidu.com/s/1LRYaBToMi3AVnXJsBhj5JA 提取码: 1234


代码:

cpp 复制代码
import os
import argparse
from PIL import Image, ImageDraw
import math
import random
from concurrent.futures import ThreadPoolExecutor, as_completed

def create_heart_mask(width, height):
    """创建爱心形状的掩码"""
    mask = Image.new('L', (width, height), 0)
    draw = ImageDraw.Draw(mask)
    
    # 调整心形大小和位置
    # 心形宽度设为画布宽度的70%,高度设为画布高度的70%
    heart_width = width * 0.7
    heart_height = height * 0.7
    
    # 计算心形中心偏移
    offset_x = width // 2
    offset_y = height // 2
    
    # 爱心形状的数学公式
    for x in range(width):
        for y in range(height):
            # 计算相对坐标(以心形中心为原点)
            rel_x = (x - offset_x) / (heart_width / 2)
            rel_y = (offset_y - y) / (heart_height / 2)  # 反转y轴
            
            # 标准心形方程
            equation = (rel_x**2 + rel_y**2 - 1)**3 - (rel_x**2) * (rel_y**3)
            if equation <= 0:
                draw.point((x, y), fill=255)
    
    return mask

def process_image(img, size):
    """处理图片,调整大小并保持比例"""
    # 调整图片大小,保持比例
    img.thumbnail((size, size), Image.LANCZOS)
    
    # 创建正方形画布
    square_img = Image.new('RGB', (size, size), (240, 240, 240))
    
    # 计算居中位置
    paste_x = (size - img.width) // 2
    paste_y = (size - img.height) // 2
    
    # 粘贴图片到正方形画布
    square_img.paste(img, (paste_x, paste_y))
    
    return square_img

def generate_test_images(count, output_dir):
    """生成测试用的示例图片"""
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    
    colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), (255, 0, 255), (0, 255, 255)]
    
    for i in range(count):
        # 创建100x100的测试图片
        img = Image.new('RGB', (100, 100), colors[i % len(colors)])
        draw = ImageDraw.Draw(img)
        
        # 在图片上添加数字
        draw.text((40, 40), str(i+1), fill=(255, 255, 255))
        
        # 保存测试图片
        img.save(os.path.join(output_dir, f'test_{i+1}.png'))
    
    print(f"生成了{count}张测试图片到{output_dir}")

def create_heart_collage(input_dir, output_path, photo_size=250, output_format='png', quality=100, log_queue=None):
    """创建爱心形状的照片拼贴画"""
    def log(message):
        """记录日志"""
        if log_queue:
            log_queue.put(message)
        print(message)
    
    # 获取输入图片列表(直接读取目录,不递归)
    image_files = []
    
    try:
        log(f"开始读取目录: {input_dir}")
        for item in os.listdir(input_dir):
            item_path = os.path.join(input_dir, item)
            if os.path.isfile(item_path):
                # 检查是否为图片文件
                if item.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
                    image_files.append(item_path)
    except Exception as e:
        log(f"读取目录{input_dir}时出错: {e}")
        return
    
    if not image_files:
        log("错误:输入目录中没有找到图片文件")
        return
    
    log(f"成功找到{len(image_files)}张图片")
    
    # 获取图片数量
    img_count = len(image_files)
    required_positions = img_count
    
    # 固定照片大小
    log(f"使用照片大小: {photo_size}×{photo_size}像素")
    log(f"需要至少{required_positions}个位置")
    
    # 计算初始心形大小
    # 估算每个心形大小能容纳的位置数量(心形面积约为正方形的一半)
    heart_size = 2000
    estimated_positions = 0
    
    # 循环扩大心形大小,直到能容纳所有图片
    while estimated_positions < required_positions:
        # 计算当前大小能容纳的位置数量
        positions_per_side = heart_size // photo_size
        estimated_positions = (positions_per_side * positions_per_side) // 2
        
        if estimated_positions < required_positions:
            # 扩大心形大小
            heart_size += 2000
            log(f"当前心形大小({heart_size-2000}像素)无法容纳所有图片,扩大到{heart_size}像素")
    
    log(f"最终心形大小: {heart_size}×{heart_size}像素")
    log(f"预计可容纳{estimated_positions}个位置")
    
    # 创建爱心掩码
    log("创建爱心掩码...")
    mask = create_heart_mask(heart_size, heart_size)
    
    # 创建输出图片
    log("创建输出图片...")
    output = Image.new('RGB', (heart_size, heart_size), (240, 240, 240))
    
    # 收集所有符合条件的位置
    log("收集有效位置...")
    valid_positions = []
    for y in range(0, heart_size, photo_size):
        for x in range(0, heart_size, photo_size):
            # 检查边界,避免索引越界
            check_x = x + photo_size//2
            check_y = y + photo_size//2
            if check_x < heart_size and check_y < heart_size:
                if mask.getpixel((check_x, check_y)) > 128:
                    valid_positions.append((x, y))
    
    log(f"找到{len(valid_positions)}个有效位置")
    
    # 随机打乱位置顺序
    random.shuffle(valid_positions)
    
    # 计算照片排列
    photo_count = 0
    img_index = 0
    
    # 确保所有图片至少使用一次
    used_images = set()
    
    # 定义图片处理函数(用于多线程)
    def process_image_task(img_path, size):
        try:
            img = Image.open(img_path)
            processed_img = process_image(img, size)
            return processed_img
        except Exception as e:
            log(f"处理图片{img_path}时出错: {e}")
            return None
    
    # 使用多线程并行处理所有图片
    log("开始并行处理图片...")
    
    # 首先处理所有需要至少使用一次的图片
    processed_images = []
    total_tasks = min(len(image_files), len(valid_positions))
    completed_tasks = 0
    
    with ThreadPoolExecutor(max_workers=8) as executor:
        # 提交所有任务
        future_to_img = {executor.submit(process_image_task, img_path, photo_size): img_path 
                       for img_path in image_files[:len(valid_positions)]}
        
        # 收集处理结果
        for future in as_completed(future_to_img):
            img_path = future_to_img[future]
            try:
                processed_img = future.result()
                if processed_img:
                    processed_images.append((img_path, processed_img))
                completed_tasks += 1
                # 更新进度
                if log_queue:
                    progress = int((completed_tasks / total_tasks) * 70)  # 70% 进度用于图片处理
                    log_queue.put({"progress": progress, "log": f"处理中... {completed_tasks}/{total_tasks}"})
            except Exception as e:
                log(f"获取结果时出错: {e}")
                completed_tasks += 1
    
    log(f"并行处理完成,成功处理{len(processed_images)}张图片")
    
    # 粘贴处理好的图片到输出
    log("开始粘贴图片到画布...")
    total_paste = len(valid_positions)
    pasted = 0
    
    # 首先使用所有图片至少一次
    for i, (x, y) in enumerate(valid_positions[:len(image_files)]):
        if i < len(processed_images):
            img_path, processed_img = processed_images[i]
            try:
                # 粘贴到输出图片
                output.paste(processed_img, (x, y))
                photo_count += 1
                pasted += 1
                # 记录使用的图片索引
                if i < len(image_files):
                    used_images.add(i)
                # 更新进度
                if log_queue:
                    progress = 70 + int((pasted / total_paste) * 30)  # 剩余30% 进度用于粘贴
                    log_queue.put({"progress": progress})
            except Exception as e:
                log(f"粘贴图片时出错: {e}")
    
    # 然后用剩余的位置循环使用所有图片
    for i, (x, y) in enumerate(valid_positions[len(image_files):]):
        img_idx = (i + len(image_files)) % len(image_files)
        if img_idx < len(processed_images):
            img_path, processed_img = processed_images[img_idx]
            try:
                # 粘贴到输出图片
                output.paste(processed_img, (x, y))
                photo_count += 1
                pasted += 1
                # 更新进度
                if log_queue:
                    progress = 70 + int((pasted / total_paste) * 30)
                    log_queue.put({"progress": progress})
            except Exception as e:
                log(f"粘贴图片时出错: {e}")
    
    # 验证所有图片都被使用
    if len(used_images) == len(image_files):
        log(f"成功使用了所有{len(image_files)}张图片")
    else:
        log(f"警告:只使用了{len(used_images)}张图片,还有{len(image_files) - len(used_images)}张未使用")
    
    # 保存输出图片
    log("保存输出图片...")
    # 根据格式和质量参数保存
    if output_format.lower() == 'jpg' or output_format.lower() == 'jpeg':
        # 保存为JPEG格式,使用指定质量
        output.save(output_path, 'JPEG', quality=quality, optimize=True)
    else:
        # 保存为PNG格式(无损)
        output.save(output_path, 'PNG', optimize=True)
    
    log(f"爱心拼贴画已保存到{output_path}")
    log(f"输出格式: {output_format.upper()}, 质量: {quality}")
    log(f"共使用了{photo_count}张照片")

def main():
    """主函数"""
    parser = argparse.ArgumentParser(description='创建爱心形状的照片拼贴画')
    parser.add_argument('--input', type=str, default=r'C:\Users\EDY\Downloads\QQ空间备份_905544506\Albums\生活\凯and娇', help='输入图片目录')
    parser.add_argument('--output', type=str, default='output/heart_collage.png', help='输出图片路径')
    parser.add_argument('--generate-test', type=int, default=0, help='生成测试图片的数量')
    parser.add_argument('--format', type=str, default='png', help='输出图片格式(png/jpg)')
    parser.add_argument('--quality', type=int, default=100, help='输出图片质量(1-100)')
    
    args = parser.parse_args()
    
    # 生成测试图片(如果需要)
    if args.generate_test > 0:
        generate_test_images(args.generate_test, 'test_images')
    
    # 创建爱心拼贴画(无上限分辨率,固定250×250像素照片大小)
    create_heart_collage(args.input, args.output, output_format=args.format, quality=args.quality)

def gui_main():
    """图形界面主函数"""
    import tkinter as tk
    from tkinter import filedialog, ttk
    import queue
    
    # 创建队列用于线程通信
    log_queue = queue.Queue()
    
    def browse_input():
        """浏览输入文件夹"""
        folder = filedialog.askdirectory()
        if folder:
            input_var.set(folder)
    
    def browse_output():
        """浏览输出文件夹"""
        folder = filedialog.askdirectory()
        if folder:
            output_var.set(folder)
    
    def update_log():
        """更新日志和进度条"""
        while not log_queue.empty():
            message = log_queue.get_nowait()
            if isinstance(message, dict):
                if 'progress' in message:
                    progress_var.set(message['progress'])
                if 'log' in message:
                    log_text.insert(tk.END, message['log'] + '\n')
                    log_text.see(tk.END)
            else:
                log_text.insert(tk.END, message + '\n')
                log_text.see(tk.END)
        root.after(100, update_log)
    
    def generate():
        """生成爱心拼贴画"""
        input_dir = input_var.get().strip()
        output_dir = output_var.get().strip()
        
        # 验证输入
        if not input_dir:
            log_queue.put("错误: 请选择输入文件夹")
            return
        
        if not output_dir:
            log_queue.put("错误: 请选择输出文件夹")
            return
        
        try:
            photo_size = int(photo_size_var.get())
        except ValueError:
            log_queue.put("错误: 照片大小必须是数字")
            return
        
        # 构建输出文件路径
        output_path = os.path.join(output_dir, "heart_collage.png")
        
        # 禁用生成按钮
        generate_button.config(state=tk.DISABLED)
        log_queue.put("开始生成爱心拼贴画...")
        
        def generate_thread():
            """在后台线程中生成拼贴画"""
            try:
                # 调用核心函数,并传入日志队列
                create_heart_collage(input_dir, output_path, photo_size=photo_size, log_queue=log_queue)
                
                log_queue.put(f"生成完成!输出到: {output_path}")
                log_queue.put({"progress": 100})
            except Exception as e:
                log_queue.put(f"错误: {str(e)}")
            finally:
                # 重新启用生成按钮
                generate_button.config(state=tk.NORMAL)
                log_queue.put("准备就绪")
        
        # 在后台线程中执行,避免阻塞GUI
        import threading
        thread = threading.Thread(target=generate_thread)
        thread.daemon = True
        thread.start()
    
    # 创建主窗口
    root = tk.Tk()
    root.title("爱心拼贴画生成器")
    root.geometry("900x600")
    root.resizable(True, True)
    
    # 创建样式
    style = ttk.Style()
    style.theme_use('clam')
    
    # 创建主框架
    main_frame = ttk.Frame(root, padding="20")
    main_frame.pack(fill=tk.BOTH, expand=True)
    
    # 左侧设置区
    left_frame = ttk.LabelFrame(main_frame, text="设置", padding="10")
    left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10))
    
    # 右侧日志区
    right_frame = ttk.LabelFrame(main_frame, text="日志和进度", padding="10")
    right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(10, 0))
    
    # 输入文件夹
    ttk.Label(left_frame, text="输入文件夹:").grid(row=0, column=0, sticky=tk.W, pady=5)
    input_var = tk.StringVar(value="")  # 初始值置空
    ttk.Entry(left_frame, textvariable=input_var, width=40).grid(row=0, column=1, pady=5)
    ttk.Button(left_frame, text="浏览", command=browse_input).grid(row=0, column=2, padx=5, pady=5)
    
    # 输出文件夹
    ttk.Label(left_frame, text="输出文件夹:").grid(row=1, column=0, sticky=tk.W, pady=5)
    output_var = tk.StringVar(value="")  # 初始值置空
    ttk.Entry(left_frame, textvariable=output_var, width=40).grid(row=1, column=1, pady=5)
    ttk.Button(left_frame, text="浏览", command=browse_output).grid(row=1, column=2, padx=5, pady=5)
    
    # 照片大小
    ttk.Label(left_frame, text="照片大小(像素):").grid(row=2, column=0, sticky=tk.W, pady=5)
    photo_size_var = tk.StringVar(value="250")
    ttk.Entry(left_frame, textvariable=photo_size_var, width=10).grid(row=2, column=1, sticky=tk.W, pady=5)
    
    # 生成按钮
    generate_button = ttk.Button(left_frame, text="生成爱心拼贴画", command=generate, style='Accent.TButton')
    generate_button.grid(row=3, column=0, columnspan=3, pady=20)
    
    # 日志文本框
    log_text = tk.Text(right_frame, wrap=tk.WORD, height=20)
    log_text.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
    
    # 进度条
    progress_var = tk.DoubleVar(value=0)
    progress_bar = ttk.Progressbar(right_frame, variable=progress_var, maximum=100)
    progress_bar.pack(fill=tk.X, pady=(0, 10))
    
    # 状态标签
    status_var = tk.StringVar(value="准备就绪")
    status_label = ttk.Label(right_frame, textvariable=status_var, relief=tk.SUNKEN, anchor=tk.W)
    status_label.pack(fill=tk.X, pady=(0, 10))
    
    # 添加样式
    style.configure('Accent.TButton', font=('Arial', 10, 'bold'))
    
    # 启动日志更新
    update_log()
    
    # 运行主循环
    root.mainloop()

if __name__ == '__main__':
    # 检测是否有命令行参数
    import sys
    if len(sys.argv) > 1:
        # 有命令行参数,使用命令行模式
        main()
    else:
        # 无命令行参数,使用图形界面
        gui_main()

欢迎转载,原文博主:https://blog.csdn.net/qq_43179428

相关推荐
Java后端的Ai之路2 小时前
【Python 教程14】- 网络编程
网络·python·php
郝学胜-神的一滴2 小时前
Python 列表 vs 数组:深入解析与最佳选择指南
开发语言·python·程序人生
ZH15455891312 小时前
Flutter for OpenHarmony Python学习助手实战:机器学习算法实现的实现
python·学习·flutter
“负拾捌”2 小时前
python + uniapp 结合腾讯云实现实时语音识别功能(WebSocket)
python·websocket·微信小程序·uni-app·大模型·腾讯云·语音识别
一个有梦有戏的人2 小时前
Python3基础:函数基础,解锁模块化编程新技能
后端·python
好家伙VCC12 小时前
### WebRTC技术:实时通信的革新与实现####webRTC(Web Real-TimeComm
java·前端·python·webrtc
前端玖耀里13 小时前
如何使用python的boto库和SES发送电子邮件?
python
serve the people13 小时前
python环境搭建 (十二) pydantic和pydantic-settings类型验证与解析
java·网络·python
小天源13 小时前
Error 1053 Error 1067 服务“启动后立即停止” Java / Python 程序无法后台运行 windows nssm注册器下载与报错处理
开发语言·windows·python·nssm·error 1053·error 1067