人脸坐标系与左右命名规范
版本: 1.0
创建日期: 2026-05-03
状态: ✅ 强制执行标准
📋 问题背景
在 PicMe 项目中,“左/右”的描述存在歧义,主要体现在:
- 基于图像的左右(Image Space):从观察者视角看屏幕,左侧 = x 坐标小的位置
- 基于人脸的左右(Face Space):从被拍摄者视角,其左手边 = 图像右侧(前置摄像头镜像后)
这种歧义导致: - 代码注释混乱(如 // 右眉,画面左侧) - 文档描述不一致 - 新成员理解困难 - Bug 修复时容易搞反方向
✅ 统一标准
核心原则:允许两种坐标系,但严禁混用
强制要求
⚠️ 从本规范生效起,所有文档和代码必须满足以下要求:
- 明确标注坐标系类型(
[图像坐标系]或[人脸坐标系]) - 同一作用域内禁止混用两种坐标系
- 跨坐标系转换必须有明确的转换函数
// ❌ 错误:混用两种坐标系
fun processFace() {
val imageLeftEye = getImageLeftEye() // [图像坐标系]
val userRightEye = getUserRightEye() // [人脸坐标系]
val distance = calculateDistance(imageLeftEye, userRightEye) // ❌ 混用!
}
// ✅ 正确:统一使用一种坐标系
fun processFace() {
// 方案 A:统一使用图像坐标系(推荐)
val imageLeftEye = getImageLeftEye()
val imageRightEye = getImageRightEye()
val distance = calculateDistance(imageLeftEye, imageRightEye)
// 方案 B:统一使用人脸坐标系(需转换)
val userLeftEye = getUserLeftEye(isFrontCamera)
val userRightEye = getUserRightEye(isFrontCamera)
val distance = calculateDistance(userLeftEye, userRightEye)
}
// ✅ 正确:跨坐标系转换有明确函数
fun convertUserToImageCoordinates(
userLandmarks: List<Point>,
isFrontCamera: Boolean
): List<Point> {
return userLandmarks.map { point ->
if (isFrontCamera) {
Point(1.0f - point.x, point.y) // 前置镜像
} else {
point
}
}
}坐标系定义
坐标系定义
| 术语 | 定义 | 适用场景 | 示例 |
|---|---|---|---|
| 图像坐标系 | 基于图像边界的坐标系统 • 原点:左上角 (0, 0) • x 轴:向右增加 • y 轴:向下增加 |
• GPU Shader 渲染 • OpenGL 纹理映射 • 图像处理算法 • 与显示系统对接 |
imageLeftEyeimageRightEyebrow |
| 人脸坐标系 | 基于被拍摄者身体的坐标系统 • 左/右:以被拍摄者为参照 • 需要区分前后置摄像头 |
• 业务逻辑层 • UI 文案显示 • 用户交互提示 • 与外部 SDK 对接 |
userLeftEyefaceRightCheek |
关键理解: - ✅ 两种坐标系都允许使用 - ⚠️ 但必须在边界处明确转换 - ❌ 严禁在同一作用域内混用
禁止使用的术语
❌ 禁止: - “左眼”、“右眼”(未指明是图像还是人脸视角) - “左侧脸”、“右侧脸” - “左眉毛”、“右眉毛”
✅ 必须使用: - “图像左侧的眼睛”(明确说明是图像坐标系) - “图像右侧的眼睛” - “图像左侧的脸部轮廓”
🎯 具体规则
规则 0: 坐标系选择策略
项目推荐策略:
┌─────────────────────────────────────────┐
│ 坐标系分层架构 │
├─────────────────────────────────────────┤
│ │
│ UI / 业务层 │
│ ├── 可使用 [人脸坐标系] │
│ ├── 便于用户理解 │
│ └── 需处理前后置差异 │
│ │
│ ↓ 转换边界(明确函数) │
│ convertUserToImageCoordinates() │
│ convertImageToUserCoordinates() │
│ │
│ 渲染 / 算法层 │
│ ├── 必须使用 [图像坐标系] │
│ ├── GPU Shader 处理 │
│ ├── OpenGL 纹理映射 │
│ └── 图像处理算法 │
│ │
└─────────────────────────────────────────┘
选择建议:
| 层级 | 推荐坐标系 | 原因 |
|---|---|---|
| UI 展示层 | 人脸坐标系 | 用户更容易理解“左眼”、“右眼” |
| 业务逻辑层 | 图像坐标系(推荐)或人脸坐标系 | 根据团队习惯统一选择 |
| 渲染引擎层 | 必须使用图像坐标系 | GPU Shader、OpenGL 原生支持 |
| 算法处理层 | 必须使用图像坐标系 | OpenCV、MediaPipe 等库的标准 |
重要原则: - ✅ 每个模块内部必须统一使用一种坐标系 - ✅ 模块间通过明确的转换函数交接 - ❌ 禁止在同一个函数/类中混用两种坐标系
规则 1: 关键点命名
标准格式:{坐标系前缀}_{位置}_{部位}
坐标系前缀: - image_ - 图像坐标系(观察者视角,x 向右增加) - user_ 或 face_ - 人脸坐标系(被拍摄者视角)
| 正确命名 | 错误命名 | 说明 |
|---|---|---|
imageLeftEye |
leftEye / userLeftEye |
明确指出是图像左侧 |
imageRightEye |
rightEye |
明确指出是图像右侧 |
userLeftEye |
leftEye |
明确指出是被拍摄者左眼 |
imageLeftEyebrow |
leftEyebrow |
图像左侧的眉毛 |
faceCenter |
- | 脸部中心(无歧义,可不加前缀) |
noseTip |
- | 鼻尖(对称器官,无歧义) |
规则 2: 文档描述
强制格式:[{坐标系}] {位置描述}
❌ 错误(未标注坐标系):
- "左眼外眼角"
- "右眉眉头"
- "下巴左侧"
✅ 正确(明确标注):
- "[图像坐标系] 图像左侧眼睛的外眼角"
- "[图像坐标系] 图像右侧眉毛的眉头"
- "[人脸坐标系] 被拍摄者左脸的下巴轮廓"
- "[图像坐标系] 图像左侧的下巴轮廓(前置时对应被拍摄者右脸)"文档章节开头必须添加坐标系声明:
## 3. 人脸关键点映射
> **坐标系说明**:本节所有坐标均基于**图像坐标系**(观察者视角)。
> - 图像左侧 = x 坐标较小的一侧
> - 图像右侧 = x 坐标较大的一侧
> - 前置摄像头镜像后,图像左侧对应被拍摄者右脸
| 索引 | 部位 | 说明 |
|------|------|------|
| 52-57 | [图像坐标系] 图像右侧眼睛外轮廓 | 后置时对应被拍摄者右眼 |
| 58-63 | [图像坐标系] 图像左侧眼睛外轮廓 | 后置时对应被拍摄者左眼 |规则 3: 代码注释
强制格式:// [{坐标系}] {描述}
// ❌ 错误:未标注坐标系
// 右眼外轮廓(画面左侧)
val rightEyeContour = landmarks.slice(52..57)
// ✅ 正确:明确标注坐标系
// [图像坐标系] 图像右侧的眼睛外轮廓(后置时对应被拍摄者右眼)
val imageRightEyeContour = landmarks.slice(52..57)
// ✅ 正确:语义标注(需说明转换关系)
// [人脸坐标系] 被拍摄者右眼(前置时位于图像右侧,后置时位于图像左侧)
val userRightEye = if (isFrontCamera) {
landmarks[IMAGE_RIGHT_EYE_INDICES] // 前置:图像右侧
} else {
landmarks[IMAGE_LEFT_EYE_INDICES] // 后置:图像左侧
}函数注释必须包含坐标系说明:
/**
* [图像坐标系] 获取图像左侧眼睛的中心点
*
* @return 图像坐标系中的点(原点在左上角,x 向右增加,y 向下增加)
* @see COORDINATE_SYSTEM_STANDARD.md 坐标系规范
*/
fun getImageLeftEyeCenter(): Point {
// ...
}
/**
* [人脸坐标系] 获取被拍摄者左眼的中心点
*
* @param isFrontCamera 是否为前置摄像头(影响坐标映射)
* @return 图像坐标系中的点(已根据前后置自动转换)
* @note 前置时返回图像右侧的点,后置时返回图像左侧的点
*/
fun getUserLeftEyeCenter(isFrontCamera: Boolean): Point {
// ...
}规则 4: 变量命名
Kotlin 命名规范:
// ❌ 错误:未标注坐标系,存在歧义
val leftEye = face.leftEye
val rightEye = face.rightEye
// ✅ 正确:图像坐标系(推荐,项目统一标准)
val imageLeftEye = face.getImageLeftEye() // 图像左侧的眼睛
val imageRightEye = face.getImageRightEye() // 图像右侧的眼睛
// ✅ 可接受:人脸坐标系(需明确标注,谨慎使用)
val userLeftEye = face.getUserLeftEye(isFrontCamera) // 被拍摄者左眼
val userRightEye = face.getUserRightEye(isFrontCamera) // 被拍摄者右眼枚举定义示例:
/**
* 眼睛位置枚举(明确标注坐标系)
*/
enum class EyePosition {
/** [图像坐标系] 图像左侧的眼睛(x 坐标较小) */
IMAGE_LEFT,
/** [图像坐标系] 图像右侧的眼睛(x 坐标较大) */
IMAGE_RIGHT,
/** [人脸坐标系] 被拍摄者左眼(前置时对应 IMAGE_RIGHT) */
USER_LEFT,
/** [人脸坐标系] 被拍摄者右眼(前置时对应 IMAGE_LEFT) */
USER_RIGHT
}规则 5: 禁止混用检查
同一作用域内禁止混用两种坐标系:
// ❌ 错误:混用图像坐标系和人脸坐标系
class FaceProcessor {
fun process() {
val imageLeftEye = landmarks[IMAGE_LEFT_EYE] // 图像坐标系
val userRightEye = getUserRightEye() // 人脸坐标系
// ❌ 错误:两种坐标系混合计算
val center = (imageLeftEye + userRightEye) / 2
}
}
// ✅ 正确:统一使用图像坐标系
class FaceProcessor {
fun process() {
val imageLeftEye = landmarks[IMAGE_LEFT_EYE]
val imageRightEye = landmarks[IMAGE_RIGHT_EYE]
// ✅ 正确:同一种坐标系计算
val center = (imageLeftEye + imageRightEye) / 2
}
}
// ✅ 正确:如需转换,使用明确的转换函数
class FaceProcessor {
fun process() {
// 从人脸坐标系转换为图像坐标系
val userLandmarks = getUserLandmarks()
val imageLandmarks = convertUserToImageCoordinates(
userLandmarks,
isFrontCamera
)
// 统一使用图像坐标系处理
val imageLeftEye = imageLandmarks[IMAGE_LEFT_EYE]
val imageRightEye = imageLandmarks[IMAGE_RIGHT_EYE]
val center = (imageLeftEye + imageRightEye) / 2
}
}Code Review 检查点: - [ ] 同一个函数内是否只使用一种坐标系? - [ ] 如有坐标系转换,是否有明确的转换函数? - [ ] 变量命名是否清晰体现坐标系类型? - [ ] 注释是否说明了坐标系转换的原因?
📊 坐标系对照表
前置摄像头(镜像模式)
┌─────────────────────────────────┐
│ 屏幕显示区域 │
│ │
│ 图像左侧 图像右侧 │
│ (x 小) (x 大) │
│ │
│ 👁️ 👁️ │
│ 被拍摄者右眼 被拍摄者左眼 │
│ (image_left) (image_right)│
│ │
└─────────────────────────────────┘
关键理解:
- 图像左侧 = 观察者看到的左边 = 被拍摄者的右边(镜像后)
- 图像右侧 = 观察者看到的右边 = 被拍摄者的左边(镜像后)
后置摄像头(非镜像模式)
┌─────────────────────────────────┐
│ 屏幕显示区域 │
│ │
│ 图像左侧 图像右侧 │
│ (x 小) (x 大) │
│ │
│ 👁️ 👁️ │
│ 被拍摄者左眼 被拍摄者右眼 │
│ (image_left) (image_right)│
│ │
└─────────────────────────────────┘
关键理解:
- 图像左侧 = 观察者看到的左边 = 被拍摄者的左边(无镜像)
- 图像右侧 = 观察者看到的右边 = 被拍摄者的右边(无镜像)
🔧 迁移指南
Step 1: 识别需要修改的位置
搜索以下模式:
# 搜索模糊的左右描述
grep -r "左眼\|右眼\|左眉\|右眉" docs/ --include="*.md"
grep -r "leftEye\|rightEye" app/src/ --include="*.kt" | grep -v "imageLeft\|imageRight"Step 2: 按优先级修改
优先级 1: 技术文档(最高)
文件列表: - InsightFace 106 映射文档(已移除,2026-05) - docs/03-TECHNICAL-SPECS/BEAUTY_ENGINE_TECH_SPEC.md(含相机预览比例与坐标转换、人脸关键点使用) - .qoder/skills/av-gl-expert/SKILL.md
修改示例:
# 修改前
| 33 | 右眉眉头(画面左侧) | 43 |
| 38 | 左眉眉头(画面右侧) | 101 |
# 修改后
| 33 | 图像右侧眉毛的眉头(被拍摄者右眉) | 43 |
| 38 | 图像左侧眉毛的眉头(被拍摄者左眉) | 101 |优先级 2: 代码注释
文件列表: - beauty-engine/src/main/java/com/picme/beauty/internal/facedetect/adapter/InsightFaceAdapter.kt - beauty-engine/src/main/java/com/picme/beauty/egl/BeautyRenderer.kt - app/src/main/java/com/picme/core/image/ImageProcessor.kt
修改示例:
// 修改前
// 右眼外轮廓:统一 52-57(画面左侧)
val rightEyeContour = unifiedLandmarks.slice(52..57)
// 修改后
// 图像左侧的眼睛外轮廓(对应被拍摄者右眼):统一索引 52-57
val imageLeftEyeContour = unifiedLandmarks.slice(52..57)优先级 3: 变量命名(谨慎执行)
注意:变量命名修改影响范围大,建议分阶段进行:
// Phase 1: 添加别名(向后兼容)
@Deprecated("使用 imageLeftEye 替代", ReplaceWith("imageLeftEye"))
val leftEye = imageLeftEye
// Phase 2: 逐步迁移调用方
// Phase 3: 移除旧命名📝 最佳实践
1. 始终明确坐标系
/**
* 获取图像左侧眼睛的中心点
*
* @return 图像坐标系中的点(原点在左上角,x 向右增加,y 向下增加)
*/
fun getImageLeftEyeCenter(): Point {
// ...
}2. 使用枚举避免歧义
enum class EyePosition {
IMAGE_LEFT, // 图像左侧的眼睛
IMAGE_RIGHT // 图像右侧的眼睛
}
fun getEyeCenter(position: EyePosition): Point {
return when (position) {
EyePosition.IMAGE_LEFT -> landmarks[IMAGE_LEFT_EYE_INDEX]
EyePosition.IMAGE_RIGHT -> landmarks[IMAGE_RIGHT_EYE_INDEX]
}
}3. 在函数名中体现坐标系
// ❌ 不明确
fun getLeftEye(): Point
// ✅ 明确
fun getImageLeftEye(): Point
fun getFaceSpaceLeftEye(): Point // 如果确实需要人脸坐标系4. 文档中添加坐标系图示
## 坐标系说明
本文档中所有坐标均基于**图像坐标系**:(0, 0) ───────────→ x 增加 │ │ 图像左侧 图像右侧 │ (x 小) (x 大) │ ↓ y 增加
🚨 常见陷阱
陷阱 1: 混淆前后置摄像头
// ❌ 错误:假设前置和后置的"左眼"相同
val leftEye = if (isFrontCamera) {
face.leftEye // 前置:这是图像右侧!
} else {
face.leftEye // 后置:这是图像左侧
}
// ✅ 正确:统一使用图像坐标系
val imageLeftEye = face.getImageLeftEye() // 永远是图像左侧陷阱 2: 镜像翻转后忘记更新命名
// ❌ 错误:镜像后仍然叫 leftEye
val mirroredLeftEye = mirror(face.leftEye) // 实际已变成图像右侧
// ✅ 正确:镜像后重新评估位置
val imageLeftEye = if (isMirrored) {
face.originalRightEye // 镜像后,原右眼变成图像左侧
} else {
face.originalLeftEye
}陷阱 3: 文档与代码不一致
# 文档说:
"左眼使用索引 58-63"
# 代码却是:
val imageLeftEye = landmarks[58..63] // 实际是图像左侧
# 问题:文档的"左眼"指什么?解决方案:文档必须与代码使用相同的术语。
✅ 验收检查清单
🚨 强制要求(必须全部满足)
文档审查
代码审查
测试验证
🔍 自动化检测脚本
检测未标注坐标系的注释
#!/bin/bash
# scripts/check-coordinate-annotation.sh
# 检测代码中未标注坐标系的左右描述
echo "🔍 检查未标注坐标系的注释..."
# 搜索常见的模糊描述
FUZZY_PATTERNS=(
"左眼"
"右眼"
"左眉"
"右眉"
"左侧脸"
"右侧脸"
)
for pattern in "${FUZZY_PATTERNS[@]}"; do
echo "\n搜索: $pattern"
grep -rn "$pattern" app/src/ --include="*.kt" | grep -v "\[图像坐标系\]" | grep -v "\[人脸坐标系\]" | grep -v "imageLeft" | grep -v "imageRight" | grep -v "userLeft" | grep -v "userRight"
done
echo "\n✅ 检查完成"检测未标注坐标系的文档
#!/bin/bash
# scripts/check-doc-coordinate-annotation.sh
# 检测文档中未标注坐标系的左右描述
echo "🔍 检查文档中的坐标系标注..."
# 搜索 Markdown 文件中的模糊描述
grep -rn "左眼\|右眼\|左眉\|右眉" docs/ --include="*.md" | \
grep -v "\[图像坐标系\]" | \
grep -v "\[人脸坐标系\]" | \
grep -v "图像左侧" | \
grep -v "图像右侧" | \
grep -v "被拍摄者"
echo "\n✅ 检查完成"Git Pre-commit Hook(可选)
#!/bin/bash
# .git/hooks/pre-commit
# 提交前自动检查坐标系标注
echo "🔍 运行坐标系标注检查..."
# 检查暂存的文件
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E "\.(kt|md)$")
if [ -z "$STAGED_FILES" ]; then
exit 0
fi
ERRORS=0
for file in $STAGED_FILES; do
# 检查 Kotlin 文件
if [[ $file == *.kt ]]; then
FUZZY_COMMENTS=$(grep -n "左眼\|右眼\|左眉\|右眉" "$file" | \
grep -v "\[图像坐标系\]" | \
grep -v "\[人脸坐标系\]" | \
grep -v "imageLeft" | \
grep -v "imageRight")
if [ ! -z "$FUZZY_COMMENTS" ]; then
echo "❌ $file 中存在未标注坐标系的注释:"
echo "$FUZZY_COMMENTS"
ERRORS=$((ERRORS + 1))
fi
fi
# 检查 Markdown 文件
if [[ $file == *.md ]]; then
FUZZY_DOCS=$(grep -n "左眼\|右眼\|左眉\|右眉" "$file" | \
grep -v "\[图像坐标系\]" | \
grep -v "\[人脸坐标系\]" | \
grep -v "图像左侧" | \
grep -v "图像右侧")
if [ ! -z "$FUZZY_DOCS" ]; then
echo "❌ $file 中存在未标注坐标系的描述:"
echo "$FUZZY_DOCS"
ERRORS=$((ERRORS + 1))
fi
fi
done
if [ $ERRORS -gt 0 ]; then
echo "\n❌ 发现 $ERRORS 个文件存在坐标系标注问题"
echo "请参考 docs/07-STANDARDS/COORDINATE_SYSTEM.md 修复"
exit 1
fi
echo "✅ 坐标系标注检查通过"
exit 0📚 参考资源
内部文档
- InsightFace 106 映射文档(已移除,2026-05;坐标系规范不受影响)
- BEAUTY_ENGINE_TECH_SPEC.md - 相机预览比例、坐标转换与人脸关键点使用
外部资源
🔄 维护策略
定期审计
每季度执行一次全文档扫描:
# 查找潜在的歧义描述
find docs/ -name "*.md" -exec grep -l "左眼\|右眼\|左眉\|右眉" {} \;
# 检查代码注释
find app/src/ -name "*.kt" -exec grep -n "//.*左眼\|//.*右眼" {} +新人培训
在新成员 onboarding 文档中加入本规范:
## PicMe 开发规范
### 坐标系与命名
- 阅读 [人脸坐标系与左右命名规范](../02-ARCHITECTURE/ADR/ADR-003-coordinate-system-management.md)
- 理解图像坐标系 vs 人脸坐标系的区别
- 掌握前置/后置摄像头的镜像差异CI/CD 集成(可选)
添加 lint 规则检测模糊命名:
// custom-lint-rules/AmbiguousNamingDetector.kt
class AmbiguousNamingDetector : Detector(), Detector.UastScanner {
override fun getApplicableUastTypes() = listOf<UClass>(
UVariableDeclaration::class.java
)
override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
override fun visitVariable(node: UVariableDeclaration) {
val name = node.name
if (name.matches(Regex("^(left|right)(Eye|Eyebrow|Cheek)$"))) {
context.report(
issue = AMBIGUOUS_NAMING,
location = context.getLocation(node),
message = "变量名 '$name' 有歧义,请使用 'imageLeft' 或 'imageRight' 前缀"
)
}
}
}
}批准人: [RD] 全栈工程师 + [CR] 规范守护者
生效日期: 2026-05-03
下次审查: 2026-08-03