1. 为什么需要专门制作光照不均匀的人脸数据做过人脸相关项目的人大概都有过这种经历模型在实验室环境下跑出来的指标漂亮得不行换到真实场景里一测准确率直接掉一大截。问题往往不出在模型结构上而是出在训练数据的分布太干净了。真实世界里的人脸图像光照条件几乎不可能均匀——侧光、顶光、逆光、斑驳的树影、窗户投下的硬阴影这些才是常态。光照不均匀对人脸任务的影响是系统性的。以人脸识别为例同一张脸在不同光照下提取出的特征向量可能比两张不同的脸还要远这就是所谓的类内距离大于类间距离。以人脸关键点检测为例强阴影会让局部对比度骤降边缘检测算子直接失效关键点定位就会漂移。以人脸属性分析为例侧光造成的明暗分区会被误判成肤色差异影响年龄、性别等属性的判断。解决这个问题的思路有两条一是在算法层面做光照归一化二是让模型在训练阶段就见过足够多的光照变化。前者依赖预处理的质量后者依赖数据的多样性。而现实情况是公开的人脸数据集大多采集于受控环境光照条件相对单一直接拿来训练模型对光照的鲁棒性天然不足。所以自己动手制作光照不均匀的人脸数据就成了很多项目的刚需。这件事听起来简单——不就是给图片加个阴影吗但真做起来坑比想象中多得多。加得太假模型学到的是人造阴影的特征迁移到真实场景照样翻车加得太猛人脸关键信息被完全遮蔽标注都成了问题加得没有规律数据分布和真实场景对不上等于白做。这篇内容就围绕基于Python制作人脸光照不均匀数据这件事把我在实际项目中踩过的坑、验证过的方案、以及一套可以直接复用的代码思路完整梳理出来。适合正在做人脸检测、识别、关键点、属性分析等任务且需要扩充训练数据多样性的开发者参考。不管你是刚入门的新手还是已经做过几轮数据增强的老手应该都能从中找到一些能直接用的东西。2. 光照不均匀的物理成因与数据模拟的底层逻辑2.1 真实场景中光照不均匀到底是怎么产生的要把数据做真首先得搞清楚真实世界里的光照不均匀是怎么来的。从物理光学的角度看无非是几个因素叠加的结果。第一是光源方向性。点光源或方向性强的光源比如太阳、射灯照射到人脸这种近似凸曲面时不同朝向的表面接收到的光强差异巨大。正对光源的区域亮背对光源的区域暗中间过渡区形成渐变。这就是最典型的侧光效果。第二是遮挡产生的阴影。光源被人脸自身结构鼻子、眉骨、下巴或其他物体手、帽子、窗框、树叶遮挡时会在人脸表面投下硬阴影或软阴影。硬阴影边缘锐利软阴影边缘模糊两者的成因不同——硬阴影来自小尺寸光源或近距离遮挡软阴影来自大尺寸光源或远距离遮挡。第三是环境反射与漫射。光线打到墙面、桌面、衣物上再反射到脸上会形成二次照明。这种反射光通常带有环境物体的颜色且分布不均匀会造成色偏和亮度不均。第四是相机响应与曝光。同一场景下相机的自动曝光会以某个区域为基准进行调节导致其他区域过曝或欠曝。高光区域细节丢失暗部区域噪声放大这本身也是一种光照不均匀的表现。理解了这些成因模拟的时候就不能只是简单地叠一个渐变图层了事。真实的光照不均匀是方向性、遮挡、反射、曝光多种因素耦合的结果模拟方案需要尽可能覆盖这些维度。2.2 用Python模拟光照的三种技术路线对比在Python生态里模拟光照不均匀大致有三条技术路线各有适用场景。路线一基于图像处理的二维叠加。核心思路是在原图上叠加一个模拟光照分布的亮度图illumination map通过逐像素乘法或加法改变亮度。实现简单速度快可控性强。缺点是缺乏三维几何感阴影不会随人脸姿态变化看起来容易贴上去。路线二基于三维人脸重建的光照渲染。先用三维人脸模型重建出人脸的几何结构再用光照模型如Lambertian、Phong渲染出带光照的图像。真实感最强阴影会随姿态和光源方向自然变化。缺点是需要三维重建能力流程复杂计算量大且重建误差会引入新的问题。路线三基于生成模型的风格迁移。用GAN或扩散模型把一张均匀光照的人脸转换成不均匀光照的风格。真实感好能学到真实光照的复杂分布。缺点是需要训练数据可控性差容易产生伪影且生成结果不可复现。对于大多数项目来说路线一是性价比最高的选择。它不需要三维重建不需要训练生成模型用OpenCV和NumPy就能搞定而且参数完全可控方便做消融实验。路线二和路线三更适合对真实感要求极高的场景或者作为路线一的补充。下面重点讲路线一的具体实现同时会提到如何借鉴路线二和路线三的思路来提升真实感。2.3 光照图的设计原则从像到真的关键光照图illumination map是整件事的核心。它是一张和原图同尺寸的单通道或多通道图像每个像素的值代表该位置的光照强度系数。原图乘以这个系数就得到了光照变化后的图像。设计光照图时有几个原则必须遵守。原则一光照图应该是平滑的但可以有锐利边缘。真实光照的渐变是平滑的所以光照图的主体应该是低频的。但阴影边缘可以是锐利的所以光照图里可以包含高频的阶跃。这两者要分开处理不能混在一起。原则二光照图的值域要合理。系数为1表示原亮度小于1表示变暗大于1表示变亮。但变亮不能无限否则会过曝。一般来说系数范围控制在0.3到1.5之间比较安全。低于0.3的区域基本全黑人脸信息丢失严重高于1.5的区域容易过曝细节同样丢失。原则三光照图要和人脸结构对齐。如果只是随机生成一张光照图叠上去阴影可能落在不该落的地方比如鼻子上出现一块亮斑看起来就很假。理想情况下光照图应该参考人脸的几何结构让阴影落在合理的区域。原则四要保留一定的随机性。如果所有样本都用同一套光照图模型会过拟合到这套光照模式上。光照图应该在合理范围内随机变化覆盖不同的光源方向、强度、阴影形状。这四条原则说起来简单做起来需要反复调试。我见过不少项目光照数据做了一堆模型效果反而下降了问题往往就出在光照图设计得不合理——要么太假要么太极端要么分布太单一。3. 从零搭建光照不均匀数据生成流水线3.1 环境准备与依赖选择这套流程用到的库不多都是常规的计算机视觉工具。pip install opencv-python numpy scipy scikit-image简单说明一下每个库的角色。OpenCV负责图像的读写、几何变换、滤波等基础操作。NumPy负责矩阵运算光照图的生成和叠加都靠它。SciPy用来做高斯滤波和插值生成平滑的光照渐变。scikit-image提供一些额外的图像处理工具比如形态学操作和直方图匹配在后期微调时会用到。版本方面OpenCV建议4.x以上NumPy建议1.20以上。这些库的API相对稳定不同版本之间差异不大不用太纠结具体版本号。有一点需要提醒OpenCV读进来的图像是BGR通道顺序而大多数其他库和显示工具用的是RGB。在调试阶段如果发现颜色不对先检查通道顺序。这个坑我踩过不止一次尤其是把OpenCV和PIL混用的时候。3.2 人脸区域的检测与对齐光照图要和人脸对齐前提是知道人脸在哪里。这一步用OpenCV自带的人脸检测器就够了不需要上深度学习模型。import cv2 import numpy as np def detect_face_region(image_path): img cv2.imread(image_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) faces cascade.detectMultiScale(gray, 1.1, 5, minSize(80, 80)) if len(faces) 0: return img, None # 取最大的人脸区域 face max(faces, keylambda f: f[2] * f[3]) return img, face这段代码返回原图和最大人脸区域的边界框。为什么要取最大的人脸因为一张图里可能有多张脸但光照模拟通常针对主体人脸小脸区域的光照变化对整体影响不大而且容易引入噪声。拿到人脸框之后可以进一步做人脸对齐。对齐的目的是消除姿态和尺度差异让光照图的叠加更可控。对齐的方法有很多简单一点的做法是根据两眼位置做仿射变换复杂一点的做法是用关键点检测加相似变换。如果项目对姿态一致性要求不高可以跳过对齐直接在原图上操作。提示Haar级联检测器对侧脸和遮挡的鲁棒性一般如果数据集里侧脸较多建议换成基于深度学习的人脸检测器比如OpenCV的DNN模块加载Caffe模型或者用MediaPipe。检测精度上去了后续光照图的对齐质量才有保障。3.3 光照图生成的核心算法这是整篇文章最核心的部分。光照图的生成质量直接决定了最终数据的可用性。先讲一个基础版本再逐步加复杂度。基础版本径向渐变光照图最简单的光照不均匀就是一侧亮一侧暗。可以用一个线性渐变或者径向渐变来模拟。def generate_radial_illumination(h, w, center, radius, intensity): 生成径向渐变光照图 center: 光源中心 (x, y) radius: 影响半径 intensity: 光照强度系数 yy, xx np.mgrid[0:h, 0:w] dist np.sqrt((xx - center[0])**2 (yy - center[1])**2) # 距离光源越近越亮越远越暗 falloff np.clip(1.0 - dist / radius, 0, 1) # 映射到 [1-intensity, 1] 范围 illum 1.0 - intensity * (1.0 - falloff) return illum.astype(np.float32)这个函数生成一张光照图中心区域接近1保持原亮度边缘区域降到1-intensity。intensity控制光照不均匀的程度0.7表示最暗处只有原亮度的30%。进阶版本多光源叠加真实场景往往有多个光源主光源加环境光。可以生成多个径向光照图然后取最大值或加权平均。def generate_multi_source_illumination(h, w, sources): sources: [(center, radius, intensity), ...] illum np.zeros((h, w), dtypenp.float32) for center, radius, intensity in sources: single generate_radial_illumination(h, w, center, radius, intensity) illum np.maximum(illum, single) return np.clip(illum, 0.2, 1.5)取最大值而不是相加是因为多个光源的照明效果在物理上是叠加的但在归一化之后取最大值更接近哪个光源主导就听谁的这种直觉。相加容易导致过曝。高阶版本加入阴影阴影是光照不均匀里最难模拟的部分。一个实用的做法是用形态学操作生成阴影掩膜。from scipy.ndimage import gaussian_filter def generate_shadow_mask(h, w, num_shadows2, blur_sigma15): 生成随机阴影掩膜 mask np.ones((h, w), dtypenp.float32) for _ in range(num_shadows): # 随机生成阴影形状椭圆或多边形 cx, cy np.random.randint(0, w), np.random.randint(0, h) axes (np.random.randint(w//8, w//3), np.random.randint(h//8, h//3)) angle np.random.randint(0, 180) shadow np.zeros((h, w), dtypenp.float32) cv2.ellipse(shadow, (cx, cy), axes, angle, 0, 360, 1, -1) # 模糊边缘模拟软阴影 shadow gaussian_filter(shadow, sigmablur_sigma) # 阴影区域亮度降低 mask mask * (1 - 0.6 * shadow) return mask这段代码生成若干随机椭圆阴影经过高斯模糊后叠加到光照图上。blur_sigma控制阴影边缘的软硬程度值越大边缘越柔和。把径向光照图和阴影掩膜相乘就得到了综合的光照图。def generate_illumination_map(h, w): # 主光源 main_center (np.random.randint(w//4, 3*w//4), np.random.randint(h//4, 3*h//4)) main_radius np.random.randint(max(h, w)//2, max(h, w)) main_intensity np.random.uniform(0.3, 0.7) illum generate_radial_illumination(h, w, main_center, main_radius, main_intensity) # 阴影 shadow generate_shadow_mask(h, w, num_shadowsnp.random.randint(1, 4)) illum illum * shadow # 轻微模糊让过渡更自然 illum gaussian_filter(illum, sigma3) return np.clip(illum, 0.15, 1.4)3.4 光照图与原图的融合策略有了光照图下一步是把它作用到原图上。最直接的做法是逐像素相乘。def apply_illumination(img, illum): img: BGR图像, uint8 illum: 光照图, float32, 值域约[0.15, 1.4] img_float img.astype(np.float32) / 255.0 illum_3ch np.stack([illum]*3, axis-1) result img_float * illum_3ch result np.clip(result, 0, 1) return (result * 255).astype(np.uint8)这是最基础的乘法融合。但直接相乘有个问题暗部区域相乘后会变得非常暗噪声被放大细节丢失严重。真实相机在暗部会有噪声但不会像乘法那样把噪声线性放大。改进的做法是加入一个环境光基底模拟漫反射。def apply_illumination_with_ambient(img, illum, ambient0.15): img_float img.astype(np.float32) / 255.0 illum_3ch np.stack([illum]*3, axis-1) # 环境光补偿 result img_float * illum_3ch ambient * (1 - illum_3ch) result np.clip(result, 0, 1) return (result * 255).astype(np.uint8)ambient参数控制暗部的最低亮度。设为0.15意味着即使光照系数为0图像也不会全黑而是保留15%的亮度。这更接近真实相机的表现。还有一个细节真实光照变化往往伴随色温变化。暖光偏黄冷光偏蓝。可以在光照图上加一个颜色偏移。def apply_illumination_with_color(img, illum, color_shift(1.0, 1.0, 1.0)): color_shift: BGR三通道的偏移系数 img_float img.astype(np.float32) / 255.0 illum_3ch np.stack([illum]*3, axis-1) color np.array(color_shift, dtypenp.float32).reshape(1, 1, 3) result img_float * illum_3ch * color result np.clip(result, 0, 1) return (result * 255).astype(np.uint8)暖光可以设color_shift(0.9, 0.95, 1.1)冷光设(1.1, 1.0, 0.9)。注意这里是BGR顺序。4. 让生成数据骗过模型的几个关键细节4.1 光照强度的分布要匹配真实场景这是最容易被忽视的一点。很多人做光照增强直接均匀采样强度参数从0.1到0.9随便取。结果生成的数据里极端光照占了很大比例而真实场景中极端光照其实是少数。正确的做法是让强度分布匹配真实场景的统计规律。根据我的经验真实人脸数据集中光照系数最暗处与最亮处的比值大致服从一个偏态分布大部分样本的比值在0.5到0.8之间比值低于0.3的极端样本占比不到10%。所以采样的时候应该用类似Beta分布的采样策略让中等强度的样本占多数极端样本占少数。def sample_intensity(): # Beta分布偏向中等强度 return np.random.beta(2, 5) * 0.7 0.2这个采样出来的值主要集中在0.3到0.6之间偶尔会有接近0.2或0.9的极端值。比均匀采样合理得多。4.2 阴影位置要符合人脸几何随机撒阴影的问题在于阴影可能落在不合理的位置。比如鼻尖上出现一块阴影或者两只眼睛一只在阴影里一只在亮处这些在真实场景中虽然可能发生但概率很低。一个改进思路是用人脸关键点来约束阴影位置。如果能检测到眼睛、鼻子、嘴巴的位置就可以让阴影避开这些关键区域或者让阴影从这些区域的边缘开始。def constrain_shadow_by_landmarks(shadow, landmarks, face_box): 根据关键点调整阴影避免关键区域被完全遮蔽 x, y, w, h face_box # 眼睛区域保护 for (lx, ly) in landmarks: cv2.circle(shadow, (int(lx), int(ly)), int(w*0.08), 0, -1) return shadow这个思路的核心是阴影可以覆盖人脸但不应该把关键信息完全抹掉。真实场景中即使有阴影人眼依然能识别出人脸说明关键信息在阴影下依然部分可见。生成数据也应该保持这个特性。4.3 避免光照图痕迹从乘法到更自然的融合乘法融合的一个副作用是光照图的纹理会被印到图像上。如果光照图有噪声或者不自然的纹理这些纹理就会出现在最终图像里形成一种脏的感觉。解决办法有两个。一是保证光照图本身足够平滑用高斯滤波多过几遍。二是在融合之后做一次轻微的直方图匹配把结果的亮度分布拉回到自然范围。from skimage import exposure def match_histogram(result, reference): 把result的亮度分布匹配到reference result_yuv cv2.cvtColor(result, cv2.COLOR_BGR2YUV) reference_yuv cv2.cvtColor(reference, cv2.COLOR_BGR2YUV) result_yuv[:,:,0] exposure.match_histograms( result_yuv[:,:,0], reference_yuv[:,:,0] ) return cv2.cvtColor(result_yuv, cv2.COLOR_YUV2BGR)直方图匹配会让结果的亮度分布更接近原图避免出现整体过暗或过亮的情况。但要注意匹配得太狠会把光照效果也抹掉所以匹配强度要控制。4.4 批量生成时的参数管理与可复现性做数据增强可复现性非常重要。如果每次生成的参数都是随机的出了问题没法回溯实验也没法对比。建议把每次生成的参数记录下来包括光照中心、半径、强度、阴影数量、阴影位置等。可以用JSON格式保存和生成的图像一一对应。import json import hashlib def generate_with_record(img_path, output_dir): img, face detect_face_region(img_path) h, w img.shape[:2] # 固定随机种子保证可复现 seed int(hashlib.md5(img_path.encode()).hexdigest()[:8], 16) np.random.seed(seed) illum generate_illumination_map(h, w) result apply_illumination_with_ambient(img, illum) # 保存 base os.path.basename(img_path) cv2.imwrite(os.path.join(output_dir, base), result) record { source: img_path, seed: seed, illum_min: float(illum.min()), illum_max: float(illum.max()), illum_mean: float(illum.mean()) } with open(os.path.join(output_dir, base .json), w) as f: json.dump(record, f) return result用图像路径的哈希作为随机种子保证同一张图每次生成的参数一致。这样调试的时候可以稳定复现实验对比也有据可依。5. 生成数据的质量验证与常见翻车场景5.1 怎么判断生成的光照数据是否可用生成完数据不能直接扔进训练得先验证质量。我一般从三个维度检查。维度一视觉合理性。随机抽一批生成结果人眼看是否自然。重点看阴影边缘是否生硬、暗部是否死黑、亮部是否过曝、颜色是否偏得离谱。这一步虽然主观但能快速筛掉明显有问题的样本。维度二光照分布统计。统计所有生成样本的光照系数分布看是否和真实场景的分布接近。如果生成样本的均值、方差、偏度都和真实数据差很远说明参数设置有问题。维度三下游任务验证。最直接的验证方式是把生成数据加入训练集看模型在真实光照测试集上的表现是否提升。如果加入后反而下降说明生成数据的分布和真实数据不匹配或者引入了太多噪声。这三个维度里第三个最有说服力但成本也最高。实际项目中我通常先用前两个维度快速筛选再用第三个维度做最终验证。5.2 翻车场景一阴影太硬模型学到假阴影特征这是最常见的翻车场景。阴影边缘太锐利看起来像贴上去的黑色色块。模型训练时会把这种锐利边缘当成一个特征来学结果在真实场景中遇到软阴影就失效了。解决办法是加大高斯模糊的sigma值。但sigma也不能太大否则阴影会糊成一片失去阴影的形态。我的经验是sigma取图像短边的2%到5%比较合适。比如512x512的图sigma取10到25之间。另外阴影的透明度也要控制。完全不透明的阴影系数为0太极端真实阴影通常是半透明的系数在0.3到0.6之间。5.3 翻车场景二暗部噪声被放大模型学到噪声特征乘法融合会把暗部的噪声线性放大。如果原图本身有噪声暗部区域会变得非常脏。模型训练时可能把这种噪声当成特征影响泛化。解决办法是在融合前先做一次降噪或者在融合后对暗部做针对性的降噪。OpenCV的fastNlMeansDenoisingColored对彩色图像降噪效果不错但速度较慢。如果对速度有要求可以用双边滤波代替。def denoise_dark_regions(img, illum, threshold0.4): 对光照系数低于threshold的区域做降噪 dark_mask (illum threshold).astype(np.uint8) * 255 denoised cv2.fastNlMeansDenoisingColored(img, None, 10, 10, 7, 21) result np.where(dark_mask[..., None] 0, denoised, img) return result这个函数只对暗部区域降噪亮部保持原样避免过度处理导致细节丢失。5.4 翻车场景三光照图和原图尺寸不匹配这个坑很隐蔽。如果原图经过缩放或裁剪光照图没有同步处理叠加的时候就会错位。表现是阴影位置和人脸对不上或者光照图边缘出现明显的接缝。解决办法是在生成光照图之前先确定最终图像的尺寸所有操作都在同一尺寸下进行。如果原图需要缩放先缩放再生成光照图。如果原图需要裁剪先裁剪再生成。def preprocess_image(img, target_size(256, 256)): 统一尺寸保持宽高比短边缩放到target_size中心裁剪 h, w img.shape[:2] scale max(target_size[0]/w, target_size[1]/h) new_w, new_h int(w*scale), int(h*scale) resized cv2.resize(img, (new_w, new_h)) # 中心裁剪 x (new_w - target_size[0]) // 2 y (new_h - target_size[1]) // 2 cropped resized[y:ytarget_size[1], x:xtarget_size[0]] return cropped统一尺寸还有一个好处批量生成时所有光照图的尺寸一致方便做统计分析。5.5 翻车场景四生成数据标签错位如果原图带标注比如关键点坐标、检测框图像经过光照变换后标注坐标必须同步变换。光照变换本身不改变几何位置所以坐标不需要变换。但如果做了缩放或裁剪坐标就必须跟着变。这个坑的典型表现是训练时loss异常高或者模型完全学不动。排查的时候先检查标注和图像是否对齐用可视化工具把标注画到图像上一眼就能看出来。def transform_landmarks(landmarks, scale, crop_offset): 根据缩放和裁剪参数变换关键点坐标 transformed [] for (x, y) in landmarks: new_x x * scale - crop_offset[0] new_y y * scale - crop_offset[1] transformed.append((new_x, new_y)) return transformed6. 进阶思路从手工模拟到数据驱动6.1 用真实光照数据校准模拟参数手工设计的模拟参数无论怎么调都带有主观性。更科学的做法是用真实的光照不均匀数据来校准参数。具体思路是收集一批真实场景下拍摄的人脸图像提取它们的光照分布特征比如亮度直方图、光照梯度方向、阴影边缘的模糊程度等然后用这些特征来指导模拟参数的采样。def extract_illumination_stats(real_images): 从真实图像中提取光照统计特征 stats [] for img in real_images: gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 亮度分布 hist cv2.calcHist([gray], [0], None, [64], [0, 256]).flatten() hist hist / hist.sum() # 光照梯度 gx cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize3) gy cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize3) grad_mag np.sqrt(gx**2 gy**2) stats.append({ hist: hist, grad_mean: float(grad_mag.mean()), grad_std: float(grad_mag.std()) }) return stats拿到真实数据的统计特征后在生成模拟数据时让模拟数据的统计特征尽量接近真实数据。可以用简单的矩匹配也可以用更复杂的对抗训练。6.2 结合三维人脸模型提升阴影真实感如果项目对真实感要求极高可以考虑引入三维人脸模型。基本流程是用三维人脸重建算法如3DMM从单张图像重建人脸几何然后在三维空间里设置光源渲染出带光照的图像。这个方案的优势是阴影会随人脸姿态自然变化不会出现贴图感。缺点是流程复杂重建误差会传播到最终结果而且计算量大。一个折中方案是用三维模型生成一批高质量的光照样本然后用这些样本训练一个轻量级的图像到图像的转换网络再用这个网络来批量生成光照数据。这样既保证了真实感又保证了生成速度。6.3 光照数据增强和光照归一化的配合使用最后提一点光照数据增强和光照归一化不是互斥的而是互补的。光照归一化的作用是消除光照影响让模型关注人脸本身的结构特征。但如果训练数据里光照变化太少归一化算法本身可能过拟合到某种光照模式上遇到没见过的光照就失效。所以正确的做法是用光照增强扩充训练数据的多样性让归一化算法见到足够多的光照变化从而学到更鲁棒的光照不变特征。两者配合效果比单独用任何一个都好。在实际项目中我通常会把光照增强后的数据分成两部分一部分直接用于训练另一部分经过光照归一化后再用于训练。这样模型既能学到对光照鲁棒的特征又能学到光照归一化的能力。7. 一套可直接复用的完整代码框架把前面的内容整合成一个完整的脚本方便直接拿去用。import cv2 import numpy as np import os import json import hashlib from scipy.ndimage import gaussian_filter class IlluminationAugmentor: def __init__(self, target_size(256, 256), ambient0.15): self.target_size target_size self.ambient ambient self.cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) def preprocess(self, img): h, w img.shape[:2] scale max(self.target_size[0]/w, self.target_size[1]/h) new_w, new_h int(w*scale), int(h*scale) resized cv2.resize(img, (new_w, new_h)) x (new_w - self.target_size[0]) // 2 y (new_h - self.target_size[1]) // 2 return resized[y:yself.target_size[1], x:xself.target_size[0]] def detect_face(self, img): gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces self.cascade.detectMultiScale(gray, 1.1, 5, minSize(60, 60)) if len(faces) 0: return None return max(faces, keylambda f: f[2]*f[3]) def generate_illumination(self, h, w, face_box): # 主光源位置偏向人脸一侧 if face_box is not None: fx, fy, fw, fh face_box cx fx np.random.uniform(0.2, 0.8) * fw cy fy np.random.uniform(0.2, 0.8) * fh else: cx np.random.uniform(0.2, 0.8) * w cy np.random.uniform(0.2, 0.8) * h radius np.random.uniform(0.6, 1.2) * max(h, w) intensity np.random.beta(2, 5) * 0.7 0.2 yy, xx np.mgrid[0:h, 0:w] dist np.sqrt((xx-cx)**2 (yy-cy)**2) falloff np.clip(1.0 - dist/radius, 0, 1) illum 1.0 - intensity * (1.0 - falloff) # 阴影 num_shadows np.random.randint(0, 3) for _ in range(num_shadows): sx np.random.randint(0, w) sy np.random.randint(0, h) axes (np.random.randint(w//8, w//3), np.random.randint(h//8, h//3)) angle np.random.randint(0, 180) shadow np.zeros((h, w), dtypenp.float32) cv2.ellipse(shadow, (sx, sy), axes, angle, 0, 360, 1, -1) shadow gaussian_filter(shadow, sigmamax(h, w)*0.03) illum illum * (1 - 0.5 * shadow) illum gaussian_filter(illum, sigmamax(h, w)*0.01) return np.clip(illum, 0.15, 1.4) def apply(self, img, illum): img_float img.astype(np.float32) / 255.0 illum_3ch np.stack([illum]*3, axis-1) result img_float * illum_3ch self.ambient * (1 - illum_3ch) result np.clip(result, 0, 1) return (result * 255).astype(np.uint8) def augment(self, img_path, output_dir): img cv2.imread(img_path) if img is None: return None img self.preprocess(img) face_box self.detect_face(img) h, w img.shape[:2] seed int(hashlib.md5(img_path.encode()).hexdigest()[:8], 16) np.random.seed(seed) illum self.generate_illumination(h, w, face_box) result self.apply(img, illum) base os.path.basename(img_path) cv2.imwrite(os.path.join(output_dir, base), result) record { source: img_path, seed: seed, illum_min: float(illum.min()), illum_max: float(illum.max()), illum_mean: float(illum.mean()), has_face: face_box is not None } with open(os.path.join(output_dir, base .json), w) as f: json.dump(record, f) return result # 使用示例 if __name__ __main__: augmentor IlluminationAugmentor(target_size(256, 256)) input_dir input_faces output_dir output_faces os.makedirs(output_dir, exist_okTrue) for fname in os.listdir(input_dir): if fname.lower().endswith((.jpg, .png, .jpeg)): augmentor.augment(os.path.join(input_dir, fname), output_dir)这个框架可以直接跑参数可以根据具体项目调整。target_size控制输出尺寸ambient控制暗部最低亮度光照强度和阴影数量都在generate_illumination里控制。实际用的时候建议先生成一小批比如50张肉眼检查质量确认没问题再批量生成。批量生成时注意监控生成速度如果太慢可以适当降低高斯滤波的sigma值或者减少阴影数量。最后分享一个我在实际项目中总结的小技巧生成数据的时候不要只生成一份而是对同一张原图生成多个不同光照强度的版本比如3到5个这样能最大化利用每张原图的信息同时保证光照分布的多样性。但要注意控制每个原图的生成数量避免某些原图的样本过多导致数据不平衡。