Quellcode durchsuchen

修复拼图bug, 升级接口

AnlaAnla vor 8 Monaten
Ursprung
Commit
404acfed87

+ 2 - 65
Test/API测试.py

@@ -7,8 +7,7 @@ from PIL import Image, ImageDraw
 # --- 配置 ---
 # 请确保你的 FastAPI 服务器正在运行,并修改此处的地址和端口
 BASE_URL = "http://127.0.0.1:7745/api"
-SINGLE_STITCH_URL = f"{BASE_URL}/stitch/"
-BATCH_STITCH_URL = f"{BASE_URL}/stitch/batch/"
+SINGLE_STITCH_URL = f"{BASE_URL}/stitch"
 
 
 # --- 测试函数 1:单个拼图接口 ---
@@ -69,69 +68,7 @@ def single_puzzle_api(zipfile_path):
 
 # --- 测试函数 2:批量拼图接口 ---
 
-def batch_puzzle_api(zipfile_path):
-    """
-    测试 /stitch/batch 接口 (批量拼图)
-    使用 点匹配法 (key_point)
-    """
-    print("--- 2. 开始测试: 批量拼图接口 (/stitch/batch) ---")
-
-    # 1. 准备请求数据 (这次我们测试 key_point 方法)
-    form_data = {
-        'method': 'template_match',
-        'num_cols': 4,
-        'num_rows': 6,
-        'overlap_h': 405,
-        'overlap_v': 440,
-        'tm_blend_type': 'half_importance_add_weight',
-        'tm_light_compensation': True,
-    }
-
-    try:
-        # 打开文件
-        with open(zipfile_path, "rb") as f:
-            files = {
-                'zip_file': (os.path.basename(zipfile_path), f, 'application/zip')
-            }
-            # 4. 发送 POST 请求
-            print("向服务器发送请求...")
-            response = requests.post(BATCH_STITCH_URL, data=form_data, files=files)  # 批量处理可能更耗时
-
-        # 5. 处理响应
-        print(f"服务器响应状态码: {response.status_code}")
-
-        if response.status_code == 200:
-            content_type = response.headers.get('content-type')
-            print(f"响应内容类型: {content_type}")
-
-            if 'application/zip' in content_type:
-                # 将返回的ZIP文件保存到本地
-                output_filename = "stitched_batch_result.zip"
-                with open(output_filename, "wb") as f:
-                    f.write(response.content)
-                print(f"✅ 成功! 包含拼接结果的ZIP包已保存为: {output_filename}")
-
-                # (可选) 解压并检查结果
-                try:
-                    extract_dir = "batch_results_unzipped"
-                    os.makedirs(extract_dir, exist_ok=True)
-                    with zipfile.ZipFile(output_filename, 'r') as zf:
-                        zf.extractall(extract_dir)
-                    print(f"  - 结果已自动解压到 '{extract_dir}' 文件夹,包含文件: {os.listdir(extract_dir)}")
-                except Exception as e:
-                    print(f"  - 解压返回的ZIP文件时出错: {e}")
-            else:
-                print(f"❌ 失败! 期望得到 'application/zip',但收到了 '{content_type}'")
-        else:
-            print(f"❌ 请求失败! 错误信息: {response.text}")
-
-    except requests.exceptions.RequestException as e:
-        print(f"❌ 请求异常! 无法连接到服务器: {e}")
-
-    print("-" * 40 + "\n")
-
 
 if __name__ == "__main__":
     # 依次运行两个测试函数
-    single_puzzle_api(r"C:\Code\ML\Project\StitchImageServer\temp\_250801_1043_0001.zip")
-    batch_puzzle_api(r"C:\Code\ML\Project\StitchImageServer\temp\Input.zip")
+    single_puzzle_api(r"C:\Code\ML\Project\StitchImageServer\temp\Input\front_0_1.zip")

+ 2 - 79
Test/API测试_v2.py

@@ -2,16 +2,13 @@ import time
 
 import requests
 import os
-import shutil
-import zipfile
-from PIL import Image, ImageDraw
+
 
 # --- 配置 ---
 # 请确保你的 FastAPI 服务器正在运行,并修改此处的地址和端口
 BASE_URL = "http://127.0.0.1:7745/api"  # 假设您的API前缀是 /api, 如果不是,请修改
 STITCH_API_PREFIX = "/stitch"
-SINGLE_FOLDER_URL = f"{BASE_URL}{STITCH_API_PREFIX}/from-folder"
-BATCH_FOLDER_URL = f"{BASE_URL}{STITCH_API_PREFIX}/batch/from-folder"
+SINGLE_FOLDER_URL = f"{BASE_URL}{STITCH_API_PREFIX}/folder"
 
 # 用于存放自动生成的测试图片的临时目录
 TEST_DATA_DIR = "temp_api_test_data"
@@ -85,82 +82,8 @@ def single_puzzle_from_folder_api(image_folder_path: str):
     print("-" * 50 + "\n")
 
 
-# --- 测试函数 2:新的批量拼图接口 (从文件夹上传, ZIP返回) ---
-def batch_puzzle_from_folder_api(image_folder_path: str):
-    """
-    测试 /stitch/batch/from-folder 接口 (批量拼图)
-    """
-    print(f"--- 2. 开始测试: 批量拼图接口 (单文件夹上传, ZIP返回) ---")
-    print(f"使用文件夹: {image_folder_path}")
-
-    # 1. 准备请求数据
-    output_filename_base = os.path.basename(image_folder_path)
-    form_data = {
-        'output_filename_base': output_filename_base,
-        'method': 'template_match',
-        'num_cols': 4,
-        'num_rows': 6,
-        'overlap_h': 405,
-        'overlap_v': 440,
-    }
-
-    # 2. 准备文件列表 (与单个接口的逻辑相同)
-    files_to_send = []
-    file_objects = []
-    try:
-        for filename in os.listdir(image_folder_path):
-            if filename.lower().endswith(('.png', '.jpg', '.jpeg')):
-                file_path = os.path.join(image_folder_path, filename)
-                f = open(file_path, 'rb')
-                file_objects.append(f)
-                files_to_send.append(('files', (filename, f, 'image/jpeg')))
-
-        if not files_to_send:
-            print("❌ 错误: 在文件夹中未找到可上传的图片。")
-            return
-
-        # 3. 发送 POST 请求
-        print(f"向服务器 {BATCH_FOLDER_URL} 发送 {len(files_to_send)} 个文件...")
-        response = requests.post(BATCH_FOLDER_URL, data=form_data, files=files_to_send, timeout=60)
-
-        # 4. 处理响应
-        print(f"服务器响应状态码: {response.status_code}")
-        if response.status_code == 200:
-            content_type = response.headers.get('content-type')
-            print(f"响应内容类型: {content_type}")
-            if 'application/zip' in content_type:
-                output_filename = f"stitched_batch_{output_filename_base}.zip"
-                with open(output_filename, "wb") as f:
-                    f.write(response.content)
-                print(f"✅ 成功! 包含拼接结果的ZIP包已保存为: {output_filename}")
-                # (可选) 解压并检查结果
-                try:
-                    extract_dir = "batch_results_unzipped"
-                    if os.path.exists(extract_dir):
-                        shutil.rmtree(extract_dir)
-                    os.makedirs(extract_dir, exist_ok=True)
-                    with zipfile.ZipFile(output_filename, 'r') as zf:
-                        zf.extractall(extract_dir)
-                    print(f"  - 结果已自动解压到 '{extract_dir}' 文件夹,包含文件: {os.listdir(extract_dir)}")
-                except Exception as e:
-                    print(f"  - 解压返回的ZIP文件时出错: {e}")
-            else:
-                print(f"❌ 失败! 期望得到 'application/zip',但收到了 '{content_type}'")
-        else:
-            print(f"❌ 请求失败! 错误信息: {response.text}")
-
-    except requests.exceptions.RequestException as e:
-        print(f"❌ 请求异常! 无法连接到服务器: {e}")
-    finally:
-        for f in file_objects:
-            f.close()
-
-    print("-" * 50 + "\n")
-
-
 if __name__ == "__main__":
     t1 = time.time()
     single_puzzle_from_folder_api(r"C:\Code\ML\Project\StitchImageServer\temp\Input\_250801_1043_0001")
-    # batch_puzzle_from_folder_api(test_folder_2)
     t2 = time.time()
     print("cost: ", t2 - t1)

+ 113 - 96
Test/key_point_test.py

@@ -1,3 +1,5 @@
+# --- START OF FILE key_point_test.py ---
+
 import cv2
 import os
 import time
@@ -5,8 +7,9 @@ from pathlib import Path
 import re
 from tqdm import tqdm
 
-# 导入您提供的拼接器类
+# 导入您提供的拼接器类和拼接顺序生成器
 from fry_project_classes.stitch_img_key_point import ImageStitcherKeyPoint
+from fry_project_classes.get_full_stitch_order import get_full_stitch_order
 
 
 def natural_sort_key(s):
@@ -16,97 +19,94 @@ def natural_sort_key(s):
     return [int(text) if text.isdigit() else text.lower() for text in re.split(r'(\d+)', str(s))]
 
 
+# --- 重构后的 stitch_img 函数 ---
 def stitch_img(IMAGE_DIR, OUTPUT_DIR, NUM_COLS: int, NUM_ROWS: int,
                ESTIMATE_OVERLAP_HORIZONTAL_PIXELS: int, ESTIMATE_OVERLAP_VERTICAL_PIXELS: int,
-               BLEND_TYPE: str, FeatureDetector: str,
-               DEBUG_MODE: bool):
-    OUTPUT_DIR.mkdir(exist_ok=True)  # 创建输出文件夹
-
-    # --- 2. 加载并排序图片 ---
+               BLEND_TYPE: str, FEATURE_DETECTOR: str, DEBUG_MODE: bool,
+               BLEND_RATIO: float, LIGHT_COMPENSATION: bool, LIGHT_COMPENSATION_WIDTH: int):
+    OUTPUT_DIR.mkdir(exist_ok=True)
 
     print("--- 图像拼接开始 ---")
     print(f"配置: {NUM_ROWS}行 x {NUM_COLS}列")
     print(f"图片目录: {IMAGE_DIR}")
     print(f"输出目录: {OUTPUT_DIR}")
     print(f"水平重叠预估: {ESTIMATE_OVERLAP_HORIZONTAL_PIXELS}px, 垂直重叠预估: {ESTIMATE_OVERLAP_VERTICAL_PIXELS}px")
-    print(f"融合模式: {BLEND_TYPE}, 特征检测器类型: {FeatureDetector}")
+    print(f"特征检测器: {FEATURE_DETECTOR}, 融合模式: {BLEND_TYPE}, 融合权重: {BLEND_RATIO}")
+    print(f"光照补偿: {'启用' if LIGHT_COMPENSATION else '禁用'}, 补偿宽度: {LIGHT_COMPENSATION_WIDTH}px")
 
-    # --- 2. 加载并排序图片 ---
+    # --- 1. 加载并排序所有图片 ---
     image_paths = sorted(list(IMAGE_DIR.glob("*.jpg")), key=natural_sort_key)
 
     if len(image_paths) != NUM_COLS * NUM_ROWS:
         print(f"错误: 找到 {len(image_paths)} 张图片, 但预期需要 {NUM_COLS * NUM_ROWS} 张。")
         return
 
