LibrePhotos 的 Stacks 与文件变体机制:RAW+JPEG、Live Photo 分组与连拍检测的实现原理
2026/9/16 18:02:33 网站建设 项目流程

LibrePhotos 的 Stacks 与文件变体机制:RAW+JPEG、Live Photo 分组与连拍检测的实现原理

【免费下载链接】librephotosA self-hosted open source photo management service.项目地址: https://gitcode.com/GitHub_Trending/li/librephotos

LibrePhotos 通过两套互补的机制自动组织相关照片:**文件变体(File Variants)**把"同一次拍摄的不同格式文件"折叠为一张照片,**堆叠(Stacks)**把"多次但相关的拍摄"归为一组。读完本文,你将理解扫描阶段两阶段分组的实现、Repair File Variants 修复任务的运作方式、基于规则引擎的连拍(Burst)检测原理,以及 Stacks 的完整数据模型与 API 调用链。

两个核心概念:文件变体 vs 堆叠

这是理解 LibrePhotos 照片分组的第一分界,二者解决的是不同层面的问题:

  • 文件变体(File Variants):同一次拍摄产生的不同格式文件(如IMG_001.CR2IMG_001.jpg),在时间线上只显示一条,但底层挂多个文件。它走Photo.files多对多关系,在扫描阶段自动建立。
  • 堆叠(Stacks):不同的、但逻辑上属于一组的照片(连拍、手动归组等),组内只有封面照片显示在时间线上。它走PhotoStack模型与Photo.stacks多对多关系,在检测阶段或手动操作时建立。

这一设计在 PhotoStack 模型 的注释中有明确说明:RAW+JPEG 对与 Live Photo "不再作为堆叠处理,而是使用 Photo.files 存储文件变体(PhotoPrism 风格的模型)",模型中RAW_JPEG_PAIRLIVE_PHOTO两个旧堆叠类型仅保留用于迁移兼容并已标记弃用。

文件变体

RAW+JPEG 对

相机以 RAW+JPEG 模式拍摄时会为每张照片生成两个文件(如IMG_001.CR2IMG_001.jpg)。LibrePhotos 在扫描时自动将两者归为一组——时间线上只显示一张照片,缩略图上叠加RAW 徽标表示存在 RAW 变体。

Live Photos

Live Photo 由一张静帧加一段短视频构成。从 Live Photo 检测模块 的源码看,LibrePhotos 实际覆盖三种形态:

形态特征检测方式
Google Pixel Motion PhotoMP4 数据直接内嵌在 JPEG 的 EOI 标记之后在文件内存映射中搜索ftypmp42/ftypisom/ftypiso2签名(ftyp前 4 字节即 MP4 头部)
Samsung Motion PhotoJPEG 后附带MotionPhoto_Data标记搜索MotionPhoto_Data字节串,其后即为视频数据
Apple Live Photo独立的同名.mov伴生文件图片名 + .mov在同目录查找,且通过_path_as_stored()匹配文件系统实际存储的大小写拼写,避免在 SMB 等大小写不敏感挂载上创建重复 File 记录

对于 Google/Samsung 的内嵌视频,extract_embedded_motion_video()会把视频从 JPEG 中抽出并写入MEDIA_ROOT/embedded_media/{hash}_motion.mp4,再作为embedded_media关联到原文件——该过程受FEATURE_PROCESS_EMBEDDED_MEDIA功能开关控制。

两阶段扫描:变体分组如何完成

LibrePhotos 使用两阶段扫描来避免并发处理的竞态条件,核心实现在 scan_jobs.py 的scan_photos()与 file_grouping.py:

  1. 阶段一 — 分组(Grouping):遍历扫描目录收集全部文件,_partition_scan_paths()按"(目录,不含扩展名的全小写基本名)"这一分组键把文件切分成组。IMG_001.jpgIMG_001.CR2落在同一组,XMP sidecar 在此阶段被单独暂存(因为它需要先找到归属的照片才能处理)。
  2. 阶段二 — 处理(Processing):每个文件组作为整体被处理(handle_file_group),创建一个 Photo 实体并把组内所有文件挂为变体。主显示文件(main_file)按类型优先级自动选择,见 file_grouping.py 中的FILE_TYPE_PRIORITY
优先级文件类型说明
1IMAGEJPEG/HEIC/PNG/TIFF,最高优先级
2VIDEO独立视频或 Live Photo 运动片段
3RAW_FILERAW 永远作为变体,不作主文件
4METADATA_FILEXMP sidecar,最低优先级
5UNKNOWN其他

select_main_file()在同类型内按路径字母序取第一个。所有图像组处理完毕后,暂存的 XMP sidecar 才通过一个"哨兵任务"(wait_for_group_and_process_metadata)按组完成顺序处理并挂接到所属照片上。

