fry_image_write_V03_250401.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. import cv2
  2. import numpy as np
  3. from datetime import datetime
  4. from PIL import Image, ImageDraw, ImageFont
  5. from typing import Tuple, Union, Dict
  6. import re
  7. class FryImageWrite:
  8. """
  9. 图片创建和文字添加类
  10. 支持英文和中文文字添加,以及基本的图形绘制
  11. """
  12. def __init__(self, width: int = 1920, height: int = 1080,
  13. background_color: Tuple[int, int, int] = (255, 255, 255)):
  14. """
  15. 初始化图片画布
  16. 参数:
  17. width: 图片宽度
  18. height: 图片高度
  19. background_color: 背景颜色,BGR格式
  20. """
  21. self.width = width
  22. self.height = height
  23. self.image = np.full((height, width, 3), background_color, dtype=np.uint8)
  24. def add_text(self, text: str, position: Tuple[int, int],
  25. font_size: int = 32, color: Tuple[int, int, int] = (0, 0, 0),
  26. font_path: str = "simhei.ttf", thickness: int = 2) -> None:
  27. """
  28. 统一的文字添加方法,自动判断中英文并对齐文字基线
  29. 参数:
  30. text: 文本内容
  31. position: 文本位置 (x, y)
  32. font_size: 字体大小
  33. color: 字体颜色 (B, G, R)
  34. font_path: 中文字体文件路径
  35. thickness: 英文字体粗细
  36. """
  37. x, y = position
  38. if self.is_chinese(text):
  39. # 中文文字处理
  40. try:
  41. # 创建临时PIL Image来计算文字尺寸
  42. font = ImageFont.truetype(font_path, font_size)
  43. # 获取文字的尺寸信息
  44. bbox = font.getbbox(text)
  45. # 调整y坐标,使文字基线对齐
  46. adjusted_y = y + bbox[3] - font_size
  47. self.add_text_cn(text, (x, y), font_size, color, font_path)
  48. except Exception as e:
  49. print(f"字体加载失败: {e}")
  50. self.add_text_cn(text, (x, y), font_size, color, font_path)
  51. else:
  52. # 英文文字处理
  53. font_scale = font_size / 32
  54. # OpenCV文字基准点在左下角,需要向上偏移一个字体高度
  55. # 估算字体高度并调整y坐标
  56. text_size = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX,
  57. font_scale, thickness)[0]
  58. adjusted_y = y + text_size[1] # 加上文字高度使基线对齐
  59. self.add_text_en(text, (x, adjusted_y), font_scale, color, thickness)
  60. def add_text_cn(self, text: str, position: Tuple[int, int],
  61. font_size: int = 32, color: Tuple[int, int, int] = (0, 0, 0),
  62. font_path: str = "simhei.ttf") -> None:
  63. """
  64. 添加中文文本
  65. 参数:
  66. text: 文本内容
  67. position: 文本位置 (x, y)
  68. font_size: 字体大小
  69. color: 字体颜色 (B, G, R)
  70. font_path: 字体文件路径
  71. """
  72. img_pil = Image.fromarray(cv2.cvtColor(self.image, cv2.COLOR_BGR2RGB))
  73. draw = ImageDraw.Draw(img_pil)
  74. try:
  75. font = ImageFont.truetype(font_path, font_size)
  76. except Exception as e:
  77. print(f"字体加载失败: {e}")
  78. font = ImageFont.load_default()
  79. color_rgb = (color[2], color[1], color[0])
  80. # 获取文字的bbox
  81. bbox = font.getbbox(text)
  82. # 计算文字的垂直中心位置
  83. text_height = bbox[3] - bbox[1]
  84. y_offset = text_height // 2
  85. x, y = position
  86. adjusted_position = (x, y - y_offset)
  87. draw.text(adjusted_position, text, font=font, fill=color_rgb)
  88. self.image = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
  89. def add_text_en(self, text: str, position: Tuple[int, int],
  90. font_scale: float = 1.0, color: Tuple[int, int, int] = (0, 0, 0),
  91. thickness: int = 2, font_face: int = cv2.FONT_HERSHEY_SIMPLEX) -> None:
  92. """
  93. 添加英文文本
  94. """
  95. # 获取文字大小
  96. text_size = cv2.getTextSize(text, font_face, font_scale, thickness)[0]
  97. x, y = position
  98. # 确保x和y是整数
  99. x = int(x)
  100. y = int(y)
  101. # 调整y坐标,使文字垂直居中
  102. adjusted_y = y - text_size[1] // 2
  103. # 确保adjusted_y也是整数
  104. adjusted_y = int(adjusted_y)
  105. # 使用整数坐标调用putText
  106. cv2.putText(self.image, text, (x, adjusted_y), font_face, font_scale,
  107. color, thickness, cv2.LINE_AA)
  108. def is_chinese(self, text: str) -> bool:
  109. """
  110. 判断字符串是否包含中文
  111. 参数:
  112. text: 需要判断的文本
  113. 返回:
  114. bool: 是否包含中文
  115. """
  116. pattern = re.compile(r'[\u4e00-\u9fff]')
  117. return bool(pattern.search(text))
  118. def add_dict_info(self, info_dict: Dict[str, str],
  119. start_position: Tuple[int, int] = (50, 50),
  120. line_spacing: int = 30,
  121. label_value_spacing: int = 150,
  122. font_size: int = 24,
  123. label_color: Tuple[int, int, int] = (0, 0, 0),
  124. value_color_map: Dict[str, Tuple[int, int, int]] = None) -> None:
  125. """
  126. 添加字典信息到图片
  127. 参数:
  128. info_dict: 信息字典,键为标签,值为内容
  129. start_position: 起始位置 (x, y)
  130. line_spacing: 行间距
  131. label_value_spacing: 标签和值之间的间距
  132. font_size: 字体大小
  133. label_color: 标签颜色
  134. value_color_map: 值的颜色映射字典,比如 {"PASS": (0, 255, 0), "FAIL": (0, 0, 255)}
  135. """
  136. if value_color_map is None:
  137. value_color_map = {}
  138. x, y = start_position
  139. for idx, (key, value) in enumerate(info_dict.items()):
  140. current_y = y + idx * line_spacing
  141. # 添加标签
  142. self.add_text(f"{key}:", (x, current_y), font_size, label_color)
  143. # 确定值的颜色
  144. value_color = value_color_map.get(str(value), label_color)
  145. # 添加值
  146. self.add_text(str(value), (x + label_value_spacing, current_y),
  147. font_size, value_color)
  148. def add_rectangle(self, start_point: Tuple[int, int],
  149. end_point: Tuple[int, int],
  150. color: Tuple[int, int, int] = (0, 0, 0),
  151. thickness: int = 2) -> None:
  152. """
  153. 添加矩形
  154. 参数:
  155. start_point: 起始点 (x, y)
  156. end_point: 结束点 (x, y)
  157. color: 颜色 (B, G, R)
  158. thickness: 线条粗细,-1表示填充
  159. """
  160. cv2.rectangle(self.image, start_point, end_point, color, thickness)
  161. def add_line(self, start_point: Tuple[int, int],
  162. end_point: Tuple[int, int],
  163. color: Tuple[int, int, int] = (0, 0, 0),
  164. thickness: int = 2) -> None:
  165. """
  166. 添加直线
  167. 参数:
  168. start_point: 起始点 (x, y)
  169. end_point: 结束点 (x, y)
  170. color: 颜色 (B, G, R)
  171. thickness: 线条粗细
  172. """
  173. cv2.line(self.image, start_point, end_point, color, thickness)
  174. def save_image(self, file_path: str) -> bool:
  175. """
  176. 保存图片
  177. 参数:
  178. file_path: 保存路径
  179. 返回:
  180. bool: 是否保存成功
  181. """
  182. try:
  183. cv2.imwrite(file_path, self.image)
  184. return True
  185. except Exception as e:
  186. print(f"保存图片失败: {e}")
  187. return False
  188. def get_image(self) -> np.ndarray:
  189. """
  190. 获取图片数组
  191. 返回:
  192. numpy.ndarray: 图片数组
  193. """
  194. return self.image.copy()
  195. def create_report(self, title: str, info_dict: Dict[str, str],
  196. value_color_map: Dict[str, Tuple[int, int, int]] = None) -> None:
  197. """
  198. 创建一个标准格式的报告
  199. 参数:
  200. title: 报告标题
  201. info_dict: 信息字典,键为标签,值为内容
  202. value_color_map: 值的颜色映射字典,比如 {"PASS": (0, 255, 0), "FAIL": (0, 0, 255)}
  203. """
  204. # 设置默认的颜色映射
  205. if value_color_map is None:
  206. value_color_map = {
  207. "PASS": (0, 255, 0), # 绿色
  208. "FAIL": (0, 0, 255), # 红色
  209. "Good": (0, 255, 0), # 绿色
  210. "Bad": (0, 0, 255) # 红色
  211. }
  212. # 计算边距和间距
  213. margin = min(self.width, self.height) // 20 # 动态边距
  214. content_width = self.width - 2 * margin
  215. content_height = self.height - 2 * margin
  216. # 计算标题大小和位置
  217. title_font_size = min(content_width // len(title) if len(title) > 0 else content_width,
  218. content_height // 8)
  219. title_font_size = min(72, max(36, title_font_size)) # 限制标题字体大小范围
  220. # 标题位置居中
  221. title_x = self.width // 2
  222. title_y = margin + title_font_size
  223. # 获取标题的大小来调整内容区域
  224. if self.is_chinese(title):
  225. font = ImageFont.truetype("simhei.ttf", title_font_size)
  226. title_width = font.getbbox(title)[2]
  227. else:
  228. title_width = cv2.getTextSize(title, cv2.FONT_HERSHEY_SIMPLEX,
  229. title_font_size / 32, 2)[0][0]
  230. # 添加标题(居中)
  231. self.add_text(title, (title_x - title_width // 2, title_y),
  232. title_font_size, (0, 0, 0))
  233. # 计算内容区域的位置和大小
  234. content_start_y = title_y + title_font_size + margin
  235. content_area_height = self.height - content_start_y - margin
  236. # 计算内容的字体大小和间距
  237. item_count = len(info_dict)
  238. font_size = min(32, max(18, int(content_area_height / (item_count * 2))))
  239. line_spacing = max(font_size * 1.8, min(content_area_height / item_count, 50))
  240. # 计算最长标签的宽度来决定label_value_spacing
  241. max_label_len = max(len(str(key)) for key in info_dict.keys())
  242. label_value_spacing = max(150, max_label_len * font_size // 2)
  243. # 添加边框
  244. border_margin = margin // 2
  245. self.add_rectangle(
  246. (border_margin, border_margin),
  247. (self.width - border_margin, self.height - border_margin),
  248. (0, 0, 0), 2
  249. )
  250. # 添加内容区域的分隔线
  251. self.add_line(
  252. (border_margin, title_y + title_font_size // 2 + margin // 2),
  253. (self.width - border_margin, title_y + title_font_size // 2 + margin // 2),
  254. (0, 0, 0), 1
  255. )
  256. # 添加信息字典
  257. self.add_dict_info(
  258. info_dict,
  259. start_position=(margin * 1.5, content_start_y),
  260. line_spacing=int(line_spacing),
  261. label_value_spacing=label_value_spacing,
  262. font_size=font_size,
  263. value_color_map=value_color_map
  264. )
  265. # 测试代码
  266. def test_fry_image_write():
  267. """
  268. 测试FryImageWrite类的各项功能
  269. """
  270. # 创建实例
  271. image_writer = FryImageWrite(800, 600)
  272. # 测试信息字典
  273. info_dict = {
  274. "产品型号": "ABC-123",
  275. "序列号": "SN20240101001",
  276. "检测时间": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
  277. "测试结果": "PASS",
  278. "Product": "Camera",
  279. "Status": "FAIL",
  280. "温度": "25摄氏度",
  281. "Quality": "Good"
  282. }
  283. # 定义值的颜色映射
  284. value_color_map = {
  285. "PASS": (0, 255, 0), # 绿色
  286. "FAIL": (0, 0, 255), # 红色
  287. "Good": (0, 255, 0) # 绿色
  288. }
  289. # 添加标题
  290. image_writer.add_text("测试报告", (300, 30), 48, (0, 0, 0))
  291. # 添加边框
  292. image_writer.add_rectangle((30, 80), (770, 580), (0, 0, 0), 2)
  293. # 添加信息字典
  294. image_writer.add_dict_info(
  295. info_dict,
  296. start_position=(50, 100),
  297. line_spacing=40,
  298. label_value_spacing=200,
  299. font_size=28,
  300. value_color_map=value_color_map
  301. )
  302. # 保存图片
  303. image_writer.save_image("test_output.jpg")
  304. def test_fry_image_write2():
  305. """
  306. 测试FryImageWrite类的各项功能
  307. """
  308. # 创建实例
  309. image_writer = FryImageWrite(1280, 800) # 使用更大的尺寸以获得更好的效果
  310. # 测试信息字典
  311. info_dict = {
  312. "产品型号": "ABC-123",
  313. "序列号": "SN20240101001",
  314. "检测时间": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
  315. "测试结果": "PASS",
  316. "Product": "Camera",
  317. "Status": "FAIL",
  318. "温度": "25摄氏度",
  319. "Quality": "Good"
  320. }
  321. # 一行代码创建完整报告
  322. image_writer.create_report("测试报告", info_dict)
  323. # 保存图片
  324. image_writer.save_image("test_output.jpg")
  325. # 运行测试
  326. if __name__ == "__main__":
  327. test_fry_image_write2()