-    # --- 3. 阶段一:水平拼接每一行 ---
-    stitched_rows = []
-    print("\n--- 阶段一: 水平拼接每一行 ---")
-
-    for i in tqdm(range(NUM_ROWS), desc="处理行"):
-        row_start_index = i * NUM_COLS
-        row_image_paths = image_paths[row_start_index: row_start_index + NUM_COLS]
-
-        # 加载行的第一张图片
-        current_row_image = cv2.imread(str(row_image_paths[0]))
-        if current_row_image is None:
-            print(f"错误: 无法读取图片 {row_image_paths[0]}")
-            continue
-
-        # 依次将该行的后续图片拼接到右侧
-        for j in range(1, NUM_COLS):
-            # 为每次拼接实例化一个新的Stitcher对象,以隔离调试文件夹
-            stitcher_h = ImageStitcherKeyPoint(
-                estimate_overlap_pixels=ESTIMATE_OVERLAP_HORIZONTAL_PIXELS,
-                stitch_type="horizontal",
-                blend_type=BLEND_TYPE,
-                feature_detector=FeatureDetector,
-                blend_ratio=0.5,
-                debug=DEBUG_MODE,
-                debug_dir=str(OUTPUT_DIR / f'debug_h_row{i + 1}_col{j}vs{j + 1}')
-            )
-
-            next_image = cv2.imread(str(row_image_paths[j]))
-            if next_image is None:
-                print(f"错误: 无法读取图片 {row_image_paths[j]}")
-                break
-
-            current_row_image = stitcher_h.stitch_main(current_row_image, next_image)
-
-        # 保存拼接好的行
-        row_output_path = OUTPUT_DIR / f"stitched_row_{i + 1}.jpg"
-        cv2.imwrite(str(row_output_path), current_row_image)
-        stitched_rows.append(current_row_image)
-        tqdm.write(f"第 {i + 1} 行拼接完成, 已保存至 {row_output_path}")
-
-    # --- 4. 阶段二:垂直拼接所有行 ---
-    print("\n--- 阶段二: 垂直拼接所有行 ---")
-    if not stitched_rows:
-        print("错误: 没有成功拼接的行,无法进行垂直拼接。")
-        return
-
-    final_image = stitched_rows[0]
-
-    for i in tqdm(range(1, NUM_ROWS), desc="拼接行"):
-        # 实例化垂直拼接器
-        stitcher_v = ImageStitcherKeyPoint(
-            estimate_overlap_pixels=ESTIMATE_OVERLAP_VERTICAL_PIXELS,
-            stitch_type="vertical",
+    # 将所有图片读入内存,并用一个字典存储
+    images_dict = {}
+    for i, path in enumerate(image_paths):
+        img = cv2.imread(str(path))
+        if img is None:
+            print(f"错误: 无法读取图片 {path}")
+            return
+        # 使用从 '1' 开始的字符串作为键
+        images_dict[str(i + 1)] = img
+
+    # --- 2. 获取拼接顺序 ---
+    full_stitch_order_dict = get_full_stitch_order(NUM_ROWS, NUM_COLS)
+    print(f"\n--- 获取到 {len(full_stitch_order_dict)} 步拼接指令 ---")
+
+    # --- 3. 按照指令集执行拼接 ---
+    final_image = None
+    progress_bar = tqdm(full_stitch_order_dict.items(), desc="执行拼接")
+
+    for step, (round_num, img1_name, img2_name, direction, result_name) in progress_bar:
+        progress_bar.set_description(f"步骤 {step}: {img1_name} + {img2_name} -> {result_name}")
+
+        img1 = images_dict[img1_name]
+        img2 = images_dict[img2_name]
+
+        # 根据方向选择重叠像素
+        overlap_pixels = 0
+        if direction == 'horizontal':
+            overlap_pixels = ESTIMATE_OVERLAP_HORIZONTAL_PIXELS
+        elif direction == 'vertical':
+            overlap_pixels = ESTIMATE_OVERLAP_VERTICAL_PIXELS
+        else:
+            raise ValueError(f"未知的拼接方向: {direction}")
+
+        # 每次都创建一个新的拼接器实例
+        stitcher = ImageStitcherKeyPoint(
+            estimate_overlap_pixels=overlap_pixels,
+            stitch_type=direction,
             blend_type=BLEND_TYPE,
-            feature_detector=FeatureDetector,
-            blend_ratio=0.5,
+            feature_detector=FEATURE_DETECTOR,
+            blend_ratio=BLEND_RATIO,
+            # 同样添加光照补偿参数,供底层的融合模块使用
+            light_uniformity_compensation_enabled=LIGHT_COMPENSATION,
+            light_uniformity_compensation_width=LIGHT_COMPENSATION_WIDTH,
             debug=DEBUG_MODE,
-            debug_dir=str(OUTPUT_DIR / f'debug_v_row{i}vs{i + 1}')
+            debug_dir=str(OUTPUT_DIR / f'debug_{result_name}')
         )
 
-        next_row_image = stitched_rows[i]
-        final_image = stitcher_v.stitch_main(final_image, next_row_image)
+        # 执行拼接
+        stitched_image = stitcher.stitch_main(img1, img2)
 
-    # --- 5. 保存最终结果 ---
-    final_output_path = OUTPUT_DIR / "final_stitched_image.jpg"
-    cv2.imwrite(str(final_output_path), final_image)
+        # 将新生成的图片存入字典,用于下一步拼接
+        images_dict[result_name] = stitched_image
+        final_image = stitched_image
 
-    print("\n--- 所有拼接任务完成!---")
-    print(f"最终的全景图已保存至: {final_output_path}")
+        if DEBUG_MODE:
+            intermediate_path = OUTPUT_DIR / f"intermediate_{result_name}.jpg"
+            cv2.imwrite(str(intermediate_path), stitched_image)
+
+    # --- 4. 保存最终结果 ---
+    if final_image is not None:
+        final_output_path = OUTPUT_DIR / "final_stitched_image.jpg"
+        cv2.imwrite(str(final_output_path), final_image)
+        print("\n--- 所有拼接任务完成!---")
+        print(f"最终的全景图已保存至: {final_output_path}")
+    else:
+        print("\n--- 拼接失败,没有生成最终图像 ---")
 
 
 def main():
@@ -116,45 +116,62 @@ def main():
     # --- 1. 配置参数 ---
 
     # 图片和输出目录设置
-    IMAGE_DIR = Path(r"C:\Code\ML\Project\StitchImageServer\temp\Input\_250801_1141_0029")
-    # OUTPUT_DIR = Path(r"C:\Code\ML\Project\StitchImageServer\temp\output")
+    IMAGE_DIR = Path(r"C:\Code\ML\Project\StitchImageServer\temp\input\front_0_1")
 
     # 拼图网格设置
     NUM_COLS = 4
     NUM_ROWS = 6
 
     # !!!关键拼接参数,您可能需要根据实际图片进行调整!!!
-    # 预估水平方向重叠的像素数。如果您的图片宽1920像素,重叠25%,则该值为 1920 * 0.25 ≈ 480
-    ESTIMATE_OVERLAP_HORIZONTAL_PIXELS = 500 * 4
-
-    # 预估垂直方向重叠的像素数。如果您的图片高1080像素,重叠25%,则该值为 1080 * 0.25 ≈ 270
-    ESTIMATE_OVERLAP_VERTICAL_PIXELS = 500 * 4
+    # 关键点匹配对这个参数不敏感,但它仍然用于界定初始搜索区域
+    ESTIMATE_OVERLAP_HORIZONTAL_PIXELS = 405
+    ESTIMATE_OVERLAP_VERTICAL_PIXELS = 440
 
+    # --- 新增和修改的参数,与原始项目对齐 ---
+    BLEND_RATIO = 0.5  # 融合权重,对 'half_importance_add_weight' 等模式有效
+    LIGHT_COMPENSATION = True  # 是否开启光照补偿
+    LIGHT_COMPENSATION_WIDTH = 15  # 光照补偿的计算宽度 (请根据原始项目调整)
 
-    blend_type_list = ["half_importance", "right_first", "left_first", "half_importance_add_weight"]
-    # BLEND_TYPE = 'blend_half_importance_partial_HSV'
+    # 可测试的融合模式列表
+    blend_type_list = ["half_importance_add_weight"]
 
+    # 可测试的特征检测器列表
+    feature_detector_list = ["sift", "orb", "akaze", "brisk", "combine"]
 
     # 是否开启调试模式(会生成大量中间过程图片,用于分析问题)
-    DEBUG_MODE = False
-
-    for i, BLEND_TYPE in enumerate(blend_type_list):
-        base_dir_path = r"C:\Code\ML\Project\StitchImageServer\temp\output"
-        img_dir_name = f"{i}_{BLEND_TYPE}"
-        OUTPUT_DIR = Path(os.path.join(base_dir_path, img_dir_name))
-
-        one_img_time = time.time()
-        stitch_img(IMAGE_DIR=IMAGE_DIR, OUTPUT_DIR=OUTPUT_DIR, NUM_COLS=NUM_COLS, NUM_ROWS=NUM_ROWS,
-                   ESTIMATE_OVERLAP_HORIZONTAL_PIXELS=ESTIMATE_OVERLAP_HORIZONTAL_PIXELS,
-                   ESTIMATE_OVERLAP_VERTICAL_PIXELS=ESTIMATE_OVERLAP_VERTICAL_PIXELS,
-                   BLEND_TYPE=BLEND_TYPE, FeatureDetector="combine",
-                   DEBUG_MODE=DEBUG_MODE)
-        print(f"{BLEND_TYPE}: {time.time() - one_img_time}")
-
+    DEBUG_MODE = True
+
+    for feature_detector in feature_detector_list:
+        for blend_type in blend_type_list:
+            base_dir_path = r"C:\Code\ML\Project\StitchImageServer\temp\output"
+            # 创建更详细的输出文件夹名
+            img_dir_name = f"keypoint_{feature_detector}_{blend_type}"
+            OUTPUT_DIR = Path(os.path.join(base_dir_path, img_dir_name))
+
+            print("\n" + "=" * 50)
+            print(f"开始测试配置: 检测器={feature_detector}, 融合模式={blend_type}")
+            print("=" * 50)
+
+            one_config_time = time.time()
+            stitch_img(
+                IMAGE_DIR=IMAGE_DIR,
+                OUTPUT_DIR=OUTPUT_DIR,
+                NUM_COLS=NUM_COLS,
+                NUM_ROWS=NUM_ROWS,
+                ESTIMATE_OVERLAP_HORIZONTAL_PIXELS=ESTIMATE_OVERLAP_HORIZONTAL_PIXELS,
+                ESTIMATE_OVERLAP_VERTICAL_PIXELS=ESTIMATE_OVERLAP_VERTICAL_PIXELS,
+                BLEND_TYPE=blend_type,
+                FEATURE_DETECTOR=feature_detector,
+                DEBUG_MODE=DEBUG_MODE,
+                BLEND_RATIO=BLEND_RATIO,
+                LIGHT_COMPENSATION=LIGHT_COMPENSATION,
+                LIGHT_COMPENSATION_WIDTH=LIGHT_COMPENSATION_WIDTH
+            )
+            print(f"配置 {img_dir_name} 完成, 耗时: {time.time() - one_config_time:.2f} 秒")
 
 
 if __name__ == '__main__':
     start_time = time.time()
     main()
     end_time = time.time()
-    print(f"\n总耗时: {end_time - start_time:.2f} 秒")
+    print(f"\n总耗时: {end_time - start_time:.2f} 秒")

+ 0 - 196
Test/key_point_多线程_test.py

@@ -1,196 +0,0 @@
-import cv2
-import os
-import time
-from pathlib import Path
-import re
-from tqdm import tqdm
-import concurrent.futures  # 导入并发库
-
-# 导入您提供的拼接器类
-from fry_project_classes.stitch_img_key_point import ImageStitcherKeyPoint
-
-
-def natural_sort_key(s):
-    """
-    提供自然排序的键,例如 '2.jpg' 会排在 '10.jpg' 之前。
-    """
-    return [int(text) if text.isdigit() else text.lower() for text in re.split(r'(\d+)', str(s))]
-
-
-# --- 新增:用于并行处理的"任务单元"函数 ---
-def stitch_single_row_keypoint(row_index, row_image_paths, stitch_params):
-    """
-    负责使用基于关键点的方法拼接单一一行的图片。这个函数将在独立的进程中运行。
-
-    Args:
-        row_index (int): 当前行的索引(从0开始)。
-        row_image_paths (list): 这一行所有图片的路径列表。
-        stitch_params (dict): 包含所有拼接所需参数的字典。
-
-    Returns:
-        tuple: 包含行索引和拼接完成的图像 (row_index, stitched_row_image)。
-    """
-    # 从参数字典中解包
-    NUM_COLS = len(row_image_paths)
-    OUTPUT_DIR = stitch_params['OUTPUT_DIR']
-    ESTIMATE_OVERLAP_HORIZONTAL_PIXELS = stitch_params['ESTIMATE_OVERLAP_HORIZONTAL_PIXELS']
-    BLEND_TYPE = stitch_params['BLEND_TYPE']
-    FeatureDetector = stitch_params['FeatureDetector']
-    DEBUG_MODE = stitch_params['DEBUG_MODE']
-
-    # 加载行的第一张图片
-    current_row_image = cv2.imread(str(row_image_paths[0]))
-    if current_row_image is None:
-        print(f"错误: 无法读取图片 {row_image_paths[0]}")
-        return row_index, None
-
-    # 依次将该行的后续图片拼接到右侧
-    for j in range(1, NUM_COLS):
-        stitcher_h = ImageStitcherKeyPoint(
-            estimate_overlap_pixels=ESTIMATE_OVERLAP_HORIZONTAL_PIXELS,
-            stitch_type="horizontal",
-            blend_type=BLEND_TYPE,
-            feature_detector=FeatureDetector,
-            blend_ratio=0.5,
-            debug=DEBUG_MODE,
-            debug_dir=str(OUTPUT_DIR / f'debug_h_row{row_index + 1}_col{j}vs{j + 1}')
-        )
-
-        next_image = cv2.imread(str(row_image_paths[j]))
-        if next_image is None:
-            print(f"错误: 无法读取图片 {row_image_paths[j]}")
-            return row_index, current_row_image
-
-        current_row_image = stitcher_h.stitch_main(current_row_image, next_image)
-
-    # 返回拼接结果和行索引,以便主进程能按正确顺序排列
-    return row_index, current_row_image
-
-
-# --- 优化后的主拼接函数 ---
-def stitch_img(IMAGE_DIR, OUTPUT_DIR, NUM_COLS: int, NUM_ROWS: int,
-               ESTIMATE_OVERLAP_HORIZONTAL_PIXELS: int, ESTIMATE_OVERLAP_VERTICAL_PIXELS: int,
-               BLEND_TYPE: str, FeatureDetector: str,
-               DEBUG_MODE: bool):
-    OUTPUT_DIR.mkdir(exist_ok=True)
-
-    print("--- 图像拼接开始 ---")
-    print(f"配置: {NUM_ROWS}行 x {NUM_COLS}列")
-    print(f"图片目录: {IMAGE_DIR}")
-    print(f"输出目录: {OUTPUT_DIR}")
-    print(f"水平重叠预估: {ESTIMATE_OVERLAP_HORIZONTAL_PIXELS}px, 垂直重叠预估: {ESTIMATE_OVERLAP_VERTICAL_PIXELS}px")
-    print(f"融合模式: {BLEND_TYPE}, 特征检测器类型: {FeatureDetector}")
-
-    image_paths = sorted(list(IMAGE_DIR.glob("*.jpg")), key=natural_sort_key)
-    if len(image_paths) != NUM_COLS * NUM_ROWS:
-        print(f"错误: 找到 {len(image_paths)} 张图片, 但预期需要 {NUM_COLS * NUM_ROWS} 张。")
-        return
-
-    # --- 3. 阶段一:并行水平拼接每一行 (核心优化点) ---
-    print("\n--- 阶段一: 并行水平拼接每一行 ---")
-
-    # 将所有固定参数打包成字典,方便传递给子进程
-    stitch_params = {
-        'OUTPUT_DIR': OUTPUT_DIR,
-        'ESTIMATE_OVERLAP_HORIZONTAL_PIXELS': ESTIMATE_OVERLAP_HORIZONTAL_PIXELS,
-        'BLEND_TYPE': BLEND_TYPE,
-        'FeatureDetector': FeatureDetector,
-        'DEBUG_MODE': DEBUG_MODE
-    }
-
-    stitched_rows = [None] * NUM_ROWS  # 预分配列表,用于按顺序存放结果
-
-    with concurrent.futures.ProcessPoolExecutor() as executor:
-        futures = []
-        for i in range(NUM_ROWS):
-            row_start_index = i * NUM_COLS
-            row_image_paths = image_paths[row_start_index: row_start_index + NUM_COLS]
-            future = executor.submit(stitch_single_row_keypoint, i, row_image_paths, stitch_params)
-            futures.append(future)
-
-        for future in tqdm(concurrent.futures.as_completed(futures), total=NUM_ROWS, desc="处理行"):
-            try:
-                row_index, result_image = future.result()
-                if result_image is not None:
-                    stitched_rows[row_index] = result_image
-                    row_output_path = OUTPUT_DIR / f"stitched_row_{row_index + 1}.jpg"
-                    cv2.imwrite(str(row_output_path), result_image)
-                    tqdm.write(f"第 {row_index + 1} 行拼接完成, 已保存至 {row_output_path}")
-                else:
-                    tqdm.write(f"第 {row_index + 1} 行拼接失败。")
-            except Exception as exc:
-                tqdm.write(f"一个行拼接任务生成了异常: {exc}")
-
-    if any(row is None for row in stitched_rows):
-        print("错误: 存在拼接失败的行,无法进行垂直拼接。")
-        return
-
-    # --- 4. 阶段二:垂直拼接所有行 (保持串行) ---
-    print("\n--- 阶段二: 垂直拼接所有行 ---")
-
-    final_image = stitched_rows[0]
-
-    for i in tqdm(range(1, NUM_ROWS), desc="拼接行"):
-        stitcher_v = ImageStitcherKeyPoint(
-            estimate_overlap_pixels=ESTIMATE_OVERLAP_VERTICAL_PIXELS,
-            stitch_type="vertical",
-            blend_type=BLEND_TYPE,
-            feature_detector=FeatureDetector,
-            blend_ratio=0.5,
-            debug=DEBUG_MODE,
-            debug_dir=str(OUTPUT_DIR / f'debug_v_row{i}vs{i + 1}')
-        )
-        next_row_image = stitched_rows[i]
-        final_image = stitcher_v.stitch_main(final_image, next_row_image)
-
-    # --- 5. 保存最终结果 ---
-    final_output_path = OUTPUT_DIR / "final_stitched_image.jpg"
-    cv2.imwrite(str(final_output_path), final_image)
-
-    print("\n--- 所有拼接任务完成!---")
-    print(f"最终的全景图已保存至: {final_output_path}")
-
-
-def main():
-    """
-    主执行函数
-    """
-    # --- 1. 配置参数 ---
-    IMAGE_DIR = Path(r"C:\Code\ML\Project\StitchImageServer\temp\Input\_250801_1146_0034")
-    NUM_COLS = 4
-    NUM_ROWS = 6
-    # 预估重叠像素
-    ESTIMATE_OVERLAP_HORIZONTAL_PIXELS = 405
-    ESTIMATE_OVERLAP_VERTICAL_PIXELS = 440
-
-    # 默认为 half_importance_add_weight 和 combine
-    blend_type_list = ['half_importance', 'right_first', "half_importance_add_weight"]
-    feature_list = ['sift', 'orb', 'brisk', 'combine']
-    DEBUG_MODE = False
-    for BLEND_TYPE in blend_type_list:
-        for i, feature_type in enumerate(feature_list):
-            base_dir_path = r"C:\Code\ML\Project\StitchImageServer\temp\key_output"  # 建议为keypoint方法用一个新目录
-            img_dir_name = f"{i}_{BLEND_TYPE}_{feature_type}"
-            OUTPUT_DIR = Path(os.path.join(base_dir_path, img_dir_name))
-
-            print("\n" + "=" * 80)
-            print(f"开始测试配置: {img_dir_name}")
-
-            one_img_time = time.time()
-            stitch_img(IMAGE_DIR=IMAGE_DIR, OUTPUT_DIR=OUTPUT_DIR, NUM_COLS=NUM_COLS, NUM_ROWS=NUM_ROWS,
-                       ESTIMATE_OVERLAP_HORIZONTAL_PIXELS=ESTIMATE_OVERLAP_HORIZONTAL_PIXELS,
-                       ESTIMATE_OVERLAP_VERTICAL_PIXELS=ESTIMATE_OVERLAP_VERTICAL_PIXELS,
-                       BLEND_TYPE=BLEND_TYPE, FeatureDetector=feature_type,
-                       DEBUG_MODE=DEBUG_MODE)
-
-            print(f"\n--- 单次配置完成 ---")
-            print(f"用时: {time.time() - one_img_time:.2f} 秒, 配置: {img_dir_name}")
-            print("=" * 80)
-
-
-if __name__ == '__main__':
-    start_time = time.time()
-    main()
-    end_time = time.time()
-
-    print(f"\n总耗时: {end_time - start_time:.2f} 秒")

+ 79 - 79
Test/template_match_test.py

@@ -4,6 +4,10 @@ import time
 from pathlib import Path
 import re
 from tqdm import tqdm
+import cv2
+# 导入您提供的拼接器类和拼接顺序生成器
+from fry_project_classes.stitch_img_template_match import ImageStitcherTemplateMatch
+from fry_project_classes.get_full_stitch_order import get_full_stitch_order
 
 # 导入您提供的拼接器类
 from fry_project_classes.stitch_img_template_match import ImageStitcherTemplateMatch
@@ -19,94 +23,89 @@ def natural_sort_key(s):
 def stitch_img(IMAGE_DIR, OUTPUT_DIR, NUM_COLS: int, NUM_ROWS: int,
                ESTIMATE_OVERLAP_HORIZONTAL_PIXELS: int, ESTIMATE_OVERLAP_VERTICAL_PIXELS: int,
                BLEND_TYPE: str, LIGHT_COMPENSATION: bool,
-               DEBUG_MODE: bool):
-    OUTPUT_DIR.mkdir(exist_ok=True)  # 创建输出文件夹
-
-    # --- 2. 加载并排序图片 ---
+               DEBUG_MODE: bool, BLEND_RATIO: float, LIGHT_COMPENSATION_WIDTH: int):
+    OUTPUT_DIR.mkdir(exist_ok=True)
 
     print("--- 图像拼接开始 ---")
     print(f"配置: {NUM_ROWS}行 x {NUM_COLS}列")
     print(f"图片目录: {IMAGE_DIR}")
     print(f"输出目录: {OUTPUT_DIR}")
     print(f"水平重叠预估: {ESTIMATE_OVERLAP_HORIZONTAL_PIXELS}px, 垂直重叠预估: {ESTIMATE_OVERLAP_VERTICAL_PIXELS}px")
