September 22, 2024Programming日常

從日本寫真集說到程序員的分享精神

不得不承認,程序員(Programmer)群體確實是最具分享精神的一種人。

我個人經常收集一些日本電子寫真圖集,但是很多圖集它是專供電子閱讀器看的,所以如果有對開頁的一整張大圖的話,這張圖會裁剪成兩半,但可以無縫合拼。

對於這些圖,我用node.js寫了個script,可以把單一文件夾的所有圖左右合拼,但是需要人工把這些圖挑出來放進文件夾,再順序 兩張兩張這樣進行處理和合拼。而這個人工挑圖的過程就非常費時間。

於是最近我在某論壇向網友提問:如果用編程方式或者用AI,有可能實現把整個文件夾扔進去就能自動合拼這些被切開兩半的圖,而不影響其他圖嗎?

而這個問題的難點無疑在於識別哪些是被切開兩半的圖。

很多網友很快提出許多方案:

  1. 把每個前後兩張接合的幾列像素點的rgb簡單判斷一下是否大概在一個趨勢內,然後再人肉刪掉實際上不是的就行
  2. 分析兩張圖分開部分最邊緣的像素點,80%接近,就認為是一張圖拆開的
  3. 沒有任何非常明顯的特征的情況下可以靠機器學習

有個網友甚至直接給我私訊了Python代碼,在這我再次感謝這位網友🙏。

我雖然不太懂電腦視覺方面的知識,但大概google了一下,他的代碼應該是用OpenCV通過比較前後兩張圖邊緣連接位的圖像相似度來識別是否有可能是一張圖。

雖然還是有誤差,但還是大大提高了效率,畢竟程序處理後我只要快速看一遍哪些是錯的刪掉就行。而原來的方案我需要看一遍所有圖,再挑出那些被裁剪的圖。當然還是有些沒有被識別的圖,但這些寫真都是我自己看的,也就無所謂了。

所以請不要再說程序員是傲慢的人了,只要你提出的需求清晰、不是那些簡單的隨便Google就找到答案的問題,很多程序員甚至願意免費幫你解決問題。

小到各種教程、大到各種開源項目都是程序員分享精神的體現。正因這種分享精神,才造就了今日我點開電腦屏幕就能看到的一個多姿多彩的世界。

原代碼如下:

