LibrePhotos模型训练数据集构建:数据采集与标注流程

LibrePhotos模型训练数据集构建:数据采集与标注流程

【免费下载链接】librephotos A self-hosted open source photo management service. This is the repository of the backend. 【免费下载链接】librephotos 项目地址: https://gitcode.com/GitHub_Trending/li/librephotos

引言:解决自托管相册的AI模型训练痛点

你是否还在为自托管相册的AI功能准确率不足而烦恼?当你使用LibrePhotos时,是否遇到过人脸识别错误、场景分类混乱的问题?本文将系统讲解如何构建高质量的模型训练数据集,从原始媒体文件到标注完成的训练样本,全程开源可复现。读完本文,你将掌握:

  • 媒体文件自动化采集与清洗的完整流程
  • EXIF元数据与视觉特征的融合提取技术
  • 人脸与场景标签的半自动化标注方案
  • 符合MLOps标准的数据集版本管理方法

数据采集:从文件系统到结构化存储

1. 目录扫描与媒体文件识别

LibrePhotos采用深度优先遍历算法扫描用户指定目录,通过directory_watcher.py实现核心逻辑:

def walk_directory(directory, callback):
    for file in os.scandir(directory):
        fpath = os.path.join(directory, file)
        if not is_hidden(fpath) and not should_skip(fpath):
            if os.path.isdir(fpath):
                walk_directory(fpath, callback)
            else:
                callback.append(fpath)

关键过滤机制包括:

  • 隐藏文件排除:通过is_hidden函数跳过以.开头的文件
  • 路径规则过滤:支持用户自定义SKIP_PATTERNS环境变量
  • 媒体类型验证is_valid_media函数检查文件扩展名与MIME类型
def is_valid_media(path):
    valid_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.mp4', '.mov'}
    return os.path.splitext(path)[1].lower() in valid_extensions

2. 文件去重与哈希计算

系统采用SHA-256哈希算法对媒体文件进行唯一标识,通过calculate_hash函数实现:

def calculate_hash(user, path):
    hash_obj = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            hash_obj.update(chunk)
    return hash_obj.hexdigest()

去重逻辑在create_new_image函数中实现,通过比对文件哈希判断重复:

photos: QuerySet[Photo] = Photo.objects.filter(Q(image_hash=hash))
if not photos.exists():
    # 创建新照片记录
else:
    # 仅添加文件引用,不重复创建
    file = File.create(path, user)
    photo.files.add(file)

3. EXIF元数据提取与结构化

EXIF数据通过独立微服务exif/main.py处理,采用Flask构建API服务:

@app.route("/get-tags", methods=["POST"])
def get_tags():
    data = request.get_json()
    files_by_reverse_priority = data["files_by_reverse_priority"]
    tags = data["tags"]
    struct = data["struct"]
    
    et = static_struct_et if struct else static_et
    if not et.running:
        et.start()
        
    values = []
    for tag in tags:
        value = None
        for file in files_by_reverse_priority:
            retrieved_value = et.get_tag(tag, file)
            if retrieved_value is not None:
                value = retrieved_value
        values.append(value)
    return {"values": values}, 201

支持提取的关键元数据包括:

  • 地理位置信息(GPS经纬度)
  • 拍摄时间与设备信息
  • 图像尺寸与分辨率
  • 光圈、快门速度等摄影参数

数据采集流程图

mermaid

数据清洗:确保训练数据质量

1. 无效文件过滤机制

系统通过多层过滤确保仅高质量媒体进入训练集:

def is_valid_media(path):
    if not os.path.isfile(path):
        return False
        
    # 检查文件扩展名
    ext = os.path.splitext(path)[1].lower()
    valid_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.mp4', '.mov'}
    if ext not in valid_extensions:
        return False
        
    # 检查文件大小 (至少10KB)
    if os.path.getsize(path) < 10 * 1024:
        return False
        
    return True

2. 重复文件处理策略

LibrePhotos采用三级去重机制:

去重级别实现方法应用场景
完全重复SHA-256哈希比对完全相同的文件
相似重复感知哈希(PHash)不同尺寸/压缩率的同一张照片
近重复CLIP特征向量余弦相似度相似场景但不同拍摄的照片

代码实现位于api/image_similarity.py

def build_image_similarity_index(user):
    photos = Photo.objects.filter(owner=user, clip_embeddings__isnull=False)
    embeddings = [np.array(photo.get_clip_embeddings()) for photo in photos]
    index = faiss.IndexFlatL2(len(embeddings[0]))
    index.add(np.array(embeddings))
    
    # 保存索引供后续查询
    faiss.write_index(index, f"{settings.MEDIA_ROOT}/similarity_index_{user.id}.index")

3. 异常值检测与修复

系统自动检测并修复常见的元数据异常:

