feat(maskSplit): computeSplitRegions TDD (v0-plan Task 6)
src/features/maskSplit/splitMask.ts: - Region / SplitRegions / ImageSize 타입 - 상의(어깨-20 ~ 골반) / 하의(골반 ~ 발목+20) / 전신(어깨-80 ~ 발목+20) - x 범위: keypoint bbox ±20px - requireJoint helper: validatePose 통과 후 호출 invariant — 누락 시 explicit throw src/features/maskSplit/__tests__/splitMask.test.ts: - 4 케이스 TDD: top / bottom / full y 좌표 + 공통 x 범위 검증: - npx jest src/features/maskSplit: 4 passed - npx tsc --noEmit: 무에러 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
41
src/features/maskSplit/__tests__/splitMask.test.ts
Normal file
41
src/features/maskSplit/__tests__/splitMask.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { computeSplitRegions } from '../splitMask';
|
||||
import type { PoseResult } from '../../pose';
|
||||
|
||||
describe('computeSplitRegions', () => {
|
||||
const pose: PoseResult = {
|
||||
left_shoulder_joint: { x: 100, y: 200, confidence: 0.9 },
|
||||
right_shoulder_joint: { x: 300, y: 200, confidence: 0.9 },
|
||||
left_hip_joint: { x: 120, y: 400, confidence: 0.85 },
|
||||
right_hip_joint: { x: 280, y: 400, confidence: 0.85 },
|
||||
left_ankle_joint: { x: 130, y: 700, confidence: 0.7 },
|
||||
right_ankle_joint: { x: 270, y: 700, confidence: 0.7 },
|
||||
};
|
||||
const imageSize = { width: 400, height: 800 };
|
||||
|
||||
it('top region은 어깨-20부터 골반까지(목~허리)', () => {
|
||||
const r = computeSplitRegions(pose, imageSize);
|
||||
expect(r.top.yStart).toBe(180);
|
||||
expect(r.top.yEnd).toBe(400);
|
||||
});
|
||||
|
||||
it('bottom region은 골반부터 발목+20까지(허리~발끝)', () => {
|
||||
const r = computeSplitRegions(pose, imageSize);
|
||||
expect(r.bottom.yStart).toBe(400);
|
||||
expect(r.bottom.yEnd).toBe(720);
|
||||
});
|
||||
|
||||
it('full region은 어깨-80부터 발목+20까지(머리위~발끝)', () => {
|
||||
const r = computeSplitRegions(pose, imageSize);
|
||||
expect(r.full.yStart).toBe(120);
|
||||
expect(r.full.yEnd).toBe(720);
|
||||
});
|
||||
|
||||
it('모든 region은 x 범위가 keypoint bbox ±20에 맞춰진다', () => {
|
||||
const r = computeSplitRegions(pose, imageSize);
|
||||
// keypoint x 최소 = 100 (left_shoulder), 최대 = 300 (right_shoulder)
|
||||
expect(r.top.xStart).toBe(80);
|
||||
expect(r.top.xEnd).toBe(320);
|
||||
expect(r.bottom.xStart).toBe(80);
|
||||
expect(r.full.xEnd).toBe(320);
|
||||
});
|
||||
});
|
||||
54
src/features/maskSplit/splitMask.ts
Normal file
54
src/features/maskSplit/splitMask.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { PoseResult, Joint, Keypoint } from '../pose';
|
||||
|
||||
export interface Region {
|
||||
xStart: number;
|
||||
yStart: number;
|
||||
xEnd: number;
|
||||
yEnd: number;
|
||||
}
|
||||
|
||||
export interface SplitRegions {
|
||||
top: Region;
|
||||
bottom: Region;
|
||||
full: Region;
|
||||
}
|
||||
|
||||
export interface ImageSize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const X_PADDING = 20;
|
||||
const TOP_HEAD_MARGIN = 20;
|
||||
const FULL_HEAD_MARGIN = 80;
|
||||
const FOOT_MARGIN = 20;
|
||||
|
||||
function requireJoint(pose: PoseResult, joint: Joint): Keypoint {
|
||||
const kp = pose[joint];
|
||||
if (!kp) throw new Error(`computeSplitRegions: ${joint} keypoint 누락 — validatePose 통과 후 호출 필요`);
|
||||
return kp;
|
||||
}
|
||||
|
||||
export function computeSplitRegions(
|
||||
pose: PoseResult,
|
||||
_imageSize: ImageSize,
|
||||
): SplitRegions {
|
||||
const ls = requireJoint(pose, 'left_shoulder_joint');
|
||||
const rs = requireJoint(pose, 'right_shoulder_joint');
|
||||
const lh = requireJoint(pose, 'left_hip_joint');
|
||||
const rh = requireJoint(pose, 'right_hip_joint');
|
||||
const la = requireJoint(pose, 'left_ankle_joint');
|
||||
const ra = requireJoint(pose, 'right_ankle_joint');
|
||||
|
||||
const shoulderY = Math.min(ls.y, rs.y);
|
||||
const hipY = Math.max(lh.y, rh.y);
|
||||
const ankleY = Math.max(la.y, ra.y);
|
||||
const xMin = Math.min(ls.x, rs.x, lh.x, rh.x, la.x, ra.x) - X_PADDING;
|
||||
const xMax = Math.max(ls.x, rs.x, lh.x, rh.x, la.x, ra.x) + X_PADDING;
|
||||
|
||||
return {
|
||||
top: { xStart: xMin, yStart: shoulderY - TOP_HEAD_MARGIN, xEnd: xMax, yEnd: hipY },
|
||||
bottom: { xStart: xMin, yStart: hipY, xEnd: xMax, yEnd: ankleY + FOOT_MARGIN },
|
||||
full: { xStart: xMin, yStart: shoulderY - FULL_HEAD_MARGIN, xEnd: xMax, yEnd: ankleY + FOOT_MARGIN },
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user