-    print(f"融合模式: {BLEND_TYPE}, 光照补偿: {'启用' if LIGHT_COMPENSATION else '禁用'}")
+    print(f"融合模式: {BLEND_TYPE}, 权重: {BLEND_RATIO}")
+    print(f"光照补偿: {'启用' if LIGHT_COMPENSATION else '禁用'}, 补偿宽度: {LIGHT_COMPENSATION_WIDTH}px")
 
-    # --- 2. 加载并排序图片 ---
+    # --- 1. 加载并排序所有图片 ---
     image_paths = sorted(list(IMAGE_DIR.glob("*.jpg")), key=natural_sort_key)
 
     if len(image_paths) != NUM_COLS * NUM_ROWS:
         print(f"错误: 找到 {len(image_paths)} 张图片, 但预期需要 {NUM_COLS * NUM_ROWS} 张。")
         return
 
-    # --- 3. 阶段一:水平拼接每一行 ---
-    stitched_rows = []
-    print("\n--- 阶段一: 水平拼接每一行 ---")
-
-    for i in tqdm(range(NUM_ROWS), desc="处理行"):
-        row_start_index = i * NUM_COLS
-        row_image_paths = image_paths[row_start_index: row_start_index + NUM_COLS]
-
-        # 加载行的第一张图片
-        current_row_image = cv2.imread(str(row_image_paths[0]))
-        if current_row_image is None:
-            print(f"错误: 无法读取图片 {row_image_paths[0]}")
-            continue
-
-        # 依次将该行的后续图片拼接到右侧
-        for j in range(1, NUM_COLS):
-            # 为每次拼接实例化一个新的Stitcher对象,以隔离调试文件夹
-            stitcher_h = ImageStitcherTemplateMatch(
-                estimate_overlap_pixels=ESTIMATE_OVERLAP_HORIZONTAL_PIXELS,
-                stitch_type="horizontal",
-                blend_type=BLEND_TYPE,
-                light_uniformity_compensation_enabled=LIGHT_COMPENSATION,
-                light_uniformity_compensation_width=30,  # 光照补偿的计算宽度
-                debug=DEBUG_MODE,
-                debug_dir=str(OUTPUT_DIR / f'debug_h_row{i + 1}_col{j}vs{j + 1}')
-            )
-
-            next_image = cv2.imread(str(row_image_paths[j]))
-            if next_image is None:
-                print(f"错误: 无法读取图片 {row_image_paths[j]}")
-                break
-
-            current_row_image = stitcher_h.stitch_main(current_row_image, next_image)
-
-        # 保存拼接好的行
-        row_output_path = OUTPUT_DIR / f"stitched_row_{i + 1}.jpg"
-        cv2.imwrite(str(row_output_path), current_row_image)
-        stitched_rows.append(current_row_image)
-        tqdm.write(f"第 {i + 1} 行拼接完成, 已保存至 {row_output_path}")
-
-    # --- 4. 阶段二:垂直拼接所有行 ---
-    print("\n--- 阶段二: 垂直拼接所有行 ---")
-    if not stitched_rows:
-        print("错误: 没有成功拼接的行,无法进行垂直拼接。")
-        return
-
-    final_image = stitched_rows[0]
-
-    for i in tqdm(range(1, NUM_ROWS), desc="拼接行"):
-        # 实例化垂直拼接器
-        stitcher_v = ImageStitcherTemplateMatch(
-            estimate_overlap_pixels=ESTIMATE_OVERLAP_VERTICAL_PIXELS,
-            stitch_type="vertical",
+    # 将所有图片读入内存,并用一个字典存储
+    images_dict = {}
+    for i, path in enumerate(image_paths):
+        img = cv2.imread(str(path))
+        if img is None:
+            print(f"错误: 无法读取图片 {path}")
+            return
+        # 使用从 '1' 开始的字符串作为键,模仿 stitch_worker.py 的行为
+        images_dict[str(i + 1)] = img
+
+    # --- 2. 获取拼接顺序 ---
+    full_stitch_order_dict = get_full_stitch_order(NUM_ROWS, NUM_COLS)
+    print(f"\n--- 获取到 {len(full_stitch_order_dict)} 步拼接指令 ---")
+
+    # --- 3. 按照指令集执行拼接 ---
+    final_image = None
+    progress_bar = tqdm(full_stitch_order_dict.items(), desc="执行拼接")
+
+    for step, (round_num, img1_name, img2_name, direction, result_name) in progress_bar:
+        progress_bar.set_description(f"步骤 {step}: {img1_name} + {img2_name} -> {result_name}")
+
+        img1 = images_dict[img1_name]
+        img2 = images_dict[img2_name]
+
+        # 根据方向选择重叠像素
+        overlap_pixels = 0
+        if direction == 'horizontal':
+            overlap_pixels = ESTIMATE_OVERLAP_HORIZONTAL_PIXELS
+        elif direction == 'vertical':
+            overlap_pixels = ESTIMATE_OVERLAP_VERTICAL_PIXELS
+        else:
+            raise ValueError(f"未知的拼接方向: {direction}")
+
+        # 每次都创建一个新的拼接器实例
+        stitcher = ImageStitcherTemplateMatch(
+            estimate_overlap_pixels=overlap_pixels,
+            stitch_type=direction,
             blend_type=BLEND_TYPE,
+            blend_ratio=BLEND_RATIO,
             light_uniformity_compensation_enabled=LIGHT_COMPENSATION,
-            light_uniformity_compensation_width=30,
+            light_uniformity_compensation_width=LIGHT_COMPENSATION_WIDTH,
             debug=DEBUG_MODE,
-            debug_dir=str(OUTPUT_DIR / f'debug_v_row{i}vs{i + 1}')
+            debug_dir=str(OUTPUT_DIR / f'debug_{result_name}')
         )
 
-        next_row_image = stitched_rows[i]
-        final_image = stitcher_v.stitch_main(final_image, next_row_image)
+        # 执行拼接
+        stitched_image = stitcher.stitch_main(img1, img2)
 
-    # --- 5. 保存最终结果 ---
-    final_output_path = OUTPUT_DIR / "final_stitched_image.jpg"
-    cv2.imwrite(str(final_output_path), final_image)
+        # 将新生成的图片存入字典,用于下一步拼接
+        images_dict[result_name] = stitched_image
+        final_image = stitched_image  # 始终保留最新的拼接结果
 
-    print("\n--- 所有拼接任务完成!---")
-    print(f"最终的全景图已保存至: {final_output_path}")
+        if DEBUG_MODE:
+            # 保存每一步的中间结果
+            intermediate_path = OUTPUT_DIR / f"intermediate_{result_name}.jpg"
+            cv2.imwrite(str(intermediate_path), stitched_image)
+
+    # --- 4. 保存最终结果 ---
+    if final_image is not None:
+        final_output_path = OUTPUT_DIR / "final_stitched_image.jpg"
+        cv2.imwrite(str(final_output_path), final_image)
+        print("\n--- 所有拼接任务完成!---")
+        print(f"最终的全景图已保存至: {final_output_path}")
+    else:
+        print("\n--- 拼接失败,没有生成最终图像 ---")
 
 
 def main():
@@ -116,7 +115,7 @@ def main():
     # --- 1. 配置参数 ---
 
     # 图片和输出目录设置
-    IMAGE_DIR = Path(r"C:\Code\ML\Project\StitchImageServer\temp\input\_250801_1142_0030")
+    IMAGE_DIR = Path(r"C:\Code\ML\Project\StitchImageServer\temp\input\front_0_1")
     # OUTPUT_DIR = Path(r"C:\Code\ML\Project\StitchImageServer\temp\output")
 
     # 拼图网格设置
@@ -126,9 +125,9 @@ def main():
     # !!!关键拼接参数,您可能需要根据实际图片进行调整!!!
     # 预估水平方向重叠的像素数。如果您的图片宽1920像素,重叠25%,则该值为 1920 * 0.25 ≈ 480
     # 预估垂直方向重叠的像素数。如果您的图片高1080像素,重叠25%,则该值为 1080 * 0.25 ≈ 270
-    estimate_overlap_ratio = 0.45
-    ESTIMATE_OVERLAP_HORIZONTAL_PIXELS = int(round(1024 * estimate_overlap_ratio))
-    ESTIMATE_OVERLAP_VERTICAL_PIXELS = int(round(1024 * estimate_overlap_ratio))
+    # estimate_overlap_ratio = 0.45
+    ESTIMATE_OVERLAP_HORIZONTAL_PIXELS = 405
+    ESTIMATE_OVERLAP_VERTICAL_PIXELS = 440
 
     # 选择融合模式。'blend_half_importance_partial_HSV' 是效果最好但最慢的模式之一
     '''
@@ -144,9 +143,10 @@ def main():
     '''
 
     blend_type_list = ["half_importance_add_weight",
-                       "half_importance_global_brightness", "half_importance_partial_brightness",
-                       "blend_half_importance_partial_HV", "blend_half_importance_partial_SV",
-                       "blend_half_importance_partial_HSV", "blend_half_importance_partial_brightness_add_weight"]
+                       # "half_importance_global_brightness", "half_importance_partial_brightness",
+                       # "blend_half_importance_partial_HV", "blend_half_importance_partial_SV",
+                       # "blend_half_importance_partial_HSV", "blend_half_importance_partial_brightness_add_weight"
+                       ]
     # BLEND_TYPE = 'blend_half_importance_partial_HSV'
 
     # 是否开启光照补偿(推荐开启以获得更好效果)