def fix_metadata_issues(photo):
    # 修复缺失的时间戳
    if not photo.exif_timestamp:
        photo.exif_timestamp = infer_timestamp_from_filename(photo.main_file.path)
        
    # 修复地理位置异常值
    if photo.exif_gps_lat and (photo.exif_gps_lat < -90 or photo.exif_gps_lat > 90):
        photo.exif_gps_lat = None
        
    # 确保宽高比合理
    if photo.width and photo.height and photo.width / photo.height > 10:
        photo.width, photo.height = photo.height, photo.width

数据标注:构建高质量标签体系

1. 人脸检测与特征提取

LibrePhotos使用MTCNN进行人脸检测,ResNet-50提取特征:

def create_face_encodings(image_path, face_locations):
    # 加载图像
    image = face_recognition.load_image_file(image_path)
    
    # 提取人脸特征
    encodings = face_recognition.face_encodings(image, face_locations)
    return encodings

人脸特征存储在Face模型中:

class Face(models.Model):
    photo = models.ForeignKey(Photo, related_name="faces", on_delete=models.CASCADE)
    location_top = models.IntegerField()
    location_bottom = models.IntegerField()
    location_left = models.IntegerField()
    location_right = models.IntegerField()
    encoding = models.TextField()  # 存储128维特征向量的十六进制表示
    
    def generate_encoding(self):
        self.encoding = get_face_encodings(
            self.photo.thumbnail.thumbnail_big.path,
            [(self.location_top, self.location_right, self.location_bottom, self.location_left)]
        )[0].tobytes().hex()
        self.save()

2. 场景分类自动标注

采用Places365模型进行场景分类标注:

def inference_places365(img_path, confidence=0.3):
    # 加载预训练模型
    model = Places365()
    model.load()
    
    # 推理场景类别
    result = model.inference_places365(img_path, confidence)
    
    return {
        "environment": result["environment"],  # indoor/outdoor
        "categories": result["categories"],    # 场景类别
        "attributes": result["attributes"]     # 场景属性
    }

标注结果存储在PhotoCaption模型:

class PhotoCaption(models.Model):
    photo = models.OneToOneField(Photo, on_delete=models.CASCADE, related_name="caption_instance")
    captions_json = models.JSONField(blank=True, null=True)
    
    def generate_places365_captions(self, commit=True):
        result = inference_places365(self.photo.main_file.path)
        self.captions_json = {"places365": result}
        if commit:
            self.save()

3. 半自动化标注流程

LibrePhotos实现了人机协同的标注流程:

mermaid

4. 标注质量控制机制

为确保标注质量,系统实现了多维度验证:

def validate_annotations(photo):
    issues = []
    
    # 人脸标注验证
    faces = Face.objects.filter(photo=photo)
    if len(faces) > 0 and not any(face.person for face in faces):
        issues.append("无人脸标注信息")
        
    # 场景标注置信度检查
    if (photo.caption_instance and 
        photo.caption_instance.captions_json and 
        photo.caption_instance.captions_json.get("places365")):
        categories = photo.caption_instance.captions_json["places365"]["categories"]
        if not categories or len(categories) == 0:
            issues.append("场景分类结果为空")
            
    return issues

数据集组织:为模型训练做准备

1. 数据集目录结构

LibrePhotos采用标准化的数据集结构:

dataset/
├── train/                 # 训练集
│   ├── images/            # 图像文件
│   ├── faces/             # 人脸裁剪图像
│   └── annotations/       # 标注文件
├── val/                   # 验证集
│   ├── ...
└── test/                  # 测试集
    ├── ...

2. 数据划分策略

采用分层抽样确保各子集分布一致:

def split_dataset(photos, train_ratio=0.7, val_ratio=0.2):
    # 按时间分层抽样
    photos = photos.order_by('exif_timestamp')
    total = len(photos)
    
    train_end = int(total * train_ratio)
    val_end = train_end + int(total * val_ratio)
    
    return {
        'train': photos[:train_end],
        'val': photos[train_end:val_end],
        'test': photos[val_end:]
    }

3. 数据集元数据文件

生成符合COCO格式的标注文件:

def export_coco_format(photos, split_name, output_dir):
    annotations = {
        "info": {"description": f"LibrePhotos {split_name} dataset"},
        "images": [],
        "annotations": [],
        "categories": []
    }
    
    # 填充图像信息
    for idx, photo in enumerate(photos):
        annotations["images"].append({
            "id": idx,
            "file_name": photo.image_hash + ".jpg",
            "width": photo.width,
            "height": photo.height,
            "date_captured": photo.exif_timestamp.isoformat() if photo.exif_timestamp else None
        })
        
        # 填充人脸标注
        for face_idx, face in enumerate(photo.faces.all()):
            annotations["annotations"].append({
                "id": face_idx,
                "image_id": idx,
                "category_id": face.person.id if face.person else 0,
                "bbox": [
                    face.location_left,
                    face.location_top,
                    face.location_right - face.location_left,
                    face.location_bottom - face.location_top
                ],
                "area": (face.location_right - face.location_left) * (face.location_bottom - face.location_top),
                "iscrowd": 0
            })
    
    # 保存标注文件
    with open(f"{output_dir}/{split_name}_annotations.json", "w") as f:
        json.dump(annotations, f, indent=2)

