| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 |
- import cv2
- import numpy as np
- from datetime import datetime
- from PIL import Image, ImageDraw, ImageFont
- from typing import Tuple, Union, Dict
- import re
- class FryImageWrite:
- """
- 图片创建和文字添加类
- 支持英文和中文文字添加,以及基本的图形绘制
- """
- def __init__(self, width: int = 1920, height: int = 1080,
- background_color: Tuple[int, int, int] = (255, 255, 255)):
- """
- 初始化图片画布
-
- 参数:
- width: 图片宽度
- height: 图片高度
- background_color: 背景颜色,BGR格式
- """
- self.width = width
- self.height = height
- self.image = np.full((height, width, 3), background_color, dtype=np.uint8)
- def add_text(self, text: str, position: Tuple[int, int],
- font_size: int = 32, color: Tuple[int, int, int] = (0, 0, 0),
- font_path: str = "simhei.ttf", thickness: int = 2) -> None:
- """
- 统一的文字添加方法,自动判断中英文并对齐文字基线
-
- 参数:
- text: 文本内容
- position: 文本位置 (x, y)
- font_size: 字体大小
- color: 字体颜色 (B, G, R)
- font_path: 中文字体文件路径
- thickness: 英文字体粗细
- """
- x, y = position
- if self.is_chinese(text):
- # 中文文字处理
- try:
- # 创建临时PIL Image来计算文字尺寸
- font = ImageFont.truetype(font_path, font_size)
- # 获取文字的尺寸信息
- bbox = font.getbbox(text)
- # 调整y坐标,使文字基线对齐
- adjusted_y = y + bbox[3] - font_size
- self.add_text_cn(text, (x, y), font_size, color, font_path)
- except Exception as e:
- print(f"字体加载失败: {e}")
- self.add_text_cn(text, (x, y), font_size, color, font_path)
- else:
- # 英文文字处理
- font_scale = font_size / 32
- # OpenCV文字基准点在左下角,需要向上偏移一个字体高度
- # 估算字体高度并调整y坐标
- text_size = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX,
- font_scale, thickness)[0]
- adjusted_y = y + text_size[1] # 加上文字高度使基线对齐
- self.add_text_en(text, (x, adjusted_y), font_scale, color, thickness)
- def add_text_cn(self, text: str, position: Tuple[int, int],
- font_size: int = 32, color: Tuple[int, int, int] = (0, 0, 0),
- font_path: str = "simhei.ttf") -> None:
- """
- 添加中文文本
-
- 参数:
- text: 文本内容
- position: 文本位置 (x, y)
- font_size: 字体大小
- color: 字体颜色 (B, G, R)
- font_path: 字体文件路径
- """
- img_pil = Image.fromarray(cv2.cvtColor(self.image, cv2.COLOR_BGR2RGB))
- draw = ImageDraw.Draw(img_pil)
- try:
- font = ImageFont.truetype(font_path, font_size)
- except Exception as e:
- print(f"字体加载失败: {e}")
- font = ImageFont.load_default()
- color_rgb = (color[2], color[1], color[0])
- # 获取文字的bbox
- bbox = font.getbbox(text)
- # 计算文字的垂直中心位置
- text_height = bbox[3] - bbox[1]
- y_offset = text_height // 2
- x, y = position
- adjusted_position = (x, y - y_offset)
- draw.text(adjusted_position, text, font=font, fill=color_rgb)
- self.image = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
- def add_text_en(self, text: str, position: Tuple[int, int],
- font_scale: float = 1.0, color: Tuple[int, int, int] = (0, 0, 0),
- thickness: int = 2, font_face: int = cv2.FONT_HERSHEY_SIMPLEX) -> None:
- """
- 添加英文文本
- """
- # 获取文字大小
- text_size = cv2.getTextSize(text, font_face, font_scale, thickness)[0]
- x, y = position
- # 确保x和y是整数
- x = int(x)
- y = int(y)
- # 调整y坐标,使文字垂直居中
- adjusted_y = y - text_size[1] // 2
- # 确保adjusted_y也是整数
- adjusted_y = int(adjusted_y)
- # 使用整数坐标调用putText
- cv2.putText(self.image, text, (x, adjusted_y), font_face, font_scale,
- color, thickness, cv2.LINE_AA)
- def is_chinese(self, text: str) -> bool:
- """
- 判断字符串是否包含中文
-
- 参数:
- text: 需要判断的文本
-
- 返回:
- bool: 是否包含中文
- """
- pattern = re.compile(r'[\u4e00-\u9fff]')
- return bool(pattern.search(text))
- def add_dict_info(self, info_dict: Dict[str, str],
- start_position: Tuple[int, int] = (50, 50),
- line_spacing: int = 30,
- label_value_spacing: int = 150,
- font_size: int = 24,
- label_color: Tuple[int, int, int] = (0, 0, 0),
- value_color_map: Dict[str, Tuple[int, int, int]] = None) -> None:
- """
- 添加字典信息到图片
-
- 参数:
- info_dict: 信息字典,键为标签,值为内容
- start_position: 起始位置 (x, y)
- line_spacing: 行间距
- label_value_spacing: 标签和值之间的间距
- font_size: 字体大小
- label_color: 标签颜色
- value_color_map: 值的颜色映射字典,比如 {"PASS": (0, 255, 0), "FAIL": (0, 0, 255)}
- """
- if value_color_map is None:
- value_color_map = {}
- x, y = start_position
- for idx, (key, value) in enumerate(info_dict.items()):
- current_y = y + idx * line_spacing
- # 添加标签
- self.add_text(f"{key}:", (x, current_y), font_size, label_color)
- # 确定值的颜色
- value_color = value_color_map.get(str(value), label_color)
- # 添加值
- self.add_text(str(value), (x + label_value_spacing, current_y),
- font_size, value_color)
- def add_rectangle(self, start_point: Tuple[int, int],
- end_point: Tuple[int, int],
- color: Tuple[int, int, int] = (0, 0, 0),
- thickness: int = 2) -> None:
- """
- 添加矩形
-
- 参数:
- start_point: 起始点 (x, y)
- end_point: 结束点 (x, y)
- color: 颜色 (B, G, R)
- thickness: 线条粗细,-1表示填充
- """
- cv2.rectangle(self.image, start_point, end_point, color, thickness)
- def add_line(self, start_point: Tuple[int, int],
- end_point: Tuple[int, int],
- color: Tuple[int, int, int] = (0, 0, 0),
- thickness: int = 2) -> None:
- """
- 添加直线
-
- 参数:
- start_point: 起始点 (x, y)
- end_point: 结束点 (x, y)
- color: 颜色 (B, G, R)
- thickness: 线条粗细
- """
- cv2.line(self.image, start_point, end_point, color, thickness)
- def save_image(self, file_path: str) -> bool:
- """
- 保存图片
-
- 参数:
- file_path: 保存路径
-
- 返回:
- bool: 是否保存成功
- """
- try:
- cv2.imwrite(file_path, self.image)
- return True
- except Exception as e:
- print(f"保存图片失败: {e}")
- return False
- def get_image(self) -> np.ndarray:
- """
- 获取图片数组
-
- 返回:
- numpy.ndarray: 图片数组
- """
- return self.image.copy()
- def create_report(self, title: str, info_dict: Dict[str, str],
- value_color_map: Dict[str, Tuple[int, int, int]] = None) -> None:
- """
- 创建一个标准格式的报告
-
- 参数:
- title: 报告标题
- info_dict: 信息字典,键为标签,值为内容
- value_color_map: 值的颜色映射字典,比如 {"PASS": (0, 255, 0), "FAIL": (0, 0, 255)}
- """
- # 设置默认的颜色映射
- if value_color_map is None:
- value_color_map = {
- "PASS": (0, 255, 0), # 绿色
- "FAIL": (0, 0, 255), # 红色
- "Good": (0, 255, 0), # 绿色
- "Bad": (0, 0, 255) # 红色
- }
- # 计算边距和间距
- margin = min(self.width, self.height) // 20 # 动态边距
- content_width = self.width - 2 * margin
- content_height = self.height - 2 * margin
- # 计算标题大小和位置
- title_font_size = min(content_width // len(title) if len(title) > 0 else content_width,
- content_height // 8)
- title_font_size = min(72, max(36, title_font_size)) # 限制标题字体大小范围
- # 标题位置居中
- title_x = self.width // 2
- title_y = margin + title_font_size
- # 获取标题的大小来调整内容区域
- if self.is_chinese(title):
- font = ImageFont.truetype("simhei.ttf", title_font_size)
- title_width = font.getbbox(title)[2]
- else:
- title_width = cv2.getTextSize(title, cv2.FONT_HERSHEY_SIMPLEX,
- title_font_size / 32, 2)[0][0]
- # 添加标题(居中)
- self.add_text(title, (title_x - title_width // 2, title_y),
- title_font_size, (0, 0, 0))
- # 计算内容区域的位置和大小
- content_start_y = title_y + title_font_size + margin
- content_area_height = self.height - content_start_y - margin
- # 计算内容的字体大小和间距
- item_count = len(info_dict)
- font_size = min(32, max(18, int(content_area_height / (item_count * 2))))
- line_spacing = max(font_size * 1.8, min(content_area_height / item_count, 50))
- # 计算最长标签的宽度来决定label_value_spacing
- max_label_len = max(len(str(key)) for key in info_dict.keys())
- label_value_spacing = max(150, max_label_len * font_size // 2)
- # 添加边框
- border_margin = margin // 2
- self.add_rectangle(
- (border_margin, border_margin),
- (self.width - border_margin, self.height - border_margin),
- (0, 0, 0), 2
- )
- # 添加内容区域的分隔线
- self.add_line(
- (border_margin, title_y + title_font_size // 2 + margin // 2),
- (self.width - border_margin, title_y + title_font_size // 2 + margin // 2),
- (0, 0, 0), 1
- )
- # 添加信息字典
- self.add_dict_info(
- info_dict,
- start_position=(margin * 1.5, content_start_y),
- line_spacing=int(line_spacing),
- label_value_spacing=label_value_spacing,
- font_size=font_size,
- value_color_map=value_color_map
- )
- # 测试代码
- def test_fry_image_write():
- """
- 测试FryImageWrite类的各项功能
- """
- # 创建实例
- image_writer = FryImageWrite(800, 600)
- # 测试信息字典
- info_dict = {
- "产品型号": "ABC-123",
- "序列号": "SN20240101001",
- "检测时间": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
- "测试结果": "PASS",
- "Product": "Camera",
- "Status": "FAIL",
- "温度": "25摄氏度",
- "Quality": "Good"
- }
- # 定义值的颜色映射
- value_color_map = {
- "PASS": (0, 255, 0), # 绿色
- "FAIL": (0, 0, 255), # 红色
- "Good": (0, 255, 0) # 绿色
- }
- # 添加标题
- image_writer.add_text("测试报告", (300, 30), 48, (0, 0, 0))
- # 添加边框
- image_writer.add_rectangle((30, 80), (770, 580), (0, 0, 0), 2)
- # 添加信息字典
- image_writer.add_dict_info(
- info_dict,
- start_position=(50, 100),
- line_spacing=40,
- label_value_spacing=200,
- font_size=28,
- value_color_map=value_color_map
- )
- # 保存图片
- image_writer.save_image("test_output.jpg")
- def test_fry_image_write2():
- """
- 测试FryImageWrite类的各项功能
- """
- # 创建实例
- image_writer = FryImageWrite(1280, 800) # 使用更大的尺寸以获得更好的效果
- # 测试信息字典
- info_dict = {
- "产品型号": "ABC-123",
- "序列号": "SN20240101001",
- "检测时间": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
- "测试结果": "PASS",
- "Product": "Camera",
- "Status": "FAIL",
- "温度": "25摄氏度",
- "Quality": "Good"
- }
- # 一行代码创建完整报告
- image_writer.create_report("测试报告", info_dict)
- # 保存图片
- image_writer.save_image("test_output.jpg")
- # 运行测试
- if __name__ == "__main__":
- test_fry_image_write2()
|