Repair File Variants 修复任务

每次扫描结束后,scan_jobs.py 的_queue_followup_jobs()会异步触发一次Repair File Variants任务(JOB_REPAIR_FILE_VARIANTS,编号 16),实现见 repair_jobs.py。它处理的是历次扫描中因竞态条件或增量添加产生的"孤儿"变体:

  • 找出所有main_file为 RAW 类型的 Photo(即 RAW 自成一张照片的情况);
  • 先用find_matching_jpeg_photo()在同目录、同基本名下查找已有的 JPEG/HEIC/PNG/TIFF 对应照片(扩展名大小写都尝试)——找到则把 RAW 文件并入该照片并删除孤立的 RAW Photo;
  • 找不到对应照片但自身已带图像变体时,由_promote_image_main_file()把主文件纠正为图像文件;
  • 已作为独立照片扫入的 Live Photo 视频不会被该任务合并(避免误伤独立视频)。

查看文件变体

  • 照片网格中,带 RAW 变体的缩略图显示RAW 徽标
  • 灯箱侧栏中,文件名旁(分辨率与文件大小旁边)出现+N format(s)链接,展开后列出所有非主变体,每个带格式徽标(JPGRAWVIDEOMETAFILE);
  • 以 zip 下载照片时,每张照片的全部文件变体会自动包含在内。

相关设置:遗留的 "Stack RAW+JPEG" 开关

设置中有一个Stack RAW+JPEG开关,它是遗留项——源自早期将 RAW+JPEG 对建模为堆叠(stack_raw_jpeg字段,User 模型 中默认True)。迁移 0109 曾把旧的skip_raw_files语义反向迁移到该字段。由于 RAW+JPEG 现在总是在扫描阶段作为文件变体分组,当前关闭该开关已无实际效果。

Stacks(堆叠)

堆叠把不同但相关的照片归组。注意:照片一旦入栈,时间线上只显示堆叠的封面照片,其余照片仍保留在图库中、折叠在堆叠之后——可以打开堆叠访问它们、用Set Cover更换封面,或Unstack解组恢复全部显示。

堆叠类型

类型说明检测方式
Burstburst连拍模式下的快速序列自动(规则引擎:默认基于 EXIF 连拍/序列标签与文件名模式)
BracketbracketHDR 曝光包围预留——当前没有包围检测逻辑,Create Stack始终创建 Manual 堆叠
Manualmanual任意手动归组手动

类型定义在 PhotoStack.StackType。模型上还保留两个弃用类型raw_jpeg/live_photo,仅用于迁移可见性。

自动检测:规则引擎如何工作

连拍检测基于每用户独立存储的规则列表(JSON 存在用户配置的burst_detection_rules字段中),实现在 burst_detection_rules.py 与 stack_detection.py。规则分为两类:

默认启用(硬标准,确定性判定)

  • EXIF Burst Mode Tag— 读取MakerNotes:BurstMode/MakerNotes:ContinuousDrive,值为1/On/True/Yes/Continuous即命中,按"相机型号 + 秒级时间戳"生成分组键;
  • EXIF Sequence Number— 相机写入的序列号(MakerNotes:SequenceNumber等),有有效序列号即判定为连拍成员,同样按秒级时间戳分组;
  • Filename Burst Pattern— 文件名命名约定。内置五组预定义正则模式(见 burst_detection_rules.py 的BURST_FILENAME_PATTERNS):
模式名正则示例
burst_suffix_BURST\d+IMG_001_BURST001.jpg
sequence_suffix_\d{3,}$以 3 位以上序号结尾的文件
bracketed_sequence\(\d+\)$photo (1).jpgphoto (2).jpg
samsung_burst_\d{3}_COVER三星连拍封面图
iphone_burstIMG_\d{4}_\d+iPhone 连拍序列

默认禁用(软标准,估计性判定——可能对无关照片误分组)

  • Timestamp Proximity— 间隔在interval_ms内(默认 2000 毫秒)且默认要求同一相机(require_same_camera: true,以相机品牌_型号比对)的连续照片归为一组,算法见group_photos_by_timestamp()
  • Visual Similarity— 对时间序上相邻照片做感知哈希比较,汉明距离 ≤similarity_threshold(默认 15)则同组,算法见group_photos_by_visual_similarity()