高级优化:提升数据集质量的技术

1. 数据增强策略

虽然LibrePhotos核心代码中未直接实现数据增强,但可通过扩展实现:

def apply_data_augmentation(image_path, output_dir):
    image = Image.open(image_path)
    
    # 生成变换后的图像
    transforms = [
        lambda x: x.rotate(0),   # 原图
        lambda x: x.rotate(90),  # 旋转90度
        lambda x: x.rotate(180), # 旋转180度
        lambda x: x.transpose(Image.FLIP_LEFT_RIGHT),  # 水平翻转
        lambda x: transforms.RandomResizedCrop(size=(224, 224), scale=(0.8, 1.0))(x)
    ]
    
    # 保存增强后的图像
    base_name = os.path.splitext(os.path.basename(image_path))[0]
    for i, transform in enumerate(transforms):
        augmented_image = transform(image)
        augmented_image.save(f"{output_dir}/{base_name}_aug_{i}.jpg")

2. 主动学习策略

通过模型不确定性指导标注优先级:

def select_uncertain_samples(model, unlabeled_photos, k=100):
    # 获取模型预测概率
    predictions = []
    for photo in unlabeled_photos:
        features = np.array(photo.get_clip_embeddings())
        pred = model.predict_proba([features])[0]
        predictions.append((photo, pred))
    
    # 选择预测不确定性最高的样本
    predictions.sort(key=lambda x: -np.max(x[1]))  # 按最大概率排序
    return [p[0] for p in predictions[:k]]  # 返回最不确定的前k个样本

数据集版本管理与应用

1. 版本控制实现

采用Git LFS管理大型数据集文件:

# 初始化数据集仓库
git init dataset_repo
cd dataset_repo
git lfs install

# 追踪大型文件
git lfs track "*.jpg"
git lfs track "*.png"
git lfs track "*.mp4"
git add .gitattributes

# 提交数据集版本
git add .
git commit -m "Initial dataset version: 10k photos with face annotations"
git tag -a v1.0 -m "Version 1.0: 10k photos"
git push origin v1.0

2. 数据集使用流程

模型训练时加载数据集:

def load_dataset(split_name, data_dir):
    # 加载图像路径
    image_dir = os.path.join(data_dir, split_name, "images")
    annotation_path = os.path.join(data_dir, split_name, "annotations.json")
    
    # 加载标注文件
    with open(annotation_path, "r") as f:
        annotations = json.load(f)
        
    # 创建数据集对象
    return PhotoDataset(
        image_dir=image_dir,
        annotations=annotations,
        transform=transforms.Compose([
            transforms.Resize((224, 224)),
            transforms.ToTensor(),
            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
        ])
    )

3. 性能评估与反馈

数据集质量评估指标:

def evaluate_dataset_quality(dataset):
    # 计算标注覆盖率
    total_photos = len(dataset)
    photos_with_faces = sum(1 for p in dataset if len(p.faces.all()) > 0)
    photos_with_scene_tags = sum(1 for p in dataset if p.caption_instance and p.caption_instance.captions_json)
    
    return {
        "total_photos": total_photos,
        "face_annotation_coverage": photos_with_faces / total_photos,
        "scene_annotation_coverage": photos_with_scene_tags / total_photos,
        "avg_faces_per_photo": sum(len(p.faces.all()) for p in dataset) / total_photos,
        "label_distribution": get_label_distribution(dataset)
    }

总结与展望

LibrePhotos通过自动化的数据采集、清洗和标注流程,构建了高质量的模型训练数据集。核心优势包括:

  1. 全自动化流水线:从文件扫描到特征提取的端到端自动化
  2. 多模态数据融合:结合视觉特征、EXIF元数据和用户标注
  3. 人机协同标注:通过主动学习减少人工标注工作量

未来优化方向:

  • 引入更多模态数据(如音频、深度信息)
  • 开发交互式标注工具提升标注效率
  • 构建跨数据集的知识迁移机制

通过本文介绍的方法,你可以构建自己的高质量媒体数据集,显著提升LibrePhotos的AI功能表现。建议从10k-50k规模的数据集开始,逐步迭代优化,同时关注数据多样性和标注质量的平衡。

如果你觉得本文对你有帮助,请点赞、收藏并关注项目更新。下一篇我们将深入探讨基于此数据集的模型训练与调优实践。

【免费下载链接】librephotos A self-hosted open source photo management service. This is the repository of the backend. 【免费下载链接】librephotos 项目地址: https://gitcode.com/GitHub_Trending/li/librephotos

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

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值