diff --git a/src/features/maskSplit/.gitkeep b/src/features/maskSplit/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/features/maskSplit/__tests__/splitMask.test.ts b/src/features/maskSplit/__tests__/splitMask.test.ts new file mode 100644 index 0000000..0a53ef9 --- /dev/null +++ b/src/features/maskSplit/__tests__/splitMask.test.ts @@ -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); + }); +}); diff --git a/src/features/maskSplit/splitMask.ts b/src/features/maskSplit/splitMask.ts new file mode 100644 index 0000000..5d4816f --- /dev/null +++ b/src/features/maskSplit/splitMask.ts @@ -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 }, + }; +}