@@ -164,13 +164,13 @@ def main():
         stitch_img(IMAGE_DIR=IMAGE_DIR, OUTPUT_DIR=OUTPUT_DIR, NUM_COLS=NUM_COLS, NUM_ROWS=NUM_ROWS,
                    ESTIMATE_OVERLAP_HORIZONTAL_PIXELS=ESTIMATE_OVERLAP_HORIZONTAL_PIXELS,
                    ESTIMATE_OVERLAP_VERTICAL_PIXELS=ESTIMATE_OVERLAP_VERTICAL_PIXELS,
-                   BLEND_TYPE=BLEND_TYPE, LIGHT_COMPENSATION=LIGHT_COMPENSATION,
+                   BLEND_TYPE=BLEND_TYPE, BLEND_RATIO=0.5,
+                   LIGHT_COMPENSATION=LIGHT_COMPENSATION, LIGHT_COMPENSATION_WIDTH=15,
                    DEBUG_MODE=DEBUG_MODE)
         print()
-        print("_"*20)
+        print("_" * 20)
         print(f"单个用时: {BLEND_TYPE}: {time.time() - one_img_time}")
-        print("_"*20)
-
+        print("_" * 20)
 
 
 if __name__ == '__main__':

+ 0 - 210
Test/template_match_多线程_test.py

@@ -1,210 +0,0 @@
-import cv2
-import os
-import time
-from pathlib import Path
-import re
-from tqdm import tqdm
-import concurrent.futures
-
-from fry_project_classes.stitch_img_template_match import ImageStitcherTemplateMatch
-
-
-def natural_sort_key(s):
-    return [int(text) if text.isdigit() else text.lower() for text in re.split(r'(\d+)', str(s))]
-
-
-# --- 新增:用于并行处理的"任务单元"函数 ---
-def stitch_single_row(row_index, row_image_paths, stitch_params):
-    """
-    负责拼接单一一行的图片。这个函数将在独立的进程中运行。
-
-    Args:
-        row_index (int): 当前行的索引(从0开始),用于日志和调试文件命名。
-        row_image_paths (list): 这一行所有图片的路径列表。
-        stitch_params (dict): 包含所有拼接所需参数的字典。
-
-    Returns:
-        tuple: 包含行索引和拼接完成的图像 (row_index, stitched_row_image)。
-    """
-    # 从参数字典中解包
-    NUM_COLS = len(row_image_paths)
-    OUTPUT_DIR = stitch_params['OUTPUT_DIR']
-    ESTIMATE_OVERLAP_HORIZONTAL_PIXELS = stitch_params['ESTIMATE_OVERLAP_HORIZONTAL_PIXELS']
-    BLEND_TYPE = stitch_params['BLEND_TYPE']
-    LIGHT_COMPENSATION = stitch_params['LIGHT_COMPENSATION']
-    DEBUG_MODE = stitch_params['DEBUG_MODE']
-
-    # 加载行的第一张图片
-    current_row_image = cv2.imread(str(row_image_paths[0]))
-    if current_row_image is None:
-        print(f"错误: 无法读取图片 {row_image_paths[0]}")
-        return row_index, None
-
-    # 依次将该行的后续图片拼接到右侧
-    for j in range(1, NUM_COLS):
-        stitcher_h = ImageStitcherTemplateMatch(
-            estimate_overlap_pixels=ESTIMATE_OVERLAP_HORIZONTAL_PIXELS,
-            stitch_type="horizontal",
-            blend_type=BLEND_TYPE,
-            light_uniformity_compensation_enabled=LIGHT_COMPENSATION,
-            light_uniformity_compensation_width=30,
-            debug=DEBUG_MODE,
-            # 注意调试目录的命名,确保不同进程不会写入同一个文件夹
-            debug_dir=str(OUTPUT_DIR / f'debug_h_row{row_index + 1}_col{j}vs{j + 1}')
-        )
-
-        next_image = cv2.imread(str(row_image_paths[j]))
-        if next_image is None:
-            print(f"错误: 无法读取图片 {row_image_paths[j]}")
-            # 如果中间一张图片读取失败,返回当前已拼接的部分
-            return row_index, current_row_image
-
-        current_row_image = stitcher_h.stitch_main(current_row_image, next_image)
-
-    # 返回拼接结果和行索引,以便主进程能按正确顺序排列
-    return row_index, current_row_image
-
-
-# --- 优化后的主拼接函数 ---
-def stitch_img(IMAGE_DIR, OUTPUT_DIR, NUM_COLS: int, NUM_ROWS: int,
-               ESTIMATE_OVERLAP_HORIZONTAL_PIXELS: int, ESTIMATE_OVERLAP_VERTICAL_PIXELS: int,
-               BLEND_TYPE: str, LIGHT_COMPENSATION: bool,
-               DEBUG_MODE: bool):
-    OUTPUT_DIR.mkdir(exist_ok=True)
-
-    print("--- 图像拼接开始 ---")
-    print(f"配置: {NUM_ROWS}行 x {NUM_COLS}列")
-    print(f"图片目录: {IMAGE_DIR}")
-    print(f"输出目录: {OUTPUT_DIR}")
-    print(f"水平重叠预估: {ESTIMATE_OVERLAP_HORIZONTAL_PIXELS}px, 垂直重叠预估: {ESTIMATE_OVERLAP_VERTICAL_PIXELS}px")
-    print(f"融合模式: {BLEND_TYPE}, 光照补偿: {'启用' if LIGHT_COMPENSATION else '禁用'}")
-
-    # --- 2. 加载并排序图片 ---
-    image_paths = sorted(list(IMAGE_DIR.glob("*.jpg")), key=natural_sort_key)
-
-    if len(image_paths) != NUM_COLS * NUM_ROWS:
-        print(f"错误: 找到 {len(image_paths)} 张图片, 但预期需要 {NUM_COLS * NUM_ROWS} 张。")
-        return
-
-    # --- 3. 阶段一:并行水平拼接每一行 (核心优化点) ---
-    print("\n--- 阶段一: 并行水平拼接每一行 ---")
-
-    # 准备传递给每个进程的参数
-    stitch_params = {
-        'OUTPUT_DIR': OUTPUT_DIR,
-        'ESTIMATE_OVERLAP_HORIZONTAL_PIXELS': ESTIMATE_OVERLAP_HORIZONTAL_PIXELS,
-        'BLEND_TYPE': BLEND_TYPE,
-        'LIGHT_COMPENSATION': LIGHT_COMPENSATION,
-        'DEBUG_MODE': DEBUG_MODE
-    }
-
-    stitched_rows = [None] * NUM_ROWS  # 预先分配列表,用于按顺序存放结果
-
-    # 使用进程池执行器
-    with concurrent.futures.ProcessPoolExecutor() as executor:
-        # 提交所有行的拼接任务
-        futures = []
-        for i in range(NUM_ROWS):
-            row_start_index = i * NUM_COLS
-            row_image_paths = image_paths[row_start_index: row_start_index + NUM_COLS]
-            # 提交任务到进程池
-            future = executor.submit(stitch_single_row, i, row_image_paths, stitch_params)
-            futures.append(future)
-
-        # 使用tqdm来显示进度条,并收集结果
-        # as_completed会在任务完成时立即返回,这比直接等待所有任务更具响应性
-        for future in tqdm(concurrent.futures.as_completed(futures), total=NUM_ROWS, desc="处理行"):
-            try:
-                row_index, result_image = future.result()
-                if result_image is not None:
-                    stitched_rows[row_index] = result_image
-                    # 保存拼接好的行
-                    row_output_path = OUTPUT_DIR / f"stitched_row_{row_index + 1}.jpg"
-                    cv2.imwrite(str(row_output_path), result_image)
-                    tqdm.write(f"第 {row_index + 1} 行拼接完成, 已保存至 {row_output_path}")
-                else:
-                    tqdm.write(f"第 {row_index + 1} 行拼接失败。")
-            except Exception as exc:
-                tqdm.write(f"一个行拼接任务生成了异常: {exc}")
-
-    # 检查是否有失败的行
-    if any(row is None for row in stitched_rows):
-        print("错误: 存在拼接失败的行,无法进行垂直拼接。")
-        return
-
-    # --- 4. 阶段二:垂直拼接所有行 (这部分保持串行) ---
-    print("\n--- 阶段二: 垂直拼接所有行 ---")
-
-    final_image = stitched_rows[0]
-
-    for i in tqdm(range(1, NUM_ROWS), desc="拼接行"):
-        stitcher_v = ImageStitcherTemplateMatch(
-            estimate_overlap_pixels=ESTIMATE_OVERLAP_VERTICAL_PIXELS,
-            stitch_type="vertical",
-            blend_type=BLEND_TYPE,
-            light_uniformity_compensation_enabled=LIGHT_COMPENSATION,
-            light_uniformity_compensation_width=30,
-            debug=DEBUG_MODE,
-            debug_dir=str(OUTPUT_DIR / f'debug_v_row{i}vs{i + 1}')
-        )
-
-        next_row_image = stitched_rows[i]
-        final_image = stitcher_v.stitch_main(final_image, next_row_image)
-
-    # --- 5. 保存最终结果 ---
-    final_output_path = OUTPUT_DIR / "final_stitched_image.jpg"
-    cv2.imwrite(str(final_output_path), final_image)
-
-    print("\n--- 所有拼接任务完成!---")
-    print(f"最终的全景图已保存至: {final_output_path}")
-
-
-def main():
-    """
-    主执行函数
-    """
-    # --- 1. 配置参数 ---
-
-    # 图片和输出目录设置
-    IMAGE_DIR = Path(r"C:\Code\ML\Project\StitchImageServer\temp\Input\_250801_1146_0034")
-
-    # 拼图网格设置
-    NUM_COLS = 4
-    NUM_ROWS = 6
-
-    # 预估重叠像素
-    ESTIMATE_OVERLAP_HORIZONTAL_PIXELS = 405
-    ESTIMATE_OVERLAP_VERTICAL_PIXELS = 440
-
-    # 融合模式列表
-    # 默认 half_importance_add_weight
-    blend_type_list = ["half_importance_add_weight",
-                       "half_importance_global_brightness", "half_importance_partial_brightness",
-                       "blend_half_importance_partial_HV", "blend_half_importance_partial_SV",
-                       "blend_half_importance_partial_HSV", "blend_half_importance_partial_brightness_add_weight"]
-
-    LIGHT_COMPENSATION = True
-    DEBUG_MODE = False
-
-    for i, BLEND_TYPE in enumerate(blend_type_list):
-        base_dir_path = r"C:\Code\ML\Project\StitchImageServer\temp\output"
-        img_dir_name = f"{i}_{BLEND_TYPE}"
-        OUTPUT_DIR = Path(os.path.join(base_dir_path, img_dir_name))
-
-        one_img_time = time.time()
-        stitch_img(IMAGE_DIR=IMAGE_DIR, OUTPUT_DIR=OUTPUT_DIR, NUM_COLS=NUM_COLS, NUM_ROWS=NUM_ROWS,
-                   ESTIMATE_OVERLAP_HORIZONTAL_PIXELS=ESTIMATE_OVERLAP_HORIZONTAL_PIXELS,
-                   ESTIMATE_OVERLAP_VERTICAL_PIXELS=ESTIMATE_OVERLAP_VERTICAL_PIXELS,
-                   BLEND_TYPE=BLEND_TYPE, LIGHT_COMPENSATION=LIGHT_COMPENSATION,
-                   DEBUG_MODE=DEBUG_MODE)
-        print()
-        print("_" * 20)
-        print(f"单个用时: {img_dir_name}: {time.time() - one_img_time}")
-        print("_" * 20)
-
-
-if __name__ == '__main__':
-    start_time = time.time()
-    main()
-    end_time = time.time()
-    print(f"\n总耗时: {end_time - start_time:.2f} 秒")