import os import argparse from PIL import Image import numpy as np import cv2 from colorama import init, Fore, Back, Style from itertools import groupby def parse_arguments(): parser = argparse.ArgumentParser(description="自动合并文件夹中的分割图像。") parser.add_argument('input_dir', type=str, help='包含图像的输入目录路径。') parser.add_argument('output_dir', type=str, help='保存合并图像的输出目录路径。') parser.add_argument('--threshold', type=float, default=0.8, help='匹配边缘的相似度阈值(0-1)。') parser.add_argument('--edge_width', type=int, default=10, choices=range(1, 31), help='用于比较的边缘宽度(1-30像素)。') parser.add_argument('--reverse', action='store_true', help='是否倒序处理图片') parser.add_argument('--copy_unmatched', action='store_true', help='是否复制未匹配的图片(默认不复制)') parser.add_argument('--direction', type=str, default='hv', choices=['h', 'v', 'hv'], help='处理方向:h(横向),v(竖向),hv(两者都处理,默认)') parser.add_argument('--output_template', type=str, default='{f0}+{f1}', help='合并后文件名模板,可用变量:{f0}(第一个文件名),{f1}(第二个文件名)。默认为 {f0}') return parser.parse_args() def get_sorted_image_files(input_dir, reverse=False): def sort_key(filename): # 返回(文件名长度, 文件名)的元组 return (len(filename), filename) files = [f for f in os.listdir(input_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp'))] # 先按文件名长度排序 files.sort(key=lambda f: len(f), reverse=reverse) # 按长度分组,并在每个组内排序 sorted_files = [] for _, group in groupby(files, key=len): group_list = list(group) group_list.sort(reverse=reverse) sorted_files.extend(group_list) return sorted_files def load_image(path): try: return Image.open(path).convert('RGB') except Exception as e: print(f"Error loading image {path}: {e}") return None def is_single_color(edge, tolerance=100): """ 检查是否为单色 """ pixels = edge.reshape(-1, edge.shape[-1]) ranges = np.ptp(pixels, axis=0) return np.all(ranges <= tolerance) def edge_similarity(img1, img2, mode='horizontal', edge_width=10): if mode == 'horizontal': if img1.width != img2.width: return 0 edge1 = np.array(img1)[:, -edge_width:, :] edge2 = np.array(img2)[:, :edge_width, :] elif mode == 'vertical': if img1.height != img2.height: return 0 edge1 = np.array(img1)[-edge_width:, :, :] edge2 = np.array(img2)[:edge_width, :, :] else: return 0 # 检查边缘是否为单色 if is_single_color(edge1) or is_single_color(edge2): return 0 # 如果任一边缘为单色,返回0相似度 # 计算直方图 hist1 = cv2.calcHist([edge1], [0, 1, 2], None, [8, 8, 8], [0, 256, 0, 256, 0, 256]) hist2 = cv2.calcHist([edge2], [0, 1, 2], None, [8, 8, 8], [0, 256, 0, 256, 0, 256]) return cv2.compareHist(hist1, hist2, cv2.HISTCMP_CORREL) def try_merge(img1, img2, threshold, edge_width, direction='hv'): """ 根据指定的方向尝试合并img1和img2。 """ if direction in ['h', 'hv']: # 尝试水平合并 sim_h = edge_similarity(img1, img2, mode='horizontal', edge_width=edge_width) print(f"水平相似度: {sim_h}") if sim_h >= threshold: merged = Image.new('RGB', (img1.width + img2.width, img1.height)) merged.paste(img1, (0, 0)) merged.paste(img2, (img1.width, 0)) return merged, 'horizontal' if direction in ['v', 'hv']: # 尝试垂直合并 sim_v = edge_similarity(img1, img2, mode='vertical', edge_width=edge_width) print(f"垂直相似度: {sim_v}") if sim_v >= threshold: merged = Image.new('RGB', (img1.width, img1.height + img2.height)) merged.paste(img1, (0, 0)) merged.paste(img2, (0, img1.height)) return merged, 'vertical' # 如果合并失败,返回None和一个表示失败的字符串 return None, 'failed' def process_images(input_dir, output_dir, threshold, edge_width, reverse=False, copy_unmatched=False, direction='hv', output_template='{f0}'): if not os.path.exists(output_dir): os.makedirs(output_dir) files = get_sorted_image_files(input_dir, reverse) total_files = len(files) processed_files = 0 merged_count = 0 # 新增:合并成功的计数器 # 显示参数 input_dir, output_dir, threshold, edge_width, reverse print(f"{Fore.CYAN}输入目录: {Style.BRIGHT}{input_dir}") print(f"{Fore.CYAN}输出目录: {Style.BRIGHT}{output_dir}") print(f"{Fore.CYAN}相似度阈值: {Style.BRIGHT}{threshold}") print(f"{Fore.CYAN}边缘宽度: {Style.BRIGHT}{edge_width}") print(f"{Fore.CYAN}处理顺序: {Style.BRIGHT}{'倒序' if reverse else '正序'}") print(f"{Fore.CYAN}是否复制未匹配图片: {Style.BRIGHT}{'是' if copy_unmatched else '否'}") print(f"{Fore.CYAN}处理方向: {Style.BRIGHT}{direction}") print(f"{Fore.CYAN}开始处理图像 合计{total_files}个") print(f"{Fore.CYAN}{'='*60}") i = 0 while i < total_files: current_file = files[i] current_path = os.path.join(input_dir, current_file) img1 = load_image(current_path) if img1 is None: i += 1 continue merged = False if i < total_files - 1: next_file = files[i + 1] next_path = os.path.join(input_dir, next_file) img2 = load_image(next_path) if img2 is not None: print(f"{Fore.YELLOW}正在处理: {Style.BRIGHT}{current_file}{next_file}") merged_img, mode = try_merge(img1, img2, threshold, edge_width, direction) if merged_img is not None: # 生成新的文件名 base_name0, ext = os.path.splitext(current_file) base_name1, _ = os.path.splitext(next_file) merged_filename = output_template.format(f0=base_name0, f1=base_name1) + ext merged_path = os.path.join(output_dir, merged_filename) merged_img.save(merged_path) print(f"{Fore.GREEN}✓ 成功合并: {Style.BRIGHT}{current_file} + {next_file}{merged_filename}") print(f" 合并模式: {Style.BRIGHT}{mode.capitalize()}") i += 2 processed_files += 2 merged = True merged_count += 1 # 新增:增加合并成功的计数 else: print(f"{Fore.RED}✗ 无法合并: {Style.BRIGHT}{current_file}{next_file}") print(f" 原因: 边缘相似度低于阈值 ({threshold})") if not merged: if copy_unmatched: output_path = os.path.join(output_dir, current_file) img1.save(output_path) print(f"{Fore.BLUE}→ 直接复制: {Style.BRIGHT}{current_file}") else: print(f"{Fore.YELLOW}→ 跳过未匹配: {Style.BRIGHT}{current_file}") i += 1 processed_files += 1 print(f"{Fore.CYAN}{'-'*60}") print(f"{Fore.CYAN}{'='*60}") print(f"{Fore.GREEN}处理完成!") print(f"{Fore.CYAN}总共处理: {Style.BRIGHT}{processed_files}/{total_files} 个文件") print(f"{Fore.CYAN}成功合并: {Style.BRIGHT}{merged_count} 对图片") # 新增:显示成功合并的数量 def main(): init(autoreset=True) # 初始化 colorama args = parse_arguments() process_images(args.input_dir, args.output_dir, args.threshold, args.edge_width, args.reverse, args.copy_unmatched, args.direction, args.output_template) if __name__ == "__main__": # 命令行示例 # python main.py ./images ./output --threshold 0.8 --edge_width 3 --reverse --copy_unmatched main()