拓冰建站拓冰建站
首页 / 资讯中心 / 正文

苹果照片转VOC2007数据集:Python处理与HEIC转换全攻略

简介面向使用YOLOv3开展目标识别与检测的开发者与学习者这套苹果目标检测数据集及配套Python处理代码可直接服务于模型训练、验证与迁移学习。资源共2000个文件涵盖1648张jpg图片、820个xml标注文件、4个txt文件与3个Python脚本压缩包约90.81MB。其中414张原始苹果图片通过数据增强、尺寸调整和填充处理扩展为828张预处理图片XML标注由LabelImg以预选框形式生成遵循VOC2007格式Python代码负责数据转换与预处理流程。相较从零收集图像和手工标注这套资源可显著缩短数据准备周期帮助读者直接进入模型训练环节并通过增强后的样本提升模型鲁棒性与泛化能力。目前已有1073人学习下载适合需要实验数据或快速搭建YOLOv3训练流程的目标检测入门者与算法工程师。1. 苹果照片数据集和python处理代码把手机相册变成能直接训练的VOC2007说到「苹果照片数据集和python处理代码-VOC2007.zip」这个标题我第一反应不是它有多漂亮而是「坑」VOC2007 格式本身不复杂复杂的是把 iPhone 或 Mac 照片应用里的照片变成一份能喂进目标检测框架的数据集。一千张照片导出来日期是乱的HEIC 和 JPEG 混在一起Live Photo 会把同一张照片在文件系统里拆成两半标注 XML 里一个字段类型写错训练 Loss 就能飞到天上去。这个压缩包想解决的就是「照片就在那里但没法直接训模型」这条最后一公里。适合谁手里有一批苹果设备拍摄的照片想自建目标检测或图像分类数据集的人以及已经把 VOC2007 当标准格式但不想用 LabelImg 一张张手动整理的从业者。我下面写的内容就是按这个标题拆出来的完整落地路径。2. 苹果照片库的真相.photoslibrary 里到底存了什么2.1 为什么不能直接拿 Python 读 Photos.sqlite很多人拿到.photoslibrary之后第一件事是打开Photos.sqlite想直接查表拿照片路径。这个思路听起来省事但实际会撞上两道墙。第一道墙是权限。macOS 从 Catalina 开始对照片库这类受保护目录有透明的访问控制终端里跑 Python 第一次读可能被静默拒绝第二次直接抛权限错误。第二道墙是数据模型Photos.sqlite里的ZASSET表存的是资源记录但图片文件本身放在originals目录下文件名不是IMG_0001.jpg这种人类可读形式而是长度 30 多位的大写十六进制串如499F65BB-...。更麻烦的是同一张照片可能同时存在「原图」和「编辑后版本」两者各占一个文件你怎么知道哪份是你想要的所以我的建议是不要直接解析 sqlite把它当成黑匣子。借助系统本身的能力导出这样权限、文件对应关系都由系统机制保证。常见做法是# 在 macOS 上打开照片应用选中要导出的相册 # 用 文件 - 导出 - 导出未修改的原片 # 导出时选择 JPEG 格式取消勾选「包含 IPTC 信息」这样导出的是一整套干净的 JPEG 文件命名规律、没有隐藏编辑版本。如果你手里的照片源不在 macOS 上而是一堆 iPhone 拍完传到 Windows 的目录那更简单直接拿文件列表当数据源反而绕过照片库这层麻烦。2.2 用 Python 快速列出现有照片的最小脚本无论照片来自哪里处理的第一步都是先建立一个「照片清单」包含路径、拍摄时间、尺寸、格式。我一般会用这段代码起底import os import json from PIL import Image PHOTO_DIR ./photos_raw # 照片统一放在这个目录 output [] for root, _, files in os.walk(PHOTO_DIR): for f in files: if f.lower().endswith((.jpg, .jpeg, .png)): path os.path.join(root, f) try: with Image.open(path) as img: w, h img.size output.append({ path: path, width: w, height: h, mtime: os.path.getmtime(path) }) except Exception as e: print(f[skip] {path}: {e}) with open(photo_manifest.json, w) as f: json.dump(output, f, indent2, ensure_asciiFalse) print(f共处理 {len(output)} 张照片)逻辑很直白遍历目录收集指定后缀用 Pillow 读取尺寸和修改时间最后落一份photo_manifest.json。这样做的意义在于后面对接 VOC2007 的 XML 时所有标注都锚定这份 manifest 里的路径不再跟真实文件系统纠缠。参数上要注意两点一是后缀过滤苹果生态里经常出现.heic这段代码默认不处理后面会单独讲转换二是mtime可以作为后续增量同步的凭据避免每次全量重扫。2.3 导出时保留哪些字段时间、位置、相册名三件套导出照片只是拿到图片标注才是重头戏。如果你打算从零标注我建议在导出阶段就同步保存一个元数据文件把拍摄时间和位置信息一起带出来。苹果照片应用导出时勾选「包含 IPTC 信息」导出的 JPEG 里就会写入拍摄时间、GPS 坐标、相机型号等 EXIF 字段。用 Python 可以很方便地提取from PIL import Image from PIL.ExifTags import TAGS with Image.open(sample.jpg) as img: exif img._getexif() or {} for tag_id, v in exif.items(): tag TAGS.get(tag_id, tag_id) if tag in (DateTimeOriginal, GPSInfo, Model): print(tag, v)为什么特意保留这些字段因为做数据集时时间可以帮你按时间段划分 train/val 避免数据泄漏位置信息可以帮你排查某些类别是否只在特定地点出现——比如你拍的「停车位」全在公司楼下模型就会学出地点偏见。相册名则对应一个粗分类标签前期可以做类别预标注的雏形。提示VOC2007 数据集本身只有一个类别列表和 XML 标注时间/位置这些字段它不关心但它们能帮你建立一套更可控的数据管线。3. 把照片转成 VOC2007 标注从路径清单到 XML 的一站式脚本3.1 VOC2007 到底规定了什么目录结构和字段语义PASCAL VOC2007 的目录约定是固定的跑不通的代码十有八九是目录层级没对上VOC2007/ ├── JPEGImages/ # 所有原始图片统一为 .jpg ├── Annotations/ # 每张图对应的 .xml 标注 ├── ImageSets/Main/ # 训练/验证/测试划分文件每行一个不带后缀的图片名 ├── SegmentationClass/ └── SegmentationObject/训练目标检测模型时真正用到的只有前三个目录。标注 XML 的结构长这样annotation folderVOC2007/folder filename000001.jpg/filename sourcedatabaseMyPhotoDB/database/source size width1920/width height1080/height depth3/depth /size segmented0/segmented object namecar/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin120/xmin ymin80/ymin xmax640/xmax ymax560/ymax /bndbox /object /annotation注意filename只写文件名不写路径size必须和图片真实尺寸一致否则不少框架会在读取时抛出宽高不匹配的诡异报错object可有多个对应图片里的多个目标。3.2 一个可复用的 CSV 标注转 XML 脚本日常工作中我一般这么做先用 LabelImg 或标注小工具生成 CSV 格式的标注路径、x1、y1、x2、y2、类别再写脚本统一转成 VOC XML。因为 CSV 比直接写 XML 容易维护错了也方便批量改。转换脚本骨架如下import csv import os from PIL import Image from xml.etree.ElementTree import Element, SubElement, tostring def build_xml(image_path, size, objects): folder os.path.basename(os.path.dirname(image_path)) filename os.path.basename(image_path) annotation Element(annotation) SubElement(annotation, folder).text folder SubElement(annotation, filename).text filename size_elm SubElement(annotation, size) SubElement(size_elm, width).text str(size[0]) SubElement(size_elm, height).text str(size[1]) SubElement(size_elm, depth).text str(size[2]) SubElement(annotation, segmented).text 0 for obj in objects: o SubElement(annotation, object) SubElement(o, name).text obj[name] SubElement(o, pose).text Unspecified SubElement(o, truncated).text str(obj.get(truncated, 0)) SubElement(o, difficult).text str(obj.get(difficult, 0)) bb SubElement(o, bndbox) SubElement(bb, xmin).text str(int(obj[xmin])) SubElement(bb, ymin).text str(int(obj[ymin])) SubElement(bb, xmax).text str(int(obj[xmax])) SubElement(bb, ymax).text str(int(obj[ymax])) return tostring(annotation, encodingunicode) # 用法逐行读 CSV调用 build_xml 后写盘 with open(annotations.csv, r) as f: reader csv.DictReader(f) for row in reader: img Image.open(row[image_path]) xml_str build_xml(row[image_path], img.size, [ {name: row[class], xmin: row[x1], ymin: row[y1], xmax: row[x2], ymax: row[y2]} ]) xml_name os.path.splitext(os.path.basename(row[image_path]))[0] .xml with open(os.path.join(Annotations, xml_name), w) as out: out.write(xml_str)逻辑说明build_xml里字段名与 VOC 官方 DTD 对齐truncated目标是否被图片边界截断和difficult是否难以识别从 CSV 中可配置默认 0。脚本末尾把 XML 写到Annotations/目录。参数说明pose在 VOC2007 里是Unspecified、Left、Right、Frontal、Rear等苹果照片的视角信息很难自动推断一律填Unspecified即可。truncated的意义如果你拍的照片里目标边缘被裁到画面外标 1否则标 0。不填或乱填会让模型在边界框回归时被误导。3.3 生成 ImageSets/Main 划分文件的规范姿势训练和验证的划分文件决定了哪些图进 train哪些进 val。常见误区是直接拿文件名随机切结果某类目标几乎全在 train 里val 上指标虚高。我用的是分层划分保证每个类别在两边都有分布import os import random from collections import defaultdict from xml.etree.ElementTree import parse xml_dir Annotations class_index defaultdict(list) for xml_file in os.listdir(xml_dir): root parse(os.path.join(xml_dir, xml_file)).getroot() filename root.findtext(filename).replace(.jpg, ) for obj in root.iter(object): class_index[obj.findtext(name)].append(filename) train, val [], [] for cls, samples in class_index.items(): random.shuffle(samples) split int(len(samples) * 0.8) train.extend(samples[:split]) val.extend(samples[split:]) with open(ImageSets/Main/train.txt, w) as f: f.write(\n.join(sorted(set(train)))) with open(ImageSets/Main/val.txt, w) as f: f.write(\n.join(sorted(set(val))))划分逻辑的核心是defaultdict按类别累积图片名再每类独立 8:2 切分。这样即便你只有 50 张「猫」的照片train 和 val 里也都会有猫不会出现 val 里没有猫类导致无法计算 mAP 的窘况。注意训练脚本读这些 txt 时要求文件名不带扩展名、每行一个与 VOC 官方一致。4. 训练前的最小校验用 OpenCV 扫一遍数据集别急着烧显卡4.1 五类常见错误的自动检查脚本在启动模型训练之前用几分钟跑一个校验脚本能替你省下数小时定位黑匣子问题的精力。我把这五类检查固化成一个脚本每次数据集构建完成后必跑import os import cv2 from xml.etree.ElementTree import parse JPEG_DIR JPEGImages XML_DIR Annotations issues [] for xml_name in os.listdir(XML_DIR): xml_path os.path.join(XML_DIR, xml_name) root parse(xml_path).getroot() # 1. 图片文件是否存在 img_name root.findtext(filename) img_path os.path.join(JPEG_DIR, img_name) if not os.path.exists(img_path): issues.append(f[缺失图片] {xml_name} - {img_name}) continue # 2. XML 里的宽高是否与图片真实值一致 img cv2.imread(img_path) if img is None: issues.append(f[不可读] {img_path}) continue ih, iw img.shape[:2] xml_w int(root.findtext(size/width)) xml_h int(root.findtext(size/height)) if (ih, iw) ! (xml_h, xml_w): issues.append(f[尺寸不符] {img_name} xml({xml_w},{xml_h}) actual({iw},{ih})) # 3. 框是否越界 for obj in root.iter(object): xmin int(obj.findtext(bndbox/xmin)) ymin int(obj.findtext(bndbox/ymin)) xmax int(obj.findtext(bndbox/xmax)) ymax int(obj.findtext(bndbox/ymax)) if xmin 0 or ymin 0 or xmax iw or ymax ih or xmax xmin or ymax ymin: issues.append(f[框越界] {img_name} ({xmin},{ymin},{xmax},{ymax})) # 4. 是否有空标注且类别缺失 class_set set() for obj in root.iter(object): class_set.add(obj.findtext(name)) # 5. 类别数量统计 print(f共发现 {len(issues)} 个问题) for issue in issues[:20]: print(issue)这段脚本里cv2.imread承担两重角色一是确认图片不是损坏文件二是拿到真实宽高。xml.findtext(size/width)是 ElementTree 的路径写法专门取size下的width。框架读 VOC 时如果宽高写错轻则警告重则直接丢弃该图片所以这项必须校验。检查逻辑还有一个隐藏好处cv2.imread对 HEIC 无能为力图片不可读时它会返回None从而把「格式没转」这个问题暴露出来。提示脚本里我只打印前 20 个问题是防止几千行的报错刷屏修正一批后重跑直到issues为空。4.2 批量把 HEIC 转成 JPEGcv2 之外的安心选择从苹果设备导出的照片常常是 HEIC 格式OpenCV 读不了NonePillow 也要靠新版才支持。要喂给 VOC 流水线最好提前统一转成 JPEG。我习惯用pyheif辅助import pyheif from PIL import Image def heic_to_jpeg(src, dst): heif_file pyheif.read(src) img Image.frombytes( heif_file.mode, (heif_file.width, heif_file.height), heif_file.data, raw, heif_file.mode, heif_file.stride, ) img.save(dst, JPEG, quality95) # 示例把单张 HEIC 转换 heic_to_jpeg(IMG_001.HEIC, IMG_001.jpg)参数说明quality95是因为目标检测对细节敏感压到 80 以下小目标可能直接消失如果数据量巨大想省磁盘再单独评估降质量的收益。stride必须从 HEIF 文件信息中读取否则色彩通道会错位成花屏。这条执行要放在 4.1 校验脚本之前否则不可读的 HEIC 会一直刷报错。4.3 用 XML 统计类别分布发现标注不平衡另一个容易被忽略的点是类别分布。训练目标检测模型如果「轿车」有 3000 个框「卡车」只有 30 个这 30 个框基本不会对模型产生有效梯度。统计起来很简单python -c from xml.etree.ElementTree import parse import glob, collections c collections.Counter() for x in glob.glob(Annotations/*.xml): for obj in parse(x).iter(object): c[obj.findtext(name)] 1 for k, v in c.most_common(): print(f{k:20s} {v}) 如果发现分布极不均衡要考虑三个方向给稀有类别过采样、使用类别权重、或者直接删掉数量太少的类别避免模型学到「见到卡车就当背景」的错误先验。这是训练习惯问题但发生在数据准备阶段。5. 避坑记录从苹果相册到 VOC 的三次翻车现场5.1 复制到一半发现图片变少了Live Photo 的隐藏拆解现象从照片应用导出的目录里同一张照片的.JPG和.MOV成对出现目标检测只需要 JPG但脚本把 MOV 当成未知文件跳过数量对不上。更隐蔽的是某些导出工具的「原片」会拆出一个单独的静帧导致同一场景出现两张几乎一样的图。原因苹果的 Live Photo 本质是一个静态图加一段三秒视频导出未修改原片时会同时输出两份。数据分布里若塞进大量运动前后帧训练时相似样本会过拟合到「位置微变」上。解决导出时在照片应用里关闭「包含 Live Photo 的完整内容」只导出静态图像或者导出后按文件扩展名过滤只保留.jpg/.jpeg。如果已经导出用fdupes去重或者按时间戳 尺寸合并近似样本。5.2 训练 Loss 不收敛XML 里的「0」被写成了「0.0」现象用 SSD 或 YOLO 训练时 Loss 在初始值附近震荡偶尔夹杂 NaN排查了半天网络结构最后发现是 XML 里坐标被写成了浮点字符串。原因转换脚本里str(xmin)没问题但str(float_val)会在某些字段上输出1920.0。部分框架的 VOC 解析器按整数解析bndbox字段遇到小数点直接赋值失败画出的框全变成 0。解决写 XML 时强制int()包裹所有坐标字段校验脚本里增加正则检查凡bndbox出现.0直接报错。这是我从血泪教训里固化下来的规则VOC 的bndbox永远是整数不妥协。5.3 数据同步覆盖标注全错位文件同名问题现象iPhone 和 Mac 都有IMG_0001这样的命名规则把两个目录合并后后拷贝的覆盖了先拷贝的但Annotations里的 XML 还指向旧图片导致训练时 cat 和 dog 张冠李戴。原因苹果相册的导出命名并非全局唯一特别是「最近项目」和「个人收藏」两个相册各导一次时会得到重复文件名。解决合并前统一重命名用哈希值或 UUID 作为文件名前缀python -c import os, hashlib for f in os.listdir(photos_raw): src os.path.join(photos_raw, f) if f.lower().endswith(.jpg): h hashlib.md5(open(src,rb).read()).hexdigest()[:12] dst os.path.join(photos_raw, h .jpg) if not os.path.exists(dst): os.rename(src, dst) 这样保证文件名全局唯一标注维护时只需关心映射表不会被同名覆盖坑到。5.4 权限黑洞第一次能跑挂第二次干脆无权限现象同样的读取脚本昨天跑得好好的今天再运行就报PermissionError目录能列但文件打不开。原因macOS 的隐私保护会在照片库或「下载」目录上随机弹出访问授权一旦选错或超时终端的权限行为会变得不确定。写死在照片库路径里的脚本也会在系统迁移后失效。解决外围策略是「先复制、后处理」——用 Finder 把照片导出到项目目录然后所有脚本只操作项目目录不直接读.photoslibrary。这样就算系统权限变化也不会影响已经落盘的数据脚本运行环境稳定得多。5.5 标注框与图片真实内容错位EXIF 旋转角现象手机横拍的照片在预览里正常但用cv2.imread读出来是旋转 90° 的XML 里的坐标点全部错位。原因iPhone 在存储时经常把「方向」放在 EXIF 的Orientation标签里像素数据本身是横的。OpenCV 不读 EXIF直接用原始像素导致检测框对不上。解决在数据集构建阶段统一做一次「EXIF 方向归一化」用 Pillow 的ImageOps.exif_transpose一次性矫正方向再保存之后再交给 OpenCV 处理就没有旋转问题了。顺便把这张图的 XML 里size的宽高也重新计算一遍。6. 一个进阶技巧用 Python 做增量同步和类别改名数据集构建不是一锤子买卖我每个月会往里面补充照片。全量导出既慢又占空间所以我把增量同步和类别维护这两个高频操作脚本固化下来。增量同步的思路基于 2.2 节 manifest 里的mtime字段。每次同步时先扫描照片源目录只拷贝修改时间晚于上次记录的文件import os, json, shutil with open(photo_manifest.json) as f: manifest json.load(f) last_cutoff manifest[last_sync_time] src_dir photos_inbox dst_dir JPEGImages new_count 0 for f in os.listdir(src_dir): src os.path.join(src_dir, f) if os.path.getmtime(src) last_cutoff and f.lower().endswith(.jpg): shutil.copy2(src, os.path.join(dst_dir, f)) new_count 1 manifest[last_sync_time] time.time() with open(photo_manifest.json, w) as f: json.dump(manifest, f, indent2) print(f新增 {new_count} 张)逻辑上把last_sync_time存回 manifest 充当书签下次从断点续传。注意shutil.copy2会保留原文件的 mtime如果你依赖文件系统时间做判断要在拷贝完成后单独标记。第二个高频操作是类别改名。比如一开始我把所有「自行车」标成bicycle后来发现数据里还包括电动车想统一把旧标签bicycle批量改成two_wheeled。直接改 XML 文本节点就行from xml.etree.ElementTree import parse import os rename_map {bicycle: two_wheeled} for xml_name in os.listdir(Annotations): path os.path.join(Annotations, xml_name) tree parse(path) root tree.getroot() for obj in root.iter(object): name obj.findtext(name) if name in rename_map: obj.find(name).text rename_map[name] tree.write(path, encodingutf-8, xml_declarationTrue)改名的同时记得同步labelmap.txt或者模型配置里的类别文件否则框架会按索引读标签训练和推理两边对不上。我自己的习惯是每轮数据集更新完先跑 4.1 的校验脚本再跑一次类别分布统计然后才允许自己开训练任务。这个流程看起来多花五分钟实际上是把「发现标注问题」的时间点从训练后挪到训练前少几次烧卡重来就值回票价了。希望帮到你。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门