+ 141 - 107
app/api/stitch.py

@@ -3,6 +3,7 @@ import shutil
 import uuid
 import zipfile
 import logging
+from typing import List
 from pathlib import Path
 
 from fastapi import APIRouter, UploadFile, File, Form, HTTPException, BackgroundTasks
@@ -11,7 +12,6 @@ from fastapi.responses import FileResponse, JSONResponse
 # 导入我们的核心逻辑和数据模型
 from app.core import stitcher_keypoint, stitcher_template
 from app.schemas import StitchingMethod, KeypointFeatureDetector, KeypointBlendType, TemplateBlendType
-
 from utils.utils import cleanup_temp_folder
 
 router = APIRouter(prefix="/stitch", tags=['拼图'])
@@ -21,34 +21,38 @@ TEMP_DIR.mkdir(exist_ok=True)
 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
 
-@router.post("/", response_class=FileResponse, summary="单个拼图接口")
-async def stitch_single_puzzle(
+@router.post("/", summary="通用拼图接口")
+async def stitch_puzzle(
         background_tasks: BackgroundTasks,
-        zip_file: UploadFile = File(..., description="包含一个文件夹的ZIP压缩包,文件夹内有24张小图。"),
+        zip_file: UploadFile = File(..., description="包含一个或多个拼图文件夹的ZIP压缩包。"),
         # --- 通用参数 ---
         method: StitchingMethod = Form(StitchingMethod.TEMPLATE_MATCH, description="选择拼图方法"),
         num_cols: int = Form(4, description="拼图的列数"),
         num_rows: int = Form(6, description="拼图的行数"),
         overlap_h: int = Form(405, description="预估的水平重叠像素"),
         overlap_v: int = Form(440, description="预估的垂直重叠像素"),
-        # --- 点匹配法 (key_point) 特定参数 ---
-        kp_blend_type: KeypointBlendType = Form(KeypointBlendType.COMBINE, description="[点匹配] 融合模式"),
+        # --- 关键点匹配法 (key_point) 特定参数 ---
+        kp_blend_type: KeypointBlendType = Form(KeypointBlendType.HALF_IMPORTANCE_ADD_WEIGHT,
+                                                description="[关键点] 融合模式"),
         kp_feature_detector: KeypointFeatureDetector = Form(KeypointFeatureDetector.SIFT,
-                                                            description="[点匹配] 特征检测器"),
+                                                            description="[关键点] 特征检测器"),
+        kp_blend_ratio: float = Form(0.5, description="[关键点] 融合权重 (0.0-1.0)"),
         # --- 模板匹配法 (template_match) 特定参数 ---
         tm_blend_type: TemplateBlendType = Form(TemplateBlendType.HALF_IMPORTANCE_ADD_WEIGHT,
                                                 description="[模板匹配] 融合模式"),
-        tm_light_compensation: bool = Form(True, description="[模板匹配] 是否启用光照补偿")
-):
+        tm_blend_ratio: float = Form(0.5, description="[模板匹配] 融合权重 (0.0-1.0)"),
+        tm_light_compensation: bool = Form(True, description="[模板/关键点] 是否启用光照补偿"),
+        tm_light_compensation_width: int = Form(15, description="[模板/关键点] 光照补偿宽度 (像素)")
+) -> FileResponse:
     """
-    上传一个包含24张小图的文件夹的ZIP压缩包,接口会将其拼接成一张大图并返回。
-
-    - **zip_file**: 必须是.zip格式,内部应仅包含一个文件夹,该文件夹内含所有待拼接的.jpg图片。
-    - **返回**: 拼接成功后,返回拼接好的图片文件,文件名与ZIP包内的文件夹名相同。
+    上传一个包含拼图文件夹的ZIP压缩包,接口会处理所有文件夹。
+    - **如果只处理了一个文件夹**:直接返回拼接好的图片文件。
+    - **如果处理了多个文件夹**:将结果打包成一个新的ZIP压缩包返回。
     """
     request_id = str(uuid.uuid4())
     session_dir = TEMP_DIR / request_id
     session_dir.mkdir()
+    # 确保无论成功或失败,临时文件夹最终都会被清理
     background_tasks.add_task(cleanup_temp_folder, session_dir)
 
     zip_path = session_dir / zip_file.filename
