utils.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. """
  2. 工具函数模块
  3. 包含图片处理、JSON解析等通用工具函数
  4. """
  5. import os
  6. import json
  7. from PIL import Image
  8. def parse_json_output(ai_response_text):
  9. """
  10. 尝试将AI的回复解析为JSON对象
  11. Args:
  12. ai_response_text: AI返回的字符串
  13. Returns:
  14. (success, data):
  15. success: 是否解析成功
  16. data: 解析后的数据字典,如果失败则包含error字段
  17. """
  18. try:
  19. # 1. 清洗数据:有时候 AI 会不听话地加上 ```json ... ```,这里做个简单的清洗
  20. cleaned_text = ai_response_text.strip()
  21. if cleaned_text.startswith("```"):
  22. cleaned_text = cleaned_text.replace("```json", "").replace("```", "")
  23. # 2. 尝试解析
  24. data = json.loads(cleaned_text)
  25. # 3. 验证关键字段是否存在
  26. if "success" in data and "prompt" in data:
  27. return True, data
  28. else:
  29. return False, {"error": "JSON格式合法,但缺少 success 或 prompt 字段"}
  30. except json.JSONDecodeError as e:
  31. return False, {"error": f"JSON解析失败: {str(e)}", "raw_text": ai_response_text}
  32. def resize_proportional(image, target_width, target_height):
  33. """
  34. 按照目标尺寸等比例缩放图片,保持宽高比
  35. 计算长和宽的比例,取较小值以确保图片完全适应目标尺寸
  36. Args:
  37. image: PIL Image 对象
  38. target_width: 目标宽度
  39. target_height: 目标高度
  40. Returns:
  41. 缩放后的 PIL Image 对象
  42. """
  43. width, height = image.size
  44. # 计算宽度和高度分别的比例
  45. width_ratio = target_width / width
  46. height_ratio = target_height / height
  47. # 取较小的比例,确保图片不会超出目标尺寸,同时保持宽高比
  48. ratio = min(width_ratio, height_ratio)
  49. new_width = int(width * ratio)
  50. new_height = int(height * ratio)
  51. return image.resize((new_width, new_height), Image.Resampling.LANCZOS)
  52. def center_image_on_white_background(image, target_width, target_height):
  53. """
  54. 将图片居中放在白底图上
  55. Args:
  56. image: PIL Image 对象(需要放置的图片)
  57. target_width: 目标宽度
  58. target_height: 目标高度
  59. Returns:
  60. 居中放置在白底图上的 PIL Image 对象
  61. """
  62. # 创建白底图
  63. white_background = Image.new('RGB', (target_width, target_height), (255, 255, 255))
  64. # 计算居中位置
  65. img_width, img_height = image.size
  66. x_offset = (target_width - img_width) // 2
  67. y_offset = (target_height - img_height) // 2
  68. # 如果图片是 RGBA 模式,需要处理透明度
  69. if image.mode == 'RGBA':
  70. white_background.paste(image, (x_offset, y_offset), image)
  71. else:
  72. white_background.paste(image, (x_offset, y_offset))
  73. return white_background
  74. def prepare_second_image(second_image_path, first_image_size):
  75. """
  76. 准备第二张图片:生成白底图,按照第一张图的长宽比例等比例缩放并居中放置
  77. Args:
  78. second_image_path: 第二张图片路径
  79. first_image_size: 第一张图片的尺寸 (width, height)
  80. Returns:
  81. 处理后的 PIL Image 对象
  82. """
  83. # 读取第二张图片
  84. second_img = Image.open(second_image_path)
  85. # 转换为 RGB 模式(如果不是的话)
  86. if second_img.mode != 'RGB' and second_img.mode != 'RGBA':
  87. second_img = second_img.convert('RGB')
  88. target_width, target_height = first_image_size
  89. # 按照第一张图的长宽比例等比例缩放第二张图(保持宽高比)
  90. resized_second = resize_proportional(second_img, target_width, target_height)
  91. # 居中放在白底图上
  92. final_second = center_image_on_white_background(resized_second, target_width, target_height)
  93. return final_second
  94. def horizontal_concatenate_images(image1_path, image2_path, output_path=None):
  95. """
  96. 将两张图片横向拼接,第二张图会按照第一张图的尺寸进行调整
  97. Args:
  98. image1_path: 第一张图片路径(作为尺寸参考)
  99. image2_path: 第二张图片路径
  100. output_path: 输出图片路径,如果为None则自动生成
  101. Returns:
  102. 输出图片路径
  103. """
  104. # 读取第一张图片
  105. first_img = Image.open(image1_path)
  106. # 转换为 RGB 模式(如果不是的话)
  107. if first_img.mode != 'RGB' and first_img.mode != 'RGBA':
  108. first_img = first_img.convert('RGB')
  109. # 获取第一张图的尺寸
  110. first_width, first_height = first_img.size
  111. # 准备第二张图片
  112. second_img = prepare_second_image(image2_path, (first_width, first_height))
  113. # 横向拼接两张图片
  114. # 总宽度 = 第一张图宽度 + 第二张图宽度
  115. # 总高度 = 两张图中较高的那个
  116. total_width = first_width + second_img.width
  117. total_height = max(first_height, second_img.height)
  118. # 创建拼接后的图片
  119. concatenated = Image.new('RGB', (total_width, total_height), (255, 255, 255))
  120. # 粘贴第一张图(左侧)
  121. concatenated.paste(first_img, (0, 0))
  122. # 粘贴第二张图(右侧)
  123. concatenated.paste(second_img, (first_width, 0))
  124. draw = ImageDraw.Draw(concatenated)
  125. # A. 画一条红色的分割线
  126. line_width = max(2, int(total_width * 0.005)) # 根据图片大小动态调整线宽
  127. draw.line([(first_width, 0), (first_width, total_height)], fill="red", width=line_width)
  128. # B. 准备字体 (尝试加载系统字体,为了防止中文乱码,我们主要用英文标签,模型读英文完全没问题)
  129. # 字体大小动态设为图片宽度的 4%
  130. font_size = int(total_width * 0.04)
  131. try:
  132. # 尝试加载常见字体(Windows/Linux兼容性尝试)
  133. # 注意:如果你的环境是 Linux docker,可能需要指定具体的 ttf 路径,例如 /usr/share/fonts/...
  134. font = ImageFont.truetype("arial.ttf", font_size)
  135. except IOError:
  136. try:
  137. font = ImageFont.truetype("DejaVuSans.ttf", font_size)
  138. except IOError:
  139. # 如果都找不到,使用默认字体(通常很小,但总比报错好)
  140. print("⚠️ 未找到系统字体,使用默认字体,可能较小")
  141. font = ImageFont.load_default()
  142. # C. 定义标签文本 (用英文更稳妥,Qwen识别英文非常准)
  143. text_left = "REFERENCE (Original)"
  144. text_right = "GENERATED (Flat Lay)"
  145. # D. 绘制文字(带描边,防止背景色干扰)
  146. # 左侧文字位置
  147. draw.text((20, 20), text_left, fill="red", font=font, stroke_width=2, stroke_fill="white")
  148. # 右侧文字位置 (起始点是第一张图宽度 + 偏移量)
  149. draw.text((first_width + 20, 20), text_right, fill="red", font=font, stroke_width=2, stroke_fill="white")
  150. # 生成输出路径
  151. if output_path is None:
  152. base_name1 = os.path.splitext(os.path.basename(image1_path))[0]
  153. base_name2 = os.path.splitext(os.path.basename(image2_path))[0]
  154. output_dir = r"D:\线稿图\temp"
  155. output_path = os.path.join(output_dir, f"{base_name1}_{base_name2}_concatenated.jpg")
  156. # 保存拼接后的图片
  157. concatenated.save(output_path, quality=95)
  158. print(f"✅ 图片拼接完成: {output_path}")
  159. print(f" 第一张图尺寸: {first_width}x{first_height}")
  160. print(f" 第二张图原始尺寸: {Image.open(image2_path).size}")
  161. print(f" 第二张图处理后尺寸: {second_img.size}")
  162. print(f" 拼接后尺寸: {total_width}x{total_height}")
  163. return output_path
  164. if __name__ == "__main__":
  165. # 示例用法
  166. image1 = r"H:\data\线稿图\S1261A097.jpg"
  167. image2 = r"D:\线稿图\平面图2\S1261A097.jpg"
  168. if os.path.exists(image1) and os.path.exists(image2):
  169. horizontal_concatenate_images(image1, image2)
  170. else:
  171. print("请修改示例中的图片路径")