| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586 |
- import math
- from pathlib import Path
- import cv2
- import numpy as np
- import os
- import time
- def fry_cv2_imread(filename, flags=cv2.IMREAD_COLOR):
- try:
- with open(filename, 'rb') as f:
- chunk = f.read()
- chunk_arr = np.frombuffer(chunk, dtype=np.uint8)
- img = cv2.imdecode(chunk_arr, flags)
- if img is None:
- error_info = f"Warning: Unable to decode image: {filename}"
- print("警告", error_info)
- return img
- except IOError as e:
- error_info = f"IOError: Unable to read file: {filename}"
- print("错误", error_info)
- print("错误", f"Error details: {str(e)}")
- return None
- def fry_cv2_imwrite(filename, img, params=None):
- try:
- ext = os.path.splitext(filename)[1].lower()
- result, encoded_img = cv2.imencode(ext, img, params)
- if result:
- with open(filename, 'wb') as f:
- encoded_img.tofile(f)
- return True
- else:
- print("警告", f"Warning: Unable to encode image: {filename}")
- return False
- except Exception as e:
- print("错误", f"Error: Unable to write file: {filename}")
- print("错误", f"Error details: {str(e)}")
- return False
- # 覆盖 OpenCV 的原始函数
- cv2.imread = fry_cv2_imread
- cv2.imwrite = fry_cv2_imwrite
- from fry_project_classes.blend_type_mixin import BlendTypeMixin
- class ImageStitcherTemplateMatch(BlendTypeMixin):
- def __init__(self, estimate_overlap_pixels=800, center_ratio=0.8,
- stitch_type="vertical",
- blend_type='half_importance',
- blend_ratio: float = 0.3,
- debug=False, debug_dir='debug_output',
- light_uniformity_compensation_enabled=False,
- light_uniformity_compensation_width=15,
- debug_draw_line_enabled=False
- ):
- """
- 初始化拼图器
-
- 参数:
- overlap_pixels: 重叠区域像素数,默认800像素(预估值)
- center_ratio: 中心区域比例,默认0.8
- debug: 是否开启调试模式,默认False
- debug_dir: 调试图片保存目录,默认'debug_output'
- use_weight_blend: 是否使用加权融合,默认True
- """
- print("警告", f"拼图方法:区域")
- self.estimate_overlap_pixels = estimate_overlap_pixels
- self.estimate_non_overlap_pixels = None
- self.center_ratio = center_ratio
- self.blend_ratio = blend_ratio
- self.blend_type = blend_type
- self.stitch_type = stitch_type
- self.debug_draw_line_enabled = debug_draw_line_enabled
- self.debug = debug
- self.init_debug = debug_dir
- self.light_uniformity_compensation_enabled = light_uniformity_compensation_enabled
- self.light_uniformity_compensation_width = light_uniformity_compensation_width
- if self.debug:
- self.debug_dir = f"{self.init_debug}_{self.stitch_type}_{self.blend_type}"
- os.makedirs(self.debug_dir, exist_ok=True)
- os.makedirs(self.debug_dir, exist_ok=True)
- # 模板匹配参数
- self.best_y = -1
- self.best_x = -1
- self.best_score = -1
- self.template_score = -1
- def save_debug_image(self, img, name, normalize=False):
- """
- 保存调试图片
-
- 参数:
- img: 要保存的图片
- name: 图片名称
- normalize: 是否需要归一化处理(对于模板匹配结果图等)
- """
- try:
- if self.debug:
- save_path = os.path.join(self.debug_dir, f"{name}.jpg")
- if normalize:
- # 归一化到0-255范围
- img_normalized = cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX)
- cv2.imwrite(save_path, img_normalized)
- else:
- cv2.imwrite(save_path, img)
- now_info = f"Debug: Saved {save_path}"
- # print("信息", now_info)
- return True, f"save_debug_image 成功: {save_path}"
- else:
- return False, "debug mode is not enabled"
- except Exception as e:
- error_info = f"save_debug_image出现bug: {str(e)}"
- print("警告", error_info)
- return False, error_info
- def visualize_template_match(self, template, search_region, best_y, best_x):
- """
- 可视化模板匹配结果
-
- 参数:
- template: 模板图片
- search_region: 搜索区域
- best_y, best_x: 最佳匹配位置
- """
- try:
- if self.debug:
- vis_img = search_region.copy()
- h, w = template.shape[:2]
- cv2.rectangle(vis_img, (best_x, best_y),
- (best_x + w, best_y + h), (0, 255, 0), 2)
- is_ok, msg = self.save_debug_image(vis_img, 'tp_140_template_match_visualization')
- return is_ok, msg
- else:
- return False, "debug mode is not enabled"
- except Exception as e:
- error_info = f"visualize_template_match 出现bug: {str(e)}"
- print("警告", error_info)
- return False, msg
- def split_image(self, img, is_left_top=True):
- """
- 分割图片为重叠区域和非重叠区域
- """
- height, width = img.shape[:2]
- overlap_width = min(self.estimate_overlap_pixels, width // 2)
- non_overlap_width = width - overlap_width
- if is_left_top:
- non_overlap_region = img[:, :non_overlap_width]
- overlap_region = img[:, non_overlap_width:]
- if self.debug:
- self.save_debug_image(img, 'split_220_left_top_original')
- self.save_debug_image(non_overlap_region, 'split_240_left_top_non_overlap')
- self.save_debug_image(overlap_region, 'split_260_left_top_overlap')
- else:
- overlap_region = img[:, :overlap_width]
- non_overlap_region = img[:, overlap_width:]
- if self.debug:
- self.save_debug_image(img, 'split_320_right_bottom_original')
- self.save_debug_image(overlap_region, 'split_340_right_bottom_overlap')
- self.save_debug_image(non_overlap_region, 'split_360_right_bottom_non_overlap')
- return overlap_region, non_overlap_region
- def get_center_region(self, img):
- """
- 获取图片的中心区域
- """
- height, width = img.shape[:2]
- margin_y = int(height * (1 - self.center_ratio) / 2)
- margin_x = int(width * (1 - self.center_ratio) / 2)
- center_region = img[margin_y:height - margin_y, margin_x:width - margin_x]
- if self.debug:
- self.save_debug_image(center_region, 'center_120_template_center_region')
- return center_region, margin_x, margin_y
- def template_matching(self, template, search_region):
- """
- 模板匹配
- """
- result = cv2.matchTemplate(search_region, template, cv2.TM_CCOEFF_NORMED)
- min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
- best_y, best_x = max_loc[1], max_loc[0]
- now_info = f"匹配结果: 最大值{max_val}, 位置{best_x, best_y}"
- print("警告", now_info)
- self.best_y = best_y
- self.best_x = best_x
- self.best_score = max_val
- self.template_score = max_val
- # 如果存在父对象且父对象有 update_match_score 方法,发送匹配分数
- # if hasattr(self, 'parent') and hasattr(self.parent, 'update_match_score'):
- # self.parent.update_match_score(max_val)
- if self.debug:
- is_ok1, opt_msg1 = self.save_debug_image(result, 'tp_120_template_matching_result', normalize=True)
- is_ok2, opt_msg2 = self.visualize_template_match(template, search_region, best_y, best_x)
- is_ok3, opt_msg3 = self.save_debug_image(template, 'tp_220_template')
- is_ok4, opt_msg4 = self.save_debug_image(search_region, 'tp_240_search_region')
- with open(os.path.join(self.debug_dir, 'tp_320_matching_info.txt'), 'w') as f:
- f.write(f"Best match position: ({best_x}, {best_y})\n")
- f.write(f"Match score: {max_val}\n")
- return best_x, best_y, max_val
- def pad_image(self, img: np.ndarray, target_width: int = None, target_height: int = None) -> np.ndarray:
- """
- 将图片填充到目标尺寸
- 参数:
- img: 输入图片
- target_width: 目标宽度,如果为None则保持原宽度
- target_height: 目标高度,如果为None则保持原高度
- 返回:
- 填充后的图片
- """
- if self.debug:
- self.save_debug_image(img, 'pad_120_before_padding')
- current_height, current_width = img.shape[:2]
- target_width = target_width if target_width is not None else current_width
- target_height = target_height if target_height is not None else current_height
- if current_width == target_width and current_height == target_height:
- return img
- # 创建黑色背景
- padded_img = np.zeros((target_height, target_width, 3), dtype=np.uint8)
- # 将原图放在中心位置
- y_offset = (target_height - current_height) // 2
- x_offset = (target_width - current_width) // 2
- padded_img[y_offset:y_offset + current_height,
- x_offset:x_offset + current_width] = img
- if self.debug:
- self.save_debug_image(padded_img, 'pad_220_after_padding')
- with open(os.path.join(self.debug_dir, 'pad_320_padding_info.txt'), 'w') as f:
- f.write(f"Original size: {current_width}x{current_height}\n")
- f.write(f"Target size: {target_width}x{target_height}\n")
- f.write(f"Padding offsets: x={x_offset}, y={y_offset}\n")
- return padded_img
- def stitch_horizontal(self, left_img, right_img):
- """
- 水平拼接两张图片
- 水平拼接的图片的高必须一样
- """
- # self.debug_dir = f"{self.init_debug}_horizontal_{self.blend_type}"
- # os.makedirs(self.debug_dir, exist_ok=True)
- # 确保两张图片高度相同
- max_height = max(left_img.shape[0], right_img.shape[0])
- left_img = self.pad_image(left_img, target_height=max_height)
- right_img = self.pad_image(right_img, target_height=max_height)
- # 1. 分割图片,使用预估的重叠像素
- left_overlap, left_non_overlap = self.split_image(left_img, is_left_top=True)
- right_overlap, right_non_overlap = self.split_image(right_img, is_left_top=False)
- # 2. 获取左图重叠区域的中心部分作为模板
- template, template_offset_x, template_offset_y = self.get_center_region(left_overlap)
- # 计算模板在左图中的位置
- self.estimate_non_overlap_pixels = left_img.shape[1] - self.estimate_overlap_pixels
- template_in_left_x = self.estimate_non_overlap_pixels + template_offset_x
- template_in_left_y = template_offset_y
- # 3. 在右图重叠区域中进行模板匹配
- best_x, best_y, max_val = self.template_matching(template, right_overlap)
- template_score = max_val
- # 计算模板在右图中的位置
- template_in_right_x = best_x
- template_in_right_y = best_y
- # 真实的重叠区域的x和y 这个还不好算
- # 04、计算最后拼接的图片的尺寸
- left_width_contribution = template_in_left_x # 左图取到模板的左上角,不包含模板
- right_width_contribution = right_img.shape[1] - template_in_right_x # 右图取到模板的左上角,包含模板
- stitch_img_width = left_width_contribution + right_width_contribution
- stitch_img_height = max(left_img.shape[0], right_img.shape[0])
- # 计算右图相对于左图的y方向的偏移量
- y_offset_right2left = template_in_left_y - template_in_right_y
- # 计算真正的重叠区域:右图的左边+模板宽度+左图的右边
- real_overlap_width = template_in_right_x + template.shape[1] + (
- left_img.shape[1] - template_in_left_x - template.shape[1])
-
- if self.debug:
- with open(os.path.join(self.debug_dir, 'h_320_alignment_info.txt'), 'w') as f:
- f.write(f"template_in_left_x: {template_in_left_x}\n")
- f.write(f"template_in_left_y: {template_in_left_y}\n")
- f.write(f"template_in_right_x: {template_in_right_x}\n")
- f.write(f"template_in_right_y: {template_in_right_y}\n")
- f.write(f"left_width_contribution: {left_width_contribution}\n")
- f.write(f"right_width_contribution: {right_width_contribution}\n")
- f.write(f"stitch_img_width: {stitch_img_width}\n")
- f.write(f"stitch_img_height: {stitch_img_height}\n")
- f.write(f"real_overlap_width: {real_overlap_width}\n")
- if self.blend_type == 'half_importance':
- blend_stitch_img = self.blend_half_importance(left_img, right_img, stitch_img_width, stitch_img_height,
- y_offset_right2left, real_overlap_width,
- light_uniformity_compensation=self.light_uniformity_compensation_enabled,
- light_uniformity_compensation_width=self.light_uniformity_compensation_width)
- if self.debug_draw_line_enabled:
- blend_stitch_img_visualize = self.blend_half_importance(left_img, right_img, stitch_img_width,
- stitch_img_height, y_offset_right2left,
- real_overlap_width, visualize=True)
- if self.debug:
- self.save_debug_image(blend_stitch_img_visualize, 'h_500_final_result_horizontal_visualize')
- elif self.blend_type == 'right_first':
- # 右边优先的拼接方式
- blend_stitch_img = self.blend_right_first(left_img, right_img, stitch_img_width, stitch_img_height,
- y_offset_right2left)
- elif self.blend_type == 'left_first':
- blend_stitch_img = self.blend_left_first(left_img, right_img,
- stitch_img_width,
- stitch_img_height,
- y_offset_right2left,
- real_overlap_width)
- elif self.blend_type == 'half_importance_add_weight':
- blend_stitch_img = self.blend_half_importance_add_weight(left_img, right_img, stitch_img_width,
- stitch_img_height, y_offset_right2left,
- real_overlap_width,
- blend_ratio=self.blend_ratio)
- elif self.blend_type == 'half_importance_global_brightness':
- blend_stitch_img = self.blend_half_importance_global_brightness(left_img, right_img, stitch_img_width,
- stitch_img_height, y_offset_right2left,
- real_overlap_width,
- light_uniformity_compensation=self.light_uniformity_compensation_enabled,
- light_uniformity_compensation_width=self.light_uniformity_compensation_width)
- if self.debug_draw_line_enabled:
- blend_stitch_img_visualize = self.blend_half_importance_global_brightness(left_img, right_img,
- stitch_img_width,
- stitch_img_height,
- y_offset_right2left,
- real_overlap_width,
- visualize=True)
- if self.debug:
- self.save_debug_image(blend_stitch_img_visualize, 'h_500_final_result_horizontal_visualize')
- elif self.blend_type == 'half_importance_partial_brightness':
- blend_stitch_img = self.blend_half_importance_partial_brightness(left_img, right_img, stitch_img_width,
- stitch_img_height, y_offset_right2left,
- real_overlap_width,
- light_uniformity_compensation=self.light_uniformity_compensation_enabled,
- light_uniformity_compensation_width=self.light_uniformity_compensation_width)
- if self.debug_draw_line_enabled:
- blend_stitch_img_visualize = self.blend_half_importance_partial_brightness(left_img, right_img,
- stitch_img_width,
- stitch_img_height,
- y_offset_right2left,
- real_overlap_width,
- visualize=True)
- if self.debug:
- self.save_debug_image(blend_stitch_img_visualize, 'h_500_final_result_horizontal_visualize')
- elif self.blend_type == 'blend_half_importance_partial_HV':
- blend_stitch_img = self.blend_half_importance_partial_HV(left_img, right_img, stitch_img_width,
- stitch_img_height, y_offset_right2left,
- real_overlap_width,
- light_uniformity_compensation=self.light_uniformity_compensation_enabled,
- light_uniformity_compensation_width=self.light_uniformity_compensation_width)
- if self.debug_draw_line_enabled:
- blend_stitch_img_visualize = self.blend_half_importance_partial_HV(left_img, right_img,
- stitch_img_width, stitch_img_height,
- y_offset_right2left,
- real_overlap_width, visualize=True)
- if self.debug:
- self.save_debug_image(blend_stitch_img_visualize, 'h_500_final_result_horizontal_visualize')
- elif self.blend_type == 'blend_half_importance_partial_SV':
- blend_stitch_img = self.blend_half_importance_partial_SV(left_img, right_img, stitch_img_width,
- stitch_img_height, y_offset_right2left,
- real_overlap_width,
- light_uniformity_compensation=self.light_uniformity_compensation_enabled,
- light_uniformity_compensation_width=self.light_uniformity_compensation_width)
- if self.debug_draw_line_enabled:
- blend_stitch_img_visualize = self.blend_half_importance_partial_SV(left_img, right_img,
- stitch_img_width, stitch_img_height,
- y_offset_right2left,
- real_overlap_width, visualize=True)
- if self.debug:
- self.save_debug_image(blend_stitch_img_visualize, 'h_500_final_result_horizontal_visualize')
- elif self.blend_type == 'blend_half_importance_partial_HSV':
- blend_stitch_img = self.blend_half_importance_partial_HSV(left_img, right_img, stitch_img_width,
- stitch_img_height, y_offset_right2left,
- real_overlap_width,
- light_uniformity_compensation=self.light_uniformity_compensation_enabled,
- light_uniformity_compensation_width=self.light_uniformity_compensation_width)
- if self.debug_draw_line_enabled:
- blend_stitch_img_visualize = self.blend_half_importance_partial_HSV(left_img, right_img,
- stitch_img_width, stitch_img_height,
- y_offset_right2left,
- real_overlap_width, visualize=True)
- if self.debug:
- self.save_debug_image(blend_stitch_img_visualize, 'h_500_final_result_horizontal_visualize')
- elif self.blend_type == 'blend_half_importance_partial_brightness_add_weight':
- blend_stitch_img = self.blend_half_importance_partial_brightness_add_weight(
- left_img=left_img,
- right_img=right_img,
- stitch_img_width=stitch_img_width,
- stitch_img_height=stitch_img_height,
- y_offset_right2left=y_offset_right2left,
- real_overlap_width=real_overlap_width,
- light_uniformity_compensation=self.light_uniformity_compensation_enabled,
- light_uniformity_compensation_width=self.light_uniformity_compensation_width,
- add_weight_rate=self.blend_ratio
- )
- if self.debug_draw_line_enabled:
- blend_stitch_img_visualize = self.blend_half_importance_partial_brightness_add_weight(left_img=left_img,
- right_img=right_img,
- stitch_img_width=stitch_img_width,
- stitch_img_height=stitch_img_height,
- y_offset_right2left=y_offset_right2left,
- real_overlap_width=real_overlap_width,
- add_weight_rate=self.blend_ratio,
- visualize=True)
- if self.debug:
- self.save_debug_image(blend_stitch_img_visualize, 'h_500_final_result_horizontal_visualize')
- else:
- # 左边优先的拼接方式
- blend_stitch_img = None
- if self.debug:
- self.save_debug_image(blend_stitch_img, 'h_500_final_result_horizontal')
- # return result
- return blend_stitch_img
- def stitch_vertical(self, top_img, bottom_img):
- """
- 垂直拼接两张图片
- 垂直拼接的图片的宽必须一样
- """
- # self.debug_dir = f"{self.init_debug}_vertical_{self.blend_type}"
- # os.makedirs(self.debug_dir, exist_ok=True)
- # 确保两张图片宽度相同
- max_width = max(top_img.shape[1], bottom_img.shape[1])
- top_img = self.pad_image(top_img, target_width=max_width)
- bottom_img = self.pad_image(bottom_img, target_width=max_width)
- if self.debug:
- self.save_debug_image(top_img, 'v_120_top_original')
- self.save_debug_image(bottom_img, 'v_140_bottom_original')
- # 将图片逆时针旋转90度
- top_rotated = cv2.rotate(top_img, cv2.ROTATE_90_COUNTERCLOCKWISE)
- bottom_rotated = cv2.rotate(bottom_img, cv2.ROTATE_90_COUNTERCLOCKWISE)
- if self.debug:
- self.save_debug_image(top_rotated, 'v_220_top_rotated')
- self.save_debug_image(bottom_rotated, 'v_240_bottom_rotated')
- # 进行水平拼接
- result_rotated = self.stitch_horizontal(top_rotated, bottom_rotated)
- # 将结果顺时针旋转90度
- result = cv2.rotate(result_rotated, cv2.ROTATE_90_CLOCKWISE)
- if self.debug:
- self.save_debug_image(result, 'v_500_final_result_vertical')
- return result
- def stitch_main(self, left_img, right_img):
- if self.stitch_type == 'horizontal':
- # 确保两张图片高度相同
- max_height = max(left_img.shape[0], right_img.shape[0])
- left_img = self.pad_image(left_img, target_height=max_height)
- right_img = self.pad_image(right_img, target_height=max_height)
- return self.stitch_horizontal(left_img, right_img)
- elif self.stitch_type == 'vertical':
- # 确保两张图片宽度相同
- max_width = max(left_img.shape[1], right_img.shape[1])
- left_img = self.pad_image(left_img, target_width=max_width)
- right_img = self.pad_image(right_img, target_width=max_width)
- return self.stitch_vertical(left_img, right_img)
- else:
- raise ValueError('Invalid stitch type, must be one of horizontal or vertical')
- def main():
- timestamp = time.strftime("%Y%m%d_%H%M%S")
- root_path = r"D:\_241231_fry_gitlab\LA_ai_main_CV_OpenCV\740_project\_250115_Stitch_Image_TemplateMatch\test_images"
- root_path_obj = Path(root_path).absolute()
- stitch_type = "vertical"
- debug_dir_str = str(root_path_obj / f'debug_{timestamp}_{stitch_type}')
- debug_dir_obj = Path(debug_dir_str).absolute()
- estimate_overlap_ratio = 0.45
- estimate_overlap_pixels = int(round(1024 * estimate_overlap_ratio))
- # 使用加权融合
- # stitcher_weight = ImageStitcher(
- # estimate_overlap_pixels=estimate_overlap_pixels,
- # center_ratio=0.8,
- # debug=True,
- # debug_dir=debug_dir+"_weight",
- # use_weight_blend=True
- # )
- # 读取图片
- left_img_name = r"20250123_162407_0001.jpg"
- right_img_name = r"20250123_162409_0002.jpg"
- bottom_img_name = r"20250123_162422_0007.jpg"
- left_img_path = str(root_path_obj / left_img_name)
- right_img_path = str(root_path_obj / right_img_name)
- bottom_img_path = str(root_path_obj / bottom_img_name)
- left_img = cv2.imread(left_img_path)
- right_img = cv2.imread(right_img_path)
- top_img = left_img
- bottom_img = cv2.imread(bottom_img_path)
- start_time = time.time()
- # 使用简单拼接
- stitcher_simple = ImageStitcherTemplateMatch(
- estimate_overlap_pixels=estimate_overlap_pixels,
- center_ratio=0.8,
- blend_type='half_importance_add_weight',
- # blend_type='left_first',
- stitch_type=stitch_type,
- blend_ratio=0.5,
- debug=True,
- debug_dir=debug_dir_str,
- )
- # 竖直拼接 - 简单拼接
- result_img = stitcher_simple.stitch_main(left_img, bottom_img)
- save_final_image_path = str(debug_dir_obj / 'result_img.jpg')
- cv2.imwrite(save_final_image_path, result_img)
- end_time = time.time()
- print(f"拼接图片耗时:{end_time - start_time:.2f}秒")
- if __name__ == '__main__':
- main()
|