@@ -57,90 +61,29 @@ async def stitch_single_puzzle(
 
     extracted_dir = session_dir / "extracted"
     extracted_dir.mkdir()
-
     try:
         with zipfile.ZipFile(zip_path, 'r') as zf:
             zf.extractall(extracted_dir)
     except zipfile.BadZipFile:
         raise HTTPException(status_code=400, detail="上传的文件不是有效的ZIP格式。")
 
-    image_dir = extracted_dir
-    output_dir = session_dir / "output"
-
-    # 根据选择的方法调用不同的拼接函数
-    stitched_image_path = None
-    if method == StitchingMethod.KEY_POINT:
-        stitched_image_path = stitcher_keypoint.stitch_img(
-            IMAGE_DIR=image_dir, OUTPUT_DIR=output_dir, NUM_COLS=num_cols, NUM_ROWS=num_rows,
-            ESTIMATE_OVERLAP_HORIZONTAL_PIXELS=overlap_h, ESTIMATE_OVERLAP_VERTICAL_PIXELS=overlap_v,
-            BLEND_TYPE=kp_blend_type.value, FeatureDetector=kp_feature_detector.value,
-            DEBUG_MODE=False
-        )
-    elif method == StitchingMethod.TEMPLATE_MATCH:
-        stitched_image_path = stitcher_template.stitch_img(
-            IMAGE_DIR=image_dir, OUTPUT_DIR=output_dir, NUM_COLS=num_cols, NUM_ROWS=num_rows,
-            ESTIMATE_OVERLAP_HORIZONTAL_PIXELS=overlap_h, ESTIMATE_OVERLAP_VERTICAL_PIXELS=overlap_v,
-            BLEND_TYPE=tm_blend_type.value, LIGHT_COMPENSATION=tm_light_compensation,
-            DEBUG_MODE=False
-        )
-
-    if not stitched_image_path or not stitched_image_path.exists():
-        raise HTTPException(status_code=500, detail=f"图片拼接失败,请检查服务器日志(请求ID: {request_id})。")
-
-    # 使用原始文件夹名命名输出图片
-    final_filename = f"{image_dir.name}.jpg"
-    final_filepath = stitched_image_path.rename(stitched_image_path.parent / final_filename)
-
-    return FileResponse(
-        path=final_filepath,
-        filename=final_filename,
-        media_type='image/jpeg'
-    )
-
-
-@router.post("/batch", response_class=FileResponse, summary="批量拼图接口")
-async def stitch_batch_puzzles(
-        background_tasks: BackgroundTasks,
-        zip_file: UploadFile = File(..., description="包含多个拼图文件夹的ZIP压缩包。"),
-        # 参数与单个拼图接口相同
-        method: StitchingMethod = Form(StitchingMethod.TEMPLATE_MATCH, description="选择拼图方法"),
-        num_cols: int = Form(4, description="拼图的列数"),
-        num_rows: int = Form(6, description="拼图的行数"),
-        overlap_h: int = Form(405, description="预估的水平重叠像素"),
-        overlap_v: int = Form(440, description="预估的垂直重叠像素"),
-        kp_blend_type: KeypointBlendType = Form(KeypointBlendType.COMBINE, description="[点匹配] 融合模式"),
-        kp_feature_detector: KeypointFeatureDetector = Form(KeypointFeatureDetector.SIFT,
-                                                            description="[点匹配] 特征检测器"),
-        tm_blend_type: TemplateBlendType = Form(TemplateBlendType.HALF_IMPORTANCE_ADD_WEIGHT,
-                                                description="[模板匹配] 融合模式"),
-        tm_light_compensation: bool = Form(True, description="[模板匹配] 是否启用光照补偿"),
-):
-    """
-    上传一个包含多个拼图文件夹的ZIP压缩包,接口会处理所有文件夹,并将结果打包成一个新的ZIP返回。
-
-    - **zip_file**: 必须是.zip格式,内部可以有多个文件夹,每个文件夹都包含待拼接的图片。
-    - **返回**: 一个ZIP压缩包,里面是所有拼接好的图片,每张图片以其对应的原文件夹名命名。
-    """
-    request_id = str(uuid.uuid4())
-    session_dir = TEMP_DIR / request_id
-    session_dir.mkdir()
-    background_tasks.add_task(cleanup_temp_folder, session_dir)
-
-    zip_path = session_dir / zip_file.filename
-    with open(zip_path, "wb") as buffer:
-        shutil.copyfileobj(zip_file.file, buffer)
-
-    extracted_dir = session_dir / "extracted"
-    extracted_dir.mkdir()
-    try:
-        with zipfile.ZipFile(zip_path, 'r') as zf:
-            zf.extractall(extracted_dir)
-    except zipfile.BadZipFile:
-        raise HTTPException(status_code=400, detail="上传的文件不是有效的ZIP格式。")
+    # 智能判断是单文件夹还是多文件夹模式
+    sub_items = list(extracted_dir.iterdir())
+    if len(sub_items) == 1 and sub_items[0].is_dir():
+        # Case 1: ZIP内只有一个顶层文件夹,任务在其子文件夹中
+        puzzle_folders = [d for d in sub_items[0].iterdir() if d.is_dir()]
+        # 如果子文件夹为空,则认为顶层文件夹本身就是任务
+        if not puzzle_folders:
+            puzzle_folders = [sub_items[0]]
+    else:
+        # Case 2: ZIP内有多个文件/文件夹,任务是其中的文件夹
+        puzzle_folders = [d for d in sub_items if d.is_dir()]
+        # 如果没有子目录,但有图片,则认为整个解压目录是一个任务
+        if not puzzle_folders and any(f.suffix.lower() in ['.jpg', '.png', '.jpeg'] for f in sub_items if f.is_file()):
+            puzzle_folders = [extracted_dir]
 
-    puzzle_folders = [d for d in extracted_dir.iterdir() if d.is_dir()]
     if not puzzle_folders:
-        raise HTTPException(status_code=400, detail="ZIP包中未找到任何拼图文件夹。")
+        raise HTTPException(status_code=400, detail="ZIP包中未找到任何包含图片的拼图文件夹。")
 
     batch_output_dir = session_dir / "batch_output"
     batch_output_dir.mkdir()
@@ -149,12 +92,8 @@ async def stitch_batch_puzzles(
     failed_folders = []
 
     for image_dir in puzzle_folders:
-        logging.info(f"--- 开始处理批量任务中的文件夹: {image_dir.name} ---")
-        # 为每个子任务创建一个独立的输出目录
-        single_output_dir = session_dir / "single_output"
-        if single_output_dir.exists():
-            shutil.rmtree(single_output_dir)  # 清理上一次循环的输出
-
+        logging.info(f"--- 开始处理文件夹: {image_dir.name} ---")
+        single_output_dir = session_dir / f"output_{image_dir.name}"
         stitched_image_path = None
         try:
             if method == StitchingMethod.KEY_POINT:
@@ -164,6 +103,9 @@ async def stitch_batch_puzzles(
                     ESTIMATE_OVERLAP_HORIZONTAL_PIXELS=overlap_h,
                     ESTIMATE_OVERLAP_VERTICAL_PIXELS=overlap_v,
                     BLEND_TYPE=kp_blend_type.value, FeatureDetector=kp_feature_detector.value,
+                    BLEND_RATIO=kp_blend_ratio,
+                    LIGHT_COMPENSATION=tm_light_compensation,
+                    LIGHT_COMPENSATION_WIDTH=tm_light_compensation_width,
                     DEBUG_MODE=False
                 )
             elif method == StitchingMethod.TEMPLATE_MATCH:
@@ -173,11 +115,12 @@ async def stitch_batch_puzzles(
                     ESTIMATE_OVERLAP_HORIZONTAL_PIXELS=overlap_h,
                     ESTIMATE_OVERLAP_VERTICAL_PIXELS=overlap_v,
                     BLEND_TYPE=tm_blend_type.value, LIGHT_COMPENSATION=tm_light_compensation,
+                    BLEND_RATIO=tm_blend_ratio,
+                    LIGHT_COMPENSATION_WIDTH=tm_light_compensation_width,
                     DEBUG_MODE=False
                 )
 
             if stitched_image_path and stitched_image_path.exists():
-                # 将成功的结果移到最终的批量输出目录
                 target_path = batch_output_dir / f"{image_dir.name}.jpg"
                 shutil.move(str(stitched_image_path), str(target_path))
                 processed_count += 1
@@ -192,17 +135,108 @@ async def stitch_batch_puzzles(
         detail_msg = f"所有 {len(puzzle_folders)} 个文件夹都拼接失败。失败列表: {failed_folders}"
         raise HTTPException(status_code=500, detail=detail_msg)
 
-    # 将最终结果打包成ZIP
-    output_zip_path = session_dir / "stitched_results.zip"
-    shutil.make_archive(str(output_zip_path.with_suffix('')), 'zip', batch_output_dir)
+    # --- 核心修改点:根据处理成功的数量决定返回类型 ---
+    successful_files = list(batch_output_dir.iterdir())
+
+    # 如果只有一个成功的拼图结果
+    if len(successful_files) == 1:
+        single_result_path = successful_files[0]
+        logging.info(f"检测到单个成功结果,直接返回图片: {single_result_path.name}")
+        return FileResponse(
+            path=single_result_path,
+            filename=single_result_path.name,
+            media_type='image/jpeg'
+        )
+
+    # 如果有多个成功的结果,或者即使只有一个成功但原始任务也是多个(为了保持一致性)
+    else:
+        output_zip_name = "stitched_results.zip"
+        output_zip_path = session_dir / output_zip_name
+        shutil.make_archive(str(output_zip_path.with_suffix('')), 'zip', batch_output_dir)
+        logging.info(f"检测到多个成功结果,返回ZIP包: {output_zip_name}")
 
-    if failed_folders:
-        logging.warning(f"批量任务完成,但有 {len(failed_folders)} 个文件夹失败: {failed_folders}")
-        # 可以在响应头中添加自定义信息来通知客户端部分失败
-        # response.headers["X-Failed-Folders"] = ",".join(failed_folders)
+        if failed_folders:
+            logging.warning(f"批量任务完成,但有 {len(failed_folders)} 个文件夹失败: {failed_folders}")
+
+        return FileResponse(
+            path=output_zip_path,
+            filename=output_zip_name,
+            media_type='application/zip'
+        )
 
+
+@router.post("/folder", response_class=FileResponse, summary="单个拼图接口 (直接上传文件)")
+async def stitch_puzzle_from_folder(
+        background_tasks: BackgroundTasks,
+        files: List[UploadFile] = File(..., description="一个文件夹中的所有待拼接图片。"),
+        output_filename_base: str = Form("stitched_result", description="输出图片的基础名称(不含扩展名)。"),
+        # --- 参数与ZIP接口完全相同 ---
+        method: StitchingMethod = Form(StitchingMethod.TEMPLATE_MATCH, description="选择拼图方法"),
+        num_cols: int = Form(4, description="拼图的列数"),
+        num_rows: int = Form(6, description="拼图的行数"),
+        overlap_h: int = Form(405, description="预估的水平重叠像素"),
+        overlap_v: int = Form(440, description="预估的垂直重叠像素"),
+        kp_blend_type: KeypointBlendType = Form(KeypointBlendType.HALF_IMPORTANCE_ADD_WEIGHT,
+                                                description="[关键点] 融合模式"),
+        kp_feature_detector: KeypointFeatureDetector = Form(KeypointFeatureDetector.SIFT,
+                                                            description="[关键点] 特征检测器"),
+        kp_blend_ratio: float = Form(0.5, description="[关键点] 融合权重 (0.0-1.0)"),
+        tm_blend_type: TemplateBlendType = Form(TemplateBlendType.HALF_IMPORTANCE_ADD_WEIGHT,
+                                                description="[模板匹配] 融合模式"),
+        tm_blend_ratio: float = Form(0.5, description="[模板匹配] 融合权重 (0.0-1.0)"),
+        tm_light_compensation: bool = Form(True, description="[模板/关键点] 是否启用光照补偿"),
+        tm_light_compensation_width: int = Form(15, description="[模板/关键点] 光照补偿宽度 (像素)")
+):
+    """
+    上传一个文件夹内的所有图片进行拼接,直接返回拼接好的单张大图。
+    此接口专为无法或不便在客户端进行ZIP压缩的场景设计。
+    """
+    if not files:
+        raise HTTPException(status_code=400, detail="没有上传任何文件。")
+
+    request_id = str(uuid.uuid4())
+    session_dir = TEMP_DIR / request_id
+    session_dir.mkdir()
+    background_tasks.add_task(cleanup_temp_folder, session_dir)
+
+    # 在会话目录中创建一个子目录来存放上传的图片,模拟一个文件夹
+    image_dir = session_dir / "images"
+    image_dir.mkdir()
+
+    for upload_file in files:
+        file_path = image_dir / upload_file.filename
+        with open(file_path, "wb") as buffer:
+            shutil.copyfileobj(upload_file.file, buffer)
+
+    output_dir = session_dir / "output"
+    stitched_image_path = None
+
+    try:
+        if method == StitchingMethod.KEY_POINT:
+            stitched_image_path = stitcher_keypoint.stitch_img(
+                IMAGE_DIR=image_dir, OUTPUT_DIR=output_dir, NUM_COLS=num_cols, NUM_ROWS=num_rows,
+                ESTIMATE_OVERLAP_HORIZONTAL_PIXELS=overlap_h, ESTIMATE_OVERLAP_VERTICAL_PIXELS=overlap_v,
+                BLEND_TYPE=kp_blend_type.value, FeatureDetector=kp_feature_detector.value, BLEND_RATIO=kp_blend_ratio,
+                LIGHT_COMPENSATION=tm_light_compensation, LIGHT_COMPENSATION_WIDTH=tm_light_compensation_width,
+                DEBUG_MODE=False
+            )
+        elif method == StitchingMethod.TEMPLATE_MATCH:
+            stitched_image_path = stitcher_template.stitch_img(
+                IMAGE_DIR=image_dir, OUTPUT_DIR=output_dir, NUM_COLS=num_cols, NUM_ROWS=num_rows,
+                ESTIMATE_OVERLAP_HORIZONTAL_PIXELS=overlap_h, ESTIMATE_OVERLAP_VERTICAL_PIXELS=overlap_v,
+                BLEND_TYPE=tm_blend_type.value, LIGHT_COMPENSATION=tm_light_compensation, BLEND_RATIO=tm_blend_ratio,
+                LIGHT_COMPENSATION_WIDTH=tm_light_compensation_width, DEBUG_MODE=False
+            )
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=f"图片拼接过程中发生内部错误: {e}")
+
+    if not stitched_image_path or not stitched_image_path.exists():
+        raise HTTPException(status_code=500, detail=f"图片拼接失败,未能生成结果文件。")
+
+    final_filename = f"{output_filename_base}.jpg"
+    # 我们直接从最终的输出路径返回,不需要移动文件
     return FileResponse(
-        path=output_zip_path,
-        filename="stitched_results.zip",
-        media_type='application/zip'
-    )
+        path=stitched_image_path,
+        filename=final_filename,
+        media_type='image/jpeg'
+    )

+ 0 - 206
app/api/stitch_v2.py

@@ -1,206 +0,0 @@
-import os
-import shutil
-import uuid
-import zipfile
-import logging
-from pathlib import Path
-from typing import List  # 导入 List 类型
-
-from fastapi import APIRouter, UploadFile, File, Form, HTTPException, BackgroundTasks
-from fastapi.responses import FileResponse, JSONResponse
-
-# 导入我们的核心逻辑和数据模型
-from app.core import stitcher_keypoint, stitcher_template
-from app.schemas import StitchingMethod, KeypointFeatureDetector, KeypointBlendType, TemplateBlendType
-
-from utils.utils import cleanup_temp_folder
-
-
-router = APIRouter(prefix="/stitch", tags=['拼图'])
-
-TEMP_DIR = Path("_temp_work")
-TEMP_DIR.mkdir(exist_ok=True)
-logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
-
-
-# --- 内部辅助函数,用于处理单个拼图任务,避免代码重复 ---
-def _process_single_puzzle(
-    image_dir: Path,
-    output_dir: Path,
-    method: StitchingMethod,
-    params: dict
-) -> Path | None:
-    """
-    处理单个拼图任务的核心逻辑。
-
-    :param image_dir: 包含所有小图的输入目录。
-    :param output_dir: 存放拼接结果的输出目录。
-    :param method: 拼图方法。
-    :param params: 包含所有拼图参数的字典。
-    :return: 成功则返回拼接后图片的路径,否则返回 None。
-    """
-    output_dir.mkdir(exist_ok=True)
-    stitched_image_path = None
-    try:
-        if method == StitchingMethod.KEY_POINT:
-            stitched_image_path = stitcher_keypoint.stitch_img(
-                IMAGE_DIR=image_dir, OUTPUT_DIR=output_dir,
-                NUM_COLS=params["num_cols"], NUM_ROWS=params["num_rows"],
-                ESTIMATE_OVERLAP_HORIZONTAL_PIXELS=params["overlap_h"],
-                ESTIMATE_OVERLAP_VERTICAL_PIXELS=params["overlap_v"],
-                BLEND_TYPE=params["kp_blend_type"].value,
-                FeatureDetector=params["kp_feature_detector"].value,
-                DEBUG_MODE=False
-            )
-        elif method == StitchingMethod.TEMPLATE_MATCH:
-            stitched_image_path = stitcher_template.stitch_img(
-                IMAGE_DIR=image_dir, OUTPUT_DIR=output_dir,
-                NUM_COLS=params["num_cols"], NUM_ROWS=params["num_rows"],
-                ESTIMATE_OVERLAP_HORIZONTAL_PIXELS=params["overlap_h"],
-                ESTIMATE_OVERLAP_VERTICAL_PIXELS=params["overlap_v"],
-                BLEND_TYPE=params["tm_blend_type"].value,
-                LIGHT_COMPENSATION=params["tm_light_compensation"],
-                DEBUG_MODE=False
-            )
-    except Exception as e:
-        logging.error(f"处理文件夹 {image_dir.name} 时发生错误: {e}")
-        return None
-
-    return stitched_image_path
-
-
-# --- 新的单个拼图接口 (直接上传图片文件夹) ---
-@router.post("/from-folder", response_class=FileResponse, summary="单个拼图接口 (从文件夹上传)")
-async def stitch_single_from_folder(
-        background_tasks: BackgroundTasks,
-        files: List[UploadFile] = File(..., description="一个文件夹中的所有待拼接图片。"),
-        output_filename_base: str = Form(..., description="输出图片的基础名称(不含扩展名),例如 'puzzle_A'。"),
-        # --- 通用参数 ---
-        method: StitchingMethod = Form(StitchingMethod.TEMPLATE_MATCH, description="选择拼图方法"),
-        num_cols: int = Form(4, description="拼图的列数"),
-        num_rows: int = Form(6, description="拼图的行数"),
-        overlap_h: int = Form(405, description="预估的水平重叠像素"),
-        overlap_v: int = Form(440, description="预估的垂直重叠像素"),
-        # --- 点匹配法 (key_point) 特定参数 ---
-        kp_blend_type: KeypointBlendType = Form(KeypointBlendType.COMBINE, description="[点匹配] 融合模式"),
-        kp_feature_detector: KeypointFeatureDetector = Form(KeypointFeatureDetector.SIFT,
-                                                            description="[点匹配] 特征检测器"),
-        # --- 模板匹配法 (template_match) 特定参数 ---
-        tm_blend_type: TemplateBlendType = Form(TemplateBlendType.HALF_IMPORTANCE_ADD_WEIGHT,
-                                                description="[模板匹配] 融合模式"),
-        tm_light_compensation: bool = Form(True, description="[模板匹配] 是否启用光照补偿")
-):
-    """
-    上传一个文件夹内的所有图片进行拼接,直接返回拼接好的单张大图。
-
-    - **files**: 选择一个文件夹中的所有图片进行上传。
-    - **output_filename_base**: 为你的拼图任务命名,这个名字将作为返回图片的文件名。
-    - **返回**: 拼接成功后,返回拼接好的图片文件。
-    """
-    if not files:
-        raise HTTPException(status_code=400, detail="没有上传任何文件。")
-
-    request_id = str(uuid.uuid4())
-    session_dir = TEMP_DIR / request_id
-    session_dir.mkdir()
-    background_tasks.add_task(cleanup_temp_folder, session_dir)
-
-    # 创建用于存放上传图片的临时目录
-    image_dir = session_dir / "images"
-    image_dir.mkdir()
-
-    # 保存所有上传的文件
-    for upload_file in files:
-        file_path = image_dir / upload_file.filename
-        with open(file_path, "wb") as buffer:
-            shutil.copyfileobj(upload_file.file, buffer)
-
-    output_dir = session_dir / "output"
-
-    # 将所有参数打包到一个字典中,方便传递
-    params = locals()
-
-    # 调用核心处理函数
-    stitched_image_path = _process_single_puzzle(image_dir, output_dir, method, params)
-
-    if not stitched_image_path or not stitched_image_path.exists():
-        raise HTTPException(status_code=500, detail=f"图片拼接失败,请检查服务器日志(请求ID: {request_id})。")
-
-    # 使用用户提供的基础名称命名输出图片
-    final_filename = f"{output_filename_base}.jpg"
-    final_filepath = stitched_image_path.rename(stitched_image_path.parent / final_filename)
-
-    return FileResponse(
-        path=final_filepath,
-        filename=final_filename,
-        media_type='image/jpeg'
-    )
-
-
-# --- 新的批量拼图接口 (上传一个拼图文件夹,返回ZIP) ---
-@router.post("/batch/from-folder", response_class=FileResponse, summary="批量拼图接口 (单文件夹上传, ZIP返回)")
-async def stitch_batch_from_folder(
-        background_tasks: BackgroundTasks,
-        files: List[UploadFile] = File(..., description="一个文件夹中的所有待拼接图片。"),
-        output_filename_base: str = Form(..., description="输出图片的基础名称(不含扩展名),例如 'puzzle_A'。"),
-        # --- 参数与单个拼图接口相同 ---
-        method: StitchingMethod = Form(StitchingMethod.TEMPLATE_MATCH, description="选择拼图方法"),
-        num_cols: int = Form(4, description="拼图的列数"),
-        num_rows: int = Form(6, description="拼图的行数"),
-        overlap_h: int = Form(405, description="预估的水平重叠像素"),
-        overlap_v: int = Form(440, description="预估的垂直重叠像素"),
-        kp_blend_type: KeypointBlendType = Form(KeypointBlendType.COMBINE, description="[点匹配] 融合模式"),
-        kp_feature_detector: KeypointFeatureDetector = Form(KeypointFeatureDetector.SIFT,
-                                                            description="[点匹配] 特征检测器"),
-        tm_blend_type: TemplateBlendType = Form(TemplateBlendType.HALF_IMPORTANCE_ADD_WEIGHT,
-                                                description="[模板匹配] 融合模式"),
-        tm_light_compensation: bool = Form(True, description="[模板匹配] 是否启用光照补偿"),
-):
-    """
-    上传一个文件夹内的所有图片进行拼接,将结果打包成一个ZIP压缩文件返回。
-
-    - **files**: 选择一个文件夹中的所有图片进行上传。
-    - **output_filename_base**: 为你的拼图任务命名,这个名字将作为ZIP包内图片的文件名。
-    - **返回**: 一个ZIP压缩包,里面包含拼接好的单张图片。
-    """
-    if not files:
-        raise HTTPException(status_code=400, detail="没有上传任何文件。")
-
-    request_id = str(uuid.uuid4())
-    session_dir = TEMP_DIR / request_id
-    session_dir.mkdir()
-    background_tasks.add_task(cleanup_temp_folder, session_dir)
-
-    image_dir = session_dir / "images"
-    image_dir.mkdir()
-
-    for upload_file in files:
-        file_path = image_dir / upload_file.filename
-        with open(file_path, "wb") as buffer:
-            shutil.copyfileobj(upload_file.file, buffer)
-
-    single_output_dir = session_dir / "single_output"
-    params = locals()
-
-    stitched_image_path = _process_single_puzzle(image_dir, single_output_dir, method, params)
-
-    if not stitched_image_path or not stitched_image_path.exists():
-        raise HTTPException(status_code=500, detail=f"图片拼接失败,请检查服务器日志(请求ID: {request_id})。")
-
-    # --- 将单个结果打包成ZIP ---
-    batch_output_dir = session_dir / "batch_output"
-    batch_output_dir.mkdir()
-
-    # 将成功的结果移到最终的批量输出目录
-    target_path = batch_output_dir / f"{output_filename_base}.jpg"
-    shutil.move(str(stitched_image_path), str(target_path))
-
-    # 将最终结果打包成ZIP
-    output_zip_path = session_dir / "stitched_result.zip"
-    shutil.make_archive(str(output_zip_path.with_suffix('')), 'zip', batch_output_dir)
-
-    return FileResponse(
-        path=output_zip_path,
-        filename=f"{output_filename_base}_stitched.zip",
-        media_type='application/zip'
-    )

+ 57 - 80
app/core/stitcher_keypoint.py

@@ -1,107 +1,84 @@
 import cv2
-from pathlib import Path
-import concurrent.futures
 import logging
+from pathlib import Path
 from tqdm import tqdm
 
 from fry_project_classes.stitch_img_key_point import ImageStitcherKeyPoint
+from fry_project_classes.get_full_stitch_order import get_full_stitch_order
 from utils.utils import natural_sort_key
 
 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
 
-def stitch_single_row_keypoint(row_index, row_image_paths, stitch_params):
-    NUM_COLS = len(row_image_paths)
-    OUTPUT_DIR = stitch_params['OUTPUT_DIR']
-    ESTIMATE_OVERLAP_HORIZONTAL_PIXELS = stitch_params['ESTIMATE_OVERLAP_HORIZONTAL_PIXELS']
-    BLEND_TYPE = stitch_params['BLEND_TYPE']
-    FeatureDetector = stitch_params['FeatureDetector']
-    DEBUG_MODE = stitch_params['DEBUG_MODE']
-
-    current_row_image = cv2.imread(str(row_image_paths[0]))
-    if current_row_image is None:
-        logging.error(f"错误: 无法读取图片 {row_image_paths[0]}")
-        return row_index, None
-
-    for j in range(1, NUM_COLS):
-        stitcher_h = ImageStitcherKeyPoint(
-            estimate_overlap_pixels=ESTIMATE_OVERLAP_HORIZONTAL_PIXELS,
-            stitch_type="horizontal",
-            blend_type=BLEND_TYPE,
-            feature_detector=FeatureDetector,
-            blend_ratio=0.5,
-            debug=DEBUG_MODE,
-            debug_dir=str(OUTPUT_DIR / f'debug_h_row{row_index + 1}_col{j}vs{j + 1}')
-        )
-        next_image = cv2.imread(str(row_image_paths[j]))
-        if next_image is None:
-            logging.error(f"错误: 无法读取图片 {row_image_paths[j]}")
-            return row_index, current_row_image
-        current_row_image = stitcher_h.stitch_main(current_row_image, next_image)
-    return row_index, current_row_image
-
-
 def stitch_img(IMAGE_DIR: Path, OUTPUT_DIR: Path, NUM_COLS: int, NUM_ROWS: int,
                ESTIMATE_OVERLAP_HORIZONTAL_PIXELS: int, ESTIMATE_OVERLAP_VERTICAL_PIXELS: int,
                BLEND_TYPE: str, FeatureDetector: str,
+               # --- 新增的关键参数 ---
+               BLEND_RATIO: float,
+               LIGHT_COMPENSATION: bool,
+               LIGHT_COMPENSATION_WIDTH: int,
                DEBUG_MODE: bool) -> Path | None:
     """
-    基于关键点的图像拼接函数。
+    基于关键点的图像拼接函数,使用优化的拼接顺序。
     成功时返回最终图像的路径,失败时返回 None。
     """
     OUTPUT_DIR.mkdir(exist_ok=True, parents=True)
+    logging.info("--- [关键点] 图像拼接开始 (优化顺序) ---")
 
-    logging.info("--- [关键点] 图像拼接开始 ---")
-    logging.info(f"配置: {NUM_ROWS}行 x {NUM_COLS}列, 图片目录: {IMAGE_DIR}")
-
-    image_paths = sorted(list(IMAGE_DIR.glob("*.jpg")), key=natural_sort_key)
-    if len(image_paths) != NUM_COLS * NUM_ROWS:
+    # 1. 加载并排序所有图片
+    image_paths = sorted(list(IMAGE_DIR.glob("*.*")), key=natural_sort_key)
+    if len(image_paths) < NUM_COLS * NUM_ROWS:
         logging.error(f"错误: 找到 {len(image_paths)} 张图片, 但预期需要 {NUM_COLS * NUM_ROWS} 张。")
         return None
 
+    images_dict = {}
+    for i, path in enumerate(image_paths):
+        img = cv2.imread(str(path))
+        if img is None:
+            logging.error(f"错误: 无法读取图片 {path}")
+            return None
+        images_dict[str(i + 1)] = img
 
-    # --- 阶段一:并行水平拼接每一行 ---
-    logging.info("--- 阶段一: 并行水平拼接每一行 ---")
-    stitch_params = {
-        'OUTPUT_DIR': OUTPUT_DIR,
-        'ESTIMATE_OVERLAP_HORIZONTAL_PIXELS': ESTIMATE_OVERLAP_HORIZONTAL_PIXELS,
-        'BLEND_TYPE': BLEND_TYPE,
-        'FeatureDetector': FeatureDetector,
-        'DEBUG_MODE': DEBUG_MODE
-    }
-    stitched_rows = [None] * NUM_ROWS
-    with concurrent.futures.ProcessPoolExecutor() as executor:
-        futures = [executor.submit(stitch_single_row_keypoint, i, image_paths[i * NUM_COLS: i * NUM_COLS + NUM_COLS],
-                                   stitch_params) for i in range(NUM_ROWS)]
-        for future in tqdm(concurrent.futures.as_completed(futures), total=NUM_ROWS, desc="[关键点]处理行"):
-            try:
-                row_index, result_image = future.result()
-                if result_image is not None:
-                    stitched_rows[row_index] = result_image
-                else:
-                    logging.warning(f"第 {row_index + 1} 行拼接失败。")
-            except Exception as exc:
-                logging.error(f"一个行拼接任务生成了异常: {exc}")
+    # 2. 获取拼接顺序
+    full_stitch_order_dict = get_full_stitch_order(NUM_ROWS, NUM_COLS)
+    logging.info(f"获取到 {len(full_stitch_order_dict)} 步拼接指令")
 
-    if any(row is None for row in stitched_rows):
-        logging.error("错误: 存在拼接失败的行,无法进行垂直拼接。")
-        return None
+    # 3. 按照指令集执行拼接
+    final_image = None
+    iterator = tqdm(full_stitch_order_dict.items(), desc="[关键点]执行拼接", total=len(full_stitch_order_dict))
 
-    # --- 阶段二:垂直拼接所有行 ---
-    logging.info("--- 阶段二: 垂直拼接所有行 ---")
-    final_image = stitched_rows[0]
-    for i in tqdm(range(1, NUM_ROWS), desc="[关键点]拼接行"):
-        stitcher_v = ImageStitcherKeyPoint(
-            estimate_overlap_pixels=ESTIMATE_OVERLAP_VERTICAL_PIXELS, stitch_type="vertical",
-            blend_type=BLEND_TYPE, feature_detector=FeatureDetector, blend_ratio=0.5,
-            debug=DEBUG_MODE, debug_dir=str(OUTPUT_DIR / f'debug_v_row{i}vs{i + 1}')
-        )
-        next_row_image = stitched_rows[i]
-        final_image = stitcher_v.stitch_main(final_image, next_row_image)
+    for step, (round_num, img1_name, img2_name, direction, result_name) in iterator:
+        img1 = images_dict[img1_name]
+        img2 = images_dict[img2_name]
+
+        overlap_pixels = ESTIMATE_OVERLAP_HORIZONTAL_PIXELS if direction == 'horizontal' else ESTIMATE_OVERLAP_VERTICAL_PIXELS
 
-    # --- 保存并返回结果 ---
-    final_output_path = OUTPUT_DIR / "final_stitched_image.jpg"
-    cv2.imwrite(str(final_output_path), final_image)
-    logging.info(f"--- [关键点] 拼接任务完成!最终图已暂存至: {final_output_path} ---")
+        stitcher = ImageStitcherKeyPoint(
+            estimate_overlap_pixels=overlap_pixels,
+            stitch_type=direction,
+            blend_type=BLEND_TYPE,
+            feature_detector=FeatureDetector,
+            blend_ratio=BLEND_RATIO,
+            light_uniformity_compensation_enabled=LIGHT_COMPENSATION,
+            light_uniformity_compensation_width=LIGHT_COMPENSATION_WIDTH,
+            debug=DEBUG_MODE,
+            debug_dir=str(OUTPUT_DIR / f'debug_{result_name}')
+        )
 
-    return final_output_path
+        stitched_image = stitcher.stitch_main(img1, img2)
+        if stitched_image is None:
+            logging.error(f"步骤 {step} 拼接失败: {img1_name} + {img2_name}")
+            return None
+
+        images_dict[result_name] = stitched_image
+        final_image = stitched_image
+
+    # 4. 保存并返回结果
+    if final_image is not None:
+        final_output_path = OUTPUT_DIR / "final_stitched_image.jpg"
+        cv2.imwrite(str(final_output_path), final_image)
+        logging.info(f"--- [关键点] 拼接任务完成!最终图已暂存至: {final_output_path} ---")
+        return final_output_path
+    else:
+        logging.error("拼接失败,未能生成最终图像。")
+        return None

+ 56 - 79
app/core/stitcher_template.py

@@ -1,106 +1,83 @@
+
 import cv2
-from pathlib import Path
-import concurrent.futures
 import logging
+from pathlib import Path
 from tqdm import tqdm
 
 from fry_project_classes.stitch_img_template_match import ImageStitcherTemplateMatch
+from fry_project_classes.get_full_stitch_order import get_full_stitch_order
 from utils.utils import natural_sort_key
 
 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
 
-def stitch_single_row(row_index, row_image_paths, stitch_params):
-    NUM_COLS = len(row_image_paths)
-    OUTPUT_DIR = stitch_params['OUTPUT_DIR']
-    ESTIMATE_OVERLAP_HORIZONTAL_PIXELS = stitch_params['ESTIMATE_OVERLAP_HORIZONTAL_PIXELS']
-    BLEND_TYPE = stitch_params['BLEND_TYPE']
-    LIGHT_COMPENSATION = stitch_params['LIGHT_COMPENSATION']
-    DEBUG_MODE = stitch_params['DEBUG_MODE']
-
-    current_row_image = cv2.imread(str(row_image_paths[0]))
-    if current_row_image is None:
-        logging.error(f"错误: 无法读取图片 {row_image_paths[0]}")
-        return row_index, None
-
-    for j in range(1, NUM_COLS):
-        stitcher_h = ImageStitcherTemplateMatch(
-            estimate_overlap_pixels=ESTIMATE_OVERLAP_HORIZONTAL_PIXELS,
-            stitch_type="horizontal",
-            blend_type=BLEND_TYPE,
-            light_uniformity_compensation_enabled=LIGHT_COMPENSATION,
-            debug=DEBUG_MODE,
-            debug_dir=str(OUTPUT_DIR / f'debug_h_row{row_index + 1}_col{j}vs{j + 1}')
-        )
-        next_image = cv2.imread(str(row_image_paths[j]))
-        if next_image is None:
-            logging.error(f"错误: 无法读取图片 {row_image_paths[j]}")
-            return row_index, current_row_image
-        current_row_image = stitcher_h.stitch_main(current_row_image, next_image)
-    return row_index, current_row_image
-
-
 def stitch_img(IMAGE_DIR: Path, OUTPUT_DIR: Path, NUM_COLS: int, NUM_ROWS: int,
                ESTIMATE_OVERLAP_HORIZONTAL_PIXELS: int, ESTIMATE_OVERLAP_VERTICAL_PIXELS: int,
                BLEND_TYPE: str, LIGHT_COMPENSATION: bool,
+               # --- 新增的关键参数 ---
+               BLEND_RATIO: float,
+               LIGHT_COMPENSATION_WIDTH: int,
                DEBUG_MODE: bool) -> Path | None:
     """
-    基于模板匹配的图像拼接函数。
+    基于模板匹配的图像拼接函数,使用优化的拼接顺序。
     成功时返回最终图像的路径,失败时返回 None。
     """
     OUTPUT_DIR.mkdir(exist_ok=True, parents=True)
+    logging.info("--- [模板匹配] 图像拼接开始 (优化顺序) ---")
 
-    logging.info("--- [模板匹配] 图像拼接开始 ---")
-    logging.info(f"配置: {NUM_ROWS}行 x {NUM_COLS}列, 图片目录: {IMAGE_DIR}")
-
-    image_paths = sorted(list(IMAGE_DIR.glob("*.jpg")), key=natural_sort_key)
-    if len(image_paths) != NUM_COLS * NUM_ROWS:
+    # 1. 加载并排序所有图片
+    image_paths = sorted(list(IMAGE_DIR.glob("*.*")), key=natural_sort_key)  # 支持更多图片格式
+    if len(image_paths) < NUM_COLS * NUM_ROWS:
         logging.error(f"错误: 找到 {len(image_paths)} 张图片, 但预期需要 {NUM_COLS * NUM_ROWS} 张。")
         return None
 
-    # --- 阶段一:并行水平拼接每一行 ---
-    logging.info("--- 阶段一: 并行水平拼接每一行 ---")
-    stitch_params = {
-        'OUTPUT_DIR': OUTPUT_DIR,
-        'ESTIMATE_OVERLAP_HORIZONTAL_PIXELS': ESTIMATE_OVERLAP_HORIZONTAL_PIXELS,
-        'BLEND_TYPE': BLEND_TYPE,
-        'LIGHT_COMPENSATION': LIGHT_COMPENSATION,
-        'DEBUG_MODE': DEBUG_MODE
-    }
-    stitched_rows = [None] * NUM_ROWS
-    with concurrent.futures.ProcessPoolExecutor() as executor:
-        futures = [
-            executor.submit(stitch_single_row, i, image_paths[i * NUM_COLS: i * NUM_COLS + NUM_COLS], stitch_params) for
-            i in range(NUM_ROWS)]
-        for future in tqdm(concurrent.futures.as_completed(futures), total=NUM_ROWS, desc="[模板]处理行"):
-            try:
-                row_index, result_image = future.result()
-                if result_image is not None:
-                    stitched_rows[row_index] = result_image
-                else:
-                    logging.warning(f"第 {row_index + 1} 行拼接失败。")
-            except Exception as exc:
-                logging.error(f"一个行拼接任务生成了异常: {exc}")
+    images_dict = {}
+    for i, path in enumerate(image_paths):
+        img = cv2.imread(str(path))
+        if img is None:
+            logging.error(f"错误: 无法读取图片 {path}")
+            return None
+        images_dict[str(i + 1)] = img
 
-    if any(row is None for row in stitched_rows):
-        logging.error("错误: 存在拼接失败的行,无法进行垂直拼接。")
-        return None
+    # 2. 获取拼接顺序
+    full_stitch_order_dict = get_full_stitch_order(NUM_ROWS, NUM_COLS)
+    logging.info(f"获取到 {len(full_stitch_order_dict)} 步拼接指令")
+
+    # 3. 按照指令集执行拼接
+    final_image = None
+    iterator = tqdm(full_stitch_order_dict.items(), desc="[模板]执行拼接", total=len(full_stitch_order_dict))
+
+    for step, (round_num, img1_name, img2_name, direction, result_name) in iterator:
+        img1 = images_dict[img1_name]
+        img2 = images_dict[img2_name]
 
-    # --- 阶段二:垂直拼接所有行 ---
-    logging.info("--- 阶段二: 垂直拼接所有行 ---")
-    final_image = stitched_rows[0]
-    for i in tqdm(range(1, NUM_ROWS), desc="[模板]拼接行"):
-        stitcher_v = ImageStitcherTemplateMatch(
-            estimate_overlap_pixels=ESTIMATE_OVERLAP_VERTICAL_PIXELS, stitch_type="vertical",
-            blend_type=BLEND_TYPE, light_uniformity_compensation_enabled=LIGHT_COMPENSATION,
-            debug=DEBUG_MODE, debug_dir=str(OUTPUT_DIR / f'debug_v_row{i}vs{i + 1}')
+        overlap_pixels = ESTIMATE_OVERLAP_HORIZONTAL_PIXELS if direction == 'horizontal' else ESTIMATE_OVERLAP_VERTICAL_PIXELS
+
+        stitcher = ImageStitcherTemplateMatch(
+            estimate_overlap_pixels=overlap_pixels,
+            stitch_type=direction,
+            blend_type=BLEND_TYPE,
+            blend_ratio=BLEND_RATIO,
+            light_uniformity_compensation_enabled=LIGHT_COMPENSATION,
+            light_uniformity_compensation_width=LIGHT_COMPENSATION_WIDTH,
+            debug=DEBUG_MODE,
+            debug_dir=str(OUTPUT_DIR / f'debug_{result_name}')
         )
-        next_row_image = stitched_rows[i]
-        final_image = stitcher_v.stitch_main(final_image, next_row_image)
 
-    # --- 保存并返回结果 ---
-    final_output_path = OUTPUT_DIR / "final_stitched_image.jpg"
-    cv2.imwrite(str(final_output_path), final_image)
-    logging.info(f"--- [模板匹配] 拼接任务完成!最终图已暂存至: {final_output_path} ---")
+        stitched_image = stitcher.stitch_main(img1, img2)
+        if stitched_image is None:
+            logging.error(f"步骤 {step} 拼接失败: {img1_name} + {img2_name}")
+            return None
+
+        images_dict[result_name] = stitched_image
+        final_image = stitched_image
 
-    return final_output_path
+    # 4. 保存并返回结果
+    if final_image is not None:
+        final_output_path = OUTPUT_DIR / "final_stitched_image.jpg"
+        cv2.imwrite(str(final_output_path), final_image)
+        logging.info(f"--- [模板匹配] 拼接任务完成!最终图已暂存至: {final_output_path} ---")
+        return final_output_path
+    else:
+        logging.error("拼接失败,未能生成最终图像。")
+        return None

+ 0 - 2
app/main.py

@@ -3,7 +3,6 @@ from fastapi.middleware.cors import CORSMiddleware
 import logging
 
 from app.api.stitch import router as stitch_router
-from app.api.stitch_v2 import router as stitch_router_v2
 
 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
@@ -24,4 +23,3 @@ app.add_middleware(
 
 
 app.include_router(stitch_router, prefix="/api")
-app.include_router(stitch_router_v2, prefix="/api")

+ 166 - 0
fry_project_classes/get_full_stitch_order.py

@@ -0,0 +1,166 @@
+
+
+def get_full_stitch_order(row_num: int, column_num: int,debug=False) -> dict:
+    """
+    生成图像拼接顺序
+    Args:
+        row_num: 总行数
+        column_num: 总列数
+    Returns:
+        dict: 拼接顺序字典,每个key是拼接步骤序号,value是一个元组(轮数, 图片1, 图片2, 拼接方向, 结果)
+    """
+    result_dict = {}
+    stitch_idx = [1]  # 使用列表以便在递归中修改
+    round_num = [1]   # 使用列表以便在递归中修改
+    
+    # 初始化图像矩阵
+    initial_matrix = []
+    for i in range(row_num):
+        row = []
+        for j in range(column_num):
+            row.append(str(i * column_num + j + 1))
+        initial_matrix.append(row)
+    
+    if debug:
+        print("\nInitial matrix:")
+        for row in initial_matrix:
+            print(row)
+
+    def recursive_stitch(matrix, direction="horizontal"):
+        """
+        递归执行拼接操作
+        Args:
+            matrix: 当前待处理的矩阵
+            direction: 当前的拼接方向 ("horizontal" 或 "vertical")
+        Returns:
+            处理后的矩阵
+        """
+        # 基本情况:矩阵只有一个元素
+        if len(matrix) == 1 and len(matrix[0]) == 1:
+            return matrix
+
+        new_matrix = []
+        has_stitching = False
+
+        if direction == "horizontal":
+            # 水平拼接
+            for row in matrix:
+                new_row = []
+                for i in range(0, len(row), 2):
+                    if i + 1 < len(row):
+                        # 两两水平拼接
+                        new_name = f"{row[i]}_{row[i+1]}"
+                        result_dict[stitch_idx[0]] = (round_num[0], row[i], row[i+1], 
+                                                    direction, new_name)
+                        stitch_idx[0] += 1
+                        new_row.append(new_name)
+                        has_stitching = True
+                    else:
+                        new_row.append(row[i])
+                new_matrix.append(new_row)
+        else:  # vertical
+            # 垂直拼接
+            max_cols = max(len(row) for row in matrix)
+            transposed_matrix = []
+            
+            # 构建转置矩阵
+            for col in range(max_cols):
+                col_elements = []
+                for row in matrix:
+                    if col < len(row):
+                        col_elements.append(row[col])
+                transposed_matrix.append(col_elements)
+            
+            # 处理每一列(现在是转置后的行)
+            result_matrix = []
+            for col in transposed_matrix:
+                new_col = []
+                for i in range(0, len(col), 2):
+                    if i + 1 < len(col):
+                        new_name = f"{col[i]}_{col[i+1]}"
+                        result_dict[stitch_idx[0]] = (round_num[0], col[i],
+                                                    col[i+1], direction, new_name)
+                        stitch_idx[0] += 1
+                        new_col.append(new_name)
+                        has_stitching = True
+                    else:
+                        new_col.append(col[i])
+                result_matrix.append(new_col)
+            
+            # 转置回来
+            new_matrix = [[row[i] for row in result_matrix if i < len(row)] 
+                         for i in range(max(len(row) for row in result_matrix))]
+
+        # 如果本轮有拼接操作,增加轮数并打印当前矩阵
+        if has_stitching:
+            round_num[0] += 1
+            if debug:
+                print(f"\n当前的方向为:{direction}")
+                print(f"\nAfter Round {round_num[0]} :")
+                for row in new_matrix:
+                    print(row)
+
+
+        # 判断是否为最后一轮(只剩下一行多列)
+        if len(new_matrix) == 1 and len(new_matrix[0]) > 1:
+            # 最后一轮,水平拼接剩余的所有元素
+            final_row = new_matrix[0]
+            final_result = []
+            for i in range(0, len(final_row), 2):
+                if i + 1 < len(final_row):
+                    new_name = f"{final_row[i]}_{final_row[i+1]}"
+                    result_dict[stitch_idx[0]] = (round_num[0], final_row[i],
+                                                final_row[i+1], "horizontal", new_name)
+                    stitch_idx[0] += 1
+                    final_result.append(new_name)
+                else:
+                    final_result.append(final_row[i])
+            new_matrix = [final_result]
+            if debug:
+                print(f"\nFinal Round {round_num[0]} (horizontal):")
+                print(new_matrix)
+            return new_matrix
+
+        # 确定下一轮的拼接方向
+        next_direction = "vertical" if direction == "horizontal" else "horizontal"
+
+        # 继续递归
+        if len(new_matrix) > 1 or len(new_matrix[0]) > 1:
+            return recursive_stitch(new_matrix, next_direction)
+        return new_matrix
+
+    # 开始递归拼接
+    recursive_stitch(initial_matrix)
+    return result_dict
+
+
+
+def test_stitch_images():
+    # 测试2×2的情况
+    print("Testing 2×2:")
+    result = get_full_stitch_order(2, 2)
+    print("\nStitch steps:")
+    for step, (round_num, img1, img2, direction, result_name) in result.items():
+        print(f"Step {step} (Round {round_num}): {img1} + {img2} ({direction}) -> {result_name}")
+    
+    print("\n" + "="*50 + "\nTesting 3×3:")
+    result = get_full_stitch_order(3, 3)
+    print("\nStitch steps:")
+    for step, (round_num, img1, img2, direction, result_name) in result.items():
+        print(f"Step {step} (Round {round_num}): {img1} + {img2} ({direction}) -> {result_name}")
+    
+    print("\n" + "="*50 + "\nTesting 4×6:")
+    result = get_full_stitch_order(4, 6)
+    print("\nStitch steps:")
+    for step, (round_num, img1, img2, direction, result_name) in result.items():
+        print(f"Step {step} (Round {round_num}): {img1} + {img2} ({direction}) -> {result_name}")
+
+    print("\n" + "="*50 + "\nTesting 7×5:")
+    result = get_full_stitch_order(7, 5,debug=True)
+    print("\nStitch steps:")
+    for step, (round_num, img1, img2, direction, result_name) in result.items():
+        print(f"Step {step} (Round {round_num}): {img1} + {img2} ({direction}) -> {result_name}")
+
+
+if __name__ == "__main__":
+    test_stitch_images()

+ 1 - 1
fry_project_classes/stitch_img_template_match.py

@@ -318,7 +318,7 @@ class ImageStitcherTemplateMatch(BlendTypeMixin):
         # 计算真正的重叠区域:右图的左边+模板宽度+左图的右边
         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")