此外源码中还预置了可选的额外规则(OTHER_RULES):仅匹配_BURST后缀的窄化规则、自定义正则文件名规则(custom_pattern),以及一条更宽松的 5 秒时间邻近规则(interval_ms: 5000且不要求同相机)——对应设置界面中可添加的"额外预设"。每条规则还支持condition_path/condition_filename/condition_exif(格式为TAG_NAME//正则)三个附加过滤条件。

SettingsBurst Detection Rules面板中可以启用/禁用/重排/增删规则;前端预定义规则列表来自api/defaultburstrulesapi/predefinedburstrules两个端点。规则修改在下一次运行Detect Stacks时生效。

检测流程的调用链

Organizing → Stacks页点击"Detect Stacks"触发检测,完整链路为:

  1. 前端调用POST /api/stacks/detect/(DetectStacksView),可选参数detect_bursts(默认 true);
  2. 视图通过async_task(batch_detect_stacks, user, options)将检测入队为后台任务,立即返回202 Accepted{"status": "queued"});
  3. batch_detect_stacks() 创建JOB_SCAN_PHOTOS类型的LongRunningJob用于进度跟踪,回调中上报{"stage": "burst_sequences", current, total, found}
  4. detect_burst_sequences()先清空该用户既有的BURST_SEQUENCE堆叠(保证重新检测不产生重复),再分两阶段执行:硬标准规则逐张读 EXIF 并归组,随后软标准规则在时间序照片上做邻近/相似度分组;
  5. 每个含 ≥2 张照片的组经PhotoStack.create_or_merge()创建堆叠——若组内照片已属于同类型堆叠则自动并入,并对连拍堆叠记录sequence_start/sequence_end时间跨度;
  6. 封面自动选择(auto_select_primary()):连拍/包围取时间中点那张,手动堆叠取分辨率最高(宽×高最大)那张。

Organizing 页面与手动堆叠

Organizing页(导航可达)是管理堆叠与重复照片的中心枢纽,含两个标签页:

Stacks 标签页
  • 浏览全部检测出的堆叠;按类型过滤(下拉框提供All Types及你实际存在的每种类型);
  • 列表接口(GET /api/stacks/,PhotoStackListView)支持stack_typepagepage_size(默认 20,上限 100)参数,只返回含 ≥2 张照片的堆叠,每页附带前 4 张预览缩略图与封面信息;
  • 点击堆叠打开Stack Modal,展示组内全部照片及细节(分辨率、文件大小、相机、时间)——详情接口GET /api/stacks/{id}/还返回每张照片的file_variants列表;
  • Set CoverPOST /api/stacks/{id}/primary/传入photo_hash更换封面;
  • UnstackPOST /api/stacks/{id}/remove/移除照片(剩余不足 2 张时堆叠自动删除);DELETE /api/stacks/{id}/直接删除整个堆叠(只解除关联,不删照片);
  • View in Lightbox:在全屏灯箱中浏览堆叠照片。
创建手动堆叠
  1. 在任意视图中多选照片;
  2. 打开选中操作菜单(三点菜单);
  3. 点击"Create Stack"
  4. 选中照片被归入一个 manual 堆叠,时间线上仅留封面照片。

后端 CreateManualStackView 要求至少 2 张去重后的唯一照片;若其中任一张已属于手动堆叠,新照片会并入该既有堆叠而非新建——这保证了"一张照片同一时间只在一个手动堆叠里"。

管理堆叠

选中操作菜单还提供:

  • Merge StacksPOST /api/stacks/merge/把包含选中照片的多个 manual 堆叠合并为一个(MergeStacksView会先找到相关 manual 堆叠,逐个调用模型的merge_with()迁移照片关联并删除空堆叠);
  • Break Apart Stacks— 把选中照片从所属的每个 manual 堆叠中移出;堆叠剩余不足 2 张时自动删除(RemoveFromStackView中可见该逻辑,移除封面后会触发auto_select_primary()重选)。

Lightbox 中的堆叠

查看属于堆叠的照片时:

  • 侧栏Stacks区块显示堆叠内其他照片的缩略图预览,点击即可切换;
  • 点击"View full stack"打开 Stack Modal;
  • 若照片同时属于多个堆叠,会按类型分组的折叠面板(accordion)展示。

验证与延伸阅读

堆叠与文件变体的行为有完整的测试覆盖,位于 tests/stacks/:test_burst_detection.py(连拍检测)、test_burst_rules_engine.py(规则引擎)、test_live_photo_detection.py(Live Photo)、test_stack_api_endpoints.py(API 端点)等;RAW 合并修复逻辑的测试见 test_repair_ungrouped_file_variants.py。API 路由定义集中在 urls.py(^api/stacks/...一组路径)。

适用前提:本文基于当前仓库的扫描架构与规则引擎源码。连拍检测依赖用户 EXIF 中实际存在的相机厂商标签(MakerNotes),不同相机厂商对 BurstMode/SequenceNumber 的写入支持不一,此时可启用软标准规则或自定义文件名正则作为补充;包围(Bracket)堆叠目前仅有类型预留,尚无可自动检测的创建路径。

【免费下载链接】librephotosA self-hosted open source photo management service.项目地址: https://gitcode.com/GitHub_Trending/li/librephotos

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询