diff --git a/README.md b/README.md index 94d0fca..abb74a8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ # Labeler for video labeling + +## Key Features +- **Frame-by-frame video labeling** +- **Keypoint annotation** +- **Displacement sway boundary labeling** + ## Web Version https://mobility-scooter-project.github.io/labeler/ - setup by `npm install` in `./web` folder diff --git a/docs/images/exportSwayData.png b/docs/images/exportSwayData.png new file mode 100644 index 0000000..ef3e3df Binary files /dev/null and b/docs/images/exportSwayData.png differ diff --git a/docs/images/exportSwayDataError.png b/docs/images/exportSwayDataError.png new file mode 100644 index 0000000..9d31342 Binary files /dev/null and b/docs/images/exportSwayDataError.png differ diff --git a/docs/images/sway-boundaries.png b/docs/images/sway-boundaries.png new file mode 100644 index 0000000..d415985 Binary files /dev/null and b/docs/images/sway-boundaries.png differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bf3ed9d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "Pro", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/web/README.md b/web/README.md index f287983..5136e46 100644 --- a/web/README.md +++ b/web/README.md @@ -68,7 +68,9 @@ After labeling all desired frames of the video, you can hit button "Save labels" The CSV file would look like this: -![](../docs/images/keypointCSV.png) +![](../docs/images/keyointCSV.png) + + ##### *Explaination* @@ -98,4 +100,90 @@ Digit of (xn,yn) where n follows the following notation: | 11 | left hip | | 12 | right hip | +### Step 4: Sway Boundary Labeling +![](../docs/images/sway-boundaries.png) + +#### Mark sway boundaries + +1. ##### Set Time Boundaries + - Click Set Start Time at the beginning of the sway motion + - Click Set End Time at the completion of marking all 6 sway positions + +2. ##### Mark Sway Positions + Place all 6 required points: + - **Left Sway** + - Sternum (upper torso) + - Umbilicus (lower torso) + - **Right Sway**: + - Sternum (upper torso) + - Umbilicus (lower torso) + - **Neutral Position**: + - Sternum (center) + - Umbilicus (center) + +3. ##### Save Boundary Set + Click Save Current Sway Boundary to store the data + +4. ##### Export Data + - Click the dropdown Export Annotation Data + - Select Export Sway Boundaries Data + +### Visual Guide +![Sway Boundary Marking Demo](docs/videos/sway-boundary.gif) + +### Tips +- Sway points turn green when successfully marked +- Use the eraser tool to correct misplaced sway points +- You must mark all 6 points and end time before saving +- Multiple boundary sets can be saved per video +![](../docs/videos/sway-boundary.gif) + +#### Sway Boundary Features: +✔ **Visual Feedback System** +- Real-time display of marked points with coordinate labels +- Color-coded indicators (Left: Blue, Right: Green, Neutral: Red) +- Completion checkmarks for finished points + +✏ **Smart Editing with Eraser Tool** +- Precision eraser for individual points +- Bulk delete for overlapping markers + + +✅ **Validation System** +- Enforces complete boundary sets: + - 2 Left sway positions (sternum/umbilicus) + - 2 Right sway positions (sternum/umbilicus) + - 2 Neutral positions (sternum/umbilicus) +- Requires valid time boundaries + +#### Export sway boundaries +Click "Export Annotation Data" to save sway boundaries data to a CSV file with: +- Start/End timestamps +- All 6 point boundary with x and y coordinates + +![](../docs/images/exportSwayData.png) + + +## Data Export Formats + +### Sway Points CSV Format: + +| Start_Time | End_Time | Left_Sternum_X | Left_Sternum_Y | Left_Umbilicus_X | Left_Umbilicus_Y | Right_Sternum_X | Right_Sternum_Y | Right_Umbilicus_X | Right_Umbilicus_Y | Neutral_Sternum_X | Neutral_Sternum_Y | Neutral_Umbilicus_X | Neutral_Umbilicus_Y | +|-----------|---------|----------------|----------------|------------------|------------------|-----------------|-----------------|-------------------|-------------------|-------------------|-------------------|---------------------|---------------------| +| 655 | 1276 | 454.5 | 197 | 461.5 | 381 | 403.5 | 210 | 471.5 | 391 | 437.5 | 218 | 465.5 | 390 | +| 2979 | 4586 | 432.5 | 194 | 427.5 | 392 | 407.5 | 197 | 467.5 | 400 | 464.5 | 209 | 476.5 | 409 | + + +**Coordinate System:** Origin (0,0) at top-left corner + + +## Troubleshooting +![](../docs/images/exportSwayDataError.png) +- If you see the above error message "Export failed: Please save boundaries before exporting", this means: + - You haven't saved any sway boundaries yet + - First complete and save at least one sway boundary before exporting +- Before saving each sway boundary, ensure: + - Both start and end times are set + - All 6 required points are marked (Left, Right, and Neutral sway positions for both sternum and umbilicus) + - The green checkmarks appear on all point buttons diff --git a/web/src/App.js b/web/src/App.js index 17accb0..16754b8 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -8,6 +8,7 @@ import Selection from "./components/Selection"; import { VERSION } from "./version"; import KeypointList from "./components/KeypointLabel"; import { keypointsIndex } from "./utils/constant"; +import { SwayPointLabel } from "./components/SwayPointLabel/SwayPointLabel"; const colorLength = 400; @@ -143,6 +144,17 @@ function App() { setMarkedKeypoints([]); setErrorChooseKeypoint(false); setIsRemoveKeypoint(false); + + //sway + setSwayPointData([]); + setSwayPoints([]); + setSelectedSwayPoint(null); + setMarkedSwayPoints([]); + setIsRemoveSwayPoint(false); + setTimeButtonsClicked({ + start: false, + end: false + }); }; const updateLabels = (start, end, label) => { @@ -365,6 +377,131 @@ function App() { setMarkedKeypoints(newMarkedKeypoints); }; + // Sway point handling + const [swayPoints, setSwayPoints] = useState([]); + const [selectedSwayPoint, setSelectedSwayPoint] = useState(); + const [markedSwayPoints, setMarkedSwayPoints] = useState([]); + const [isRemoveSwayPoint, setIsRemoveSwayPoint] = useState(false); + const [swayPointData, setSwayPointData] = useState([]); + const [startTime, setStartTime] = useState([]); + const [endTime, setEndTime] = useState([]); + const [timeButtonsClicked, setTimeButtonsClicked] = useState({ + start: false, + end: false + }); + + // TODO: NEED TO IMPLEMENT + const handleCompleteMarkSwayPoint = () => { + + if (swayPoints.length !== 6) { + setMessage("Please mark all 6 sway points before saving"); + return; + } + + // Sort points by label + const sortedPoints = [...swayPoints].sort((a, b) => a.label - b.label); + + // Store the data + setSwayPointData([ + ...swayPointData, + { + startTime: startTime, + endTime: endTime, + points: sortedPoints + } + ]); + + setSwayPoints([]); + setSelectedSwayPoint(undefined); + setMarkedSwayPoints([]); + setTimeButtonsClicked({ + start: false, + end: false + }); + console.log("The current sway boundary has been saved!!!!"); + }; + + const handleSaveSwayBoundaries = () => { + if (!swayPointData || swayPointData.length === 0 || !swayPointData[0]?.points) { + setMessage("Export failed: Please save boundaries before exporting"); + + // Clear message after 3 seconds + setTimeout(() => { + setMessage(""); + }, 3000); + return; + } + + // Create CSV header + const csvHeader = [ + "Start_Time", + "End_Time", + "Left_Sternum_X", + "Left_Sternum_Y", + "Left_Umbilicus_X", + "Left_Umbilicus_Y", + "Right_Sternum_X", + "Right_Sternum_Y", + "Right_Umbilicus_X", + "Right_Umbilicus_Y", + "Neutral_Sternum_X", + "Neutral_Sternum_Y", + "Neutral_Umbilicus_X", + "Neutral_Umbilicus_Y" + ]; + + + const dataRows = swayPointData.map((boundary) => { + // Sort points by label to ensure consistent order (13-16) + const sortedPoints = boundary.points.sort((a, b) => a.label - b.label); + + // Extract coordinates in order: left sternum, right sternum, left umbilicus, right umbilicus + const coordinates = sortedPoints.flatMap(point => [point.x, point.y]); + + return [ + boundary.startTime, + boundary.endTime, + ...coordinates + ]; + }); + + // Download CSV with header and all data rows + downloadCSV([csvHeader, ...dataRows], `${video}-sway-boundaries.csv`); + }; + + const handleSetStart = () => { + setStartTime(getIndex(time.current, fpsRef.current)); + setTimeButtonsClicked(prev => ({...prev, start: true})); + }; + + const handleSetEnd = () => { + setEndTime(getIndex(time.current, fpsRef.current)); + setTimeButtonsClicked(prev => ({...prev, end: true})); + }; + + console.log("start time: "); + console.log(startTime); + + const handleMarkedSwayPoint = (key) => { + setSelectedSwayPoint(undefined) + setMarkedSwayPoints([...markedSwayPoints, key]); + }; + + const handleRemoveSwayPoint = (key) => { + if (Array.isArray(key)) { // handle multiple overlaping sway points being removed + setMarkedSwayPoints(prev => prev.filter(k => !key.includes(k))); + } + // If single sway point removed + else { + setMarkedSwayPoints(prev => prev.filter(k => k !== key)); + } + setTimeButtonsClicked(prev => ({...prev, end: false})); + }; + + console.log(swayPointData); + console.log("..."); + console.log(swayPoints); + return (
@@ -456,10 +593,18 @@ function App() { onErrorMarkedKeypoint={() => setErrorChooseKeypoint(true)} isRemoveKeypoint={isRemoveKeypoint} onRemoveKeypoint={handleRemoveKeypoint} + + //Swaypoint handler + swayPoints={swayPoints} + setSwayPoints={setSwayPoints} + selectedSwayPoint={selectedSwayPoint} + onMarkSwayPoint={handleMarkedSwayPoint} + isRemoveSwayPoint={isRemoveSwayPoint} + onRemoveSwayPoint={handleRemoveSwayPoint} />

{video}

{source !== "" && ( - <> - +
+ - - - )} + +
+
+ )}
@@ -516,9 +678,36 @@ function App() {
Rotation is under construction...
Don't refresh midway, no cache yet
+ + {/* Sway Boundaries Labeling */} +
+

Sway Boundaries

+
+

+ Tooltip: Click + to begin. All 6 points and timing must be set before saving +

+
+ { + setSelectedSwayPoint(k); + }} + selected={selectedSwayPoint} + marked={markedSwayPoints} + onComplete={handleCompleteMarkSwayPoint} + isRemoveSwayPoint={isRemoveSwayPoint} + setIsRemoveSwayPoint={setIsRemoveSwayPoint} + onSetStart={handleSetStart} + onSetEnd={handleSetEnd} + timeButtonsClicked={timeButtonsClicked} + /> +
); } export default App; + diff --git a/web/src/assets/tick-blue-icon.png b/web/src/assets/tick-blue-icon.png new file mode 100644 index 0000000..652a8a4 Binary files /dev/null and b/web/src/assets/tick-blue-icon.png differ diff --git a/web/src/components/Canvas/Canvas.jsx b/web/src/components/Canvas/Canvas.jsx index e2560f4..5c9cf87 100644 --- a/web/src/components/Canvas/Canvas.jsx +++ b/web/src/components/Canvas/Canvas.jsx @@ -15,10 +15,14 @@ import { keypointsText, labelKeypointWidth, skeletonPair, + swayPointsText, + swaySkeletonPair, + colorSwayPointByIndexInSkeletonPair, } from "../../utils/constant"; const CIRCLE_RAD = 4; const ERROR_TEXT_THRESHOLD = 5; + export const Canvas = ({ points, setPoints, @@ -27,6 +31,14 @@ export const Canvas = ({ onMarkKeypoint, onRemoveKeypoint, onErrorMarkedKeypoint, + + // sway + swayPoints, + setSwayPoints, + currentSwayLabel, + onMarkSwayPoint, + isRemoveSwayPoint = false, + onRemoveSwayPoint, }) => { const isInsideCircle = ( circle_x, @@ -42,8 +54,11 @@ export const Canvas = ({ return true; else return false; }; + const handleMouseDown = (event) => { const { x, y } = event.target?.getStage()?.getPointerPosition(); + + // Handle keypoints if (isRemove) { let deletedPoint = null; const newPoints = points.filter((point) => { @@ -71,9 +86,43 @@ export const Canvas = ({ onErrorMarkedKeypoint(); } } + + // Handle sway points + if (isRemoveSwayPoint) { + const deletedPoints = []; //handle multiple close sway points being deleted + const newPoints = swayPoints.filter((point) => { + const { x: x_circle, y: y_circle, label } = point; + if (isInsideCircle(x_circle, y_circle, x, y)) { + deletedPoints.push(label); + return false; + } + return true; + }); + + if (deletedPoints.length > 0) { + // Remove all corresponding labels from markedSwayPoints + deletedPoints.forEach((label) => { + onRemoveSwayPoint(label); + }); + setSwayPoints(newPoints); + } + } else { + if (currentSwayLabel != null) { + if (event.evt.button !== 2 && event.target.getStage()) { + setSwayPoints((prevArray) => [ + ...prevArray, + { x: x, y: y, label: currentSwayLabel }, + ]); + onMarkSwayPoint(currentSwayLabel); + } + } + } }; + const renderSkeleton = () => { const lineArray = []; + + // Render keypoint skeleton skeletonPair.forEach((pair, index) => { const line = points.filter( (point) => point.label === pair[0] || point.label === pair[1] @@ -85,23 +134,50 @@ export const Canvas = ({ }); } }); - return lineArray.map((linePoints) => ( + + // Render sway skeleton + swaySkeletonPair.forEach((pair, index) => { + const line = swayPoints.filter( + (point) => point.label === pair[0] || point.label === pair[1] + ); + if (line.length === 2) { + lineArray.push({ + points: [line[0].x, line[0].y, line[1].x, line[1].y], + color: colorSwayPointByIndexInSkeletonPair(index), + dash: [5, 5], + }); + } + }); + + return lineArray.map((linePoints, i) => ( )); }; + const getLabelText = (label) => { + return keypointsText[label] || swayPointsText[label] || "Unknown"; + }; + + const getLabelColor = (label) => { + return swayPointsText[label] ? "green" : "red"; // green for sway points, red for keypoints + }; + return ( {renderSkeleton()} - {points.map((e) => ( - <> + + {/* Render keypoints */} + {points.map((e, i) => ( + + + ))} + + {/* Render sway points */} + {swayPoints.map((e, i) => ( + + + + - + ))} ); -}; +}; \ No newline at end of file diff --git a/web/src/components/KeypointLabel/KeypointLabel.module.css b/web/src/components/KeypointLabel/KeypointLabel.module.css index bc0113e..b369cdb 100644 --- a/web/src/components/KeypointLabel/KeypointLabel.module.css +++ b/web/src/components/KeypointLabel/KeypointLabel.module.css @@ -15,6 +15,7 @@ justify-content: space-between; height: 30px; } + .keypointButton, .completeButton { border-radius: 5px; padding: 0; diff --git a/web/src/components/SwayPointLabel/SwayPointLabel.jsx b/web/src/components/SwayPointLabel/SwayPointLabel.jsx new file mode 100644 index 0000000..5088f40 --- /dev/null +++ b/web/src/components/SwayPointLabel/SwayPointLabel.jsx @@ -0,0 +1,127 @@ +import styles from "./SwayPointLabel.module.css"; +import { swayPointsText, swayPointsIndex } from "../../utils/constant"; +import clsx from "clsx"; +import tickGreenIcon from "../../assets/tick-blue-icon.png"; +import eraserIcon from "../../assets/cards_school_eraser.svg"; + +export function SwayPointLabel({ + marked, + selected, + swayPoints = swayPointsIndex, + onSwayPoint, + onComplete, + isRemoveSwayPoint, + setIsRemoveSwayPoint, + onSetStart, + onSetEnd, + timeButtonsClicked, +}){ + // Disable save current sway boundary button + // if all points are not marked OR end time isn't set + // const isSaveDisabled = marked.length < 4 || !timeButtonsClicked.end; + const isSaveDisabled = marked.length < 6 || !timeButtonsClicked.end; + + return ( +
+ {/* Set time Buttons */} +
+
+ + {timeButtonsClicked.start && ( + green-tick + )} +
+
+ + {timeButtonsClicked.end && ( + green-tick + )} +
+
+ {/* Set left and right boundary buttons */} + {swayPoints.map((value, index) => { + const isMarked = marked.includes(value); + return ( +
+ + {isMarked && ( + green-tick + )} +
+ ); + })} + {/* Set save current sway boundary button */} +
+ + +
+
+ ); +} + diff --git a/web/src/components/SwayPointLabel/SwayPointLabel.module.css b/web/src/components/SwayPointLabel/SwayPointLabel.module.css new file mode 100644 index 0000000..49f9a58 --- /dev/null +++ b/web/src/components/SwayPointLabel/SwayPointLabel.module.css @@ -0,0 +1,88 @@ +.swayPointContainer { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + padding-left: 20px; + padding-right: 10px; +} + + +.timeButtonsColumn { + display: flex; + flex-direction: column; + width: 100%; +} + + +.swayPointOption { + margin-top: 10px; + display: flex; + width: 100%; + flex-direction: row; + align-items: center; + justify-content: space-between; + height: 30px; +} + +.swayPointButton, .timeButton, .completeButton { + border-radius: 5px; + padding: 0; + width: 155px; + font-size: 14px; + border: none; + height: inherit; + text-align: center; +} + +.timeButton{ + background-color: #D3FDCC; +} + +.completeButton { + padding: 8px; + width: 80%; + background-color: #6497dd; +} + +.swayPointButton:hover, .timeButton:hover, .completeButton:hover{ + filter: brightness(80%); + cursor: pointer; +} + +.greenTickIcon { + width: 10px; + height: 10px; +} + + +.selected { + border: 4px solid #797eb7; +} + +.swayPointController { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + margin-top: 10px; +} + +.eraserIcon { + width: 20px; + height: 20px; +} + +.eraserButton { + padding: 3px; + background-color: transparent; + border-radius: 5px; + border: none; + cursor: pointer; + border: 2px solid transparent; +} + +.isEraserActive { + border: 2px solid grey; + background-color: #D3FDCC; +} \ No newline at end of file diff --git a/web/src/components/SwayPointLabel/index.js b/web/src/components/SwayPointLabel/index.js new file mode 100644 index 0000000..dafff95 --- /dev/null +++ b/web/src/components/SwayPointLabel/index.js @@ -0,0 +1,2 @@ +import { SwayPointLabel } from "./SwayPointLabel"; +export default SwayPointLabel; diff --git a/web/src/components/VideoController/VideoController.jsx b/web/src/components/VideoController/VideoController.jsx index 08472c0..1edd7b2 100644 --- a/web/src/components/VideoController/VideoController.jsx +++ b/web/src/components/VideoController/VideoController.jsx @@ -26,6 +26,14 @@ export function VideoController({ onErrorMarkedKeypoint, isRemoveKeypoint, onRemoveKeypoint, + + // Sway point + swayPoints, + setSwayPoints, + selectedSwayPoint, + onMarkSwayPoint, + isRemoveSwayPoint, + onRemoveSwayPoint, }) { const [volume, setVolume] = useState(0.7); const [timeStart] = useState(0); @@ -93,6 +101,13 @@ export function VideoController({ onErrorMarkedKeypoint={onErrorMarkedKeypoint} isRemove={isRemoveKeypoint} onRemoveKeypoint={onRemoveKeypoint} + //sway + swayPoints={swayPoints} + setSwayPoints={setSwayPoints} + currentSwayLabel={selectedSwayPoint} + onMarkSwayPoint={onMarkSwayPoint} + isRemoveSwayPoint={isRemoveSwayPoint} + onRemoveSwayPoint={onRemoveSwayPoint} /> )} @@ -101,3 +116,4 @@ export function VideoController({ ) ); } + diff --git a/web/src/index.css b/web/src/index.css index 27e3be5..d40f4c7 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -22,7 +22,7 @@ code { } h3 { - margin: 5px !important; + margin: 5px; } .inactive { @@ -147,7 +147,8 @@ h3 { } .right-container h3 { - margin: 10px; + /* margin: 10px; */ + margin: 10px 8px 5px 10px; } .file-upload-button { @@ -337,6 +338,7 @@ select:hover { padding: 10px; margin: 10px; } + .selections-container h3 { margin: 0; } @@ -346,7 +348,7 @@ select:hover { flex-direction: row; justify-content: space-evenly; align-items: center; - margin-top: 10px; + margin-top: 7px; } .selection-item div { @@ -363,4 +365,103 @@ select:hover { color: red; padding: 0 10px; font-size: 12px; +} + +.sway-hint { + /* padding: 0px 8px; + user-select: none; + font-size: 13px; */ + margin-top: 2px; + padding: 0 8px; + font-size: 13px; +} + +.sway-hint p { + margin: 0; +} + +.demo-btn { + background-color: #D3FDCC; + border: 1px solid #ced4da; + padding: 2px 2px; + margin: 5px; + border-radius: 4px; + font-size: 13px; +} + +/* Dropdown Styles to Match Existing Design */ +.dropdown-container { + position: relative; + display: inline-block; + margin: 20px; /* Match your button margin */ +} + +.dropdown-toggle { + all: unset; /* Reset button styles */ + background-color: #4c7faf; + color: white; + padding: 8px 20px; + text-align: center; + font-size: 16px; + cursor: pointer; + border-radius: 5px; + user-select: none; + display: flex; + align-items: center; + justify-content: space-between; + min-width: 210px; +} + +.dropdown-toggle:hover { + background-color: #264871; /* Match your button hover */ +} + +.dropdown-toggle::after { + content: "▼"; + margin-left: 8px; + font-size: 12px; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + width: 100%; + background-color: #aabdd4; /* Match mid-container bg */ + border-radius: 5px; + box-shadow: 0 2px 10px rgba(0,0,0,0.2); + z-index: 100; + overflow: hidden; + display: none; +} + +.dropdown-container:hover .dropdown-menu { + display: block; +} + +.dropdown-item { + all: unset; + display: block; + width: 100%; + padding: 8px 20px; + text-align: left; + background-color: transparent; + color: white; + font-size: 16px; + cursor: pointer; + transition: background-color 0.15s ease-in-out; +} + +.dropdown-item:hover { + background-color: #4c7faf; /* Match your button color */ +} + +/* Active state for dropdown */ +.dropdown-container.active .dropdown-toggle::after { + content: "▲"; +} + +/* For the state-managed version */ +.dropdown-container.active .dropdown-menu { + display: block; } \ No newline at end of file diff --git a/web/src/utils/constant.js b/web/src/utils/constant.js index 3615b43..34c3137 100644 --- a/web/src/utils/constant.js +++ b/web/src/utils/constant.js @@ -1,5 +1,6 @@ export const keypointsIndex = [0, 5, 6, 7, 8, 9, 10, 11, 12]; + export const labelKeypointWidth = (text) => { const len = text.length; if (len < 5) { @@ -12,10 +13,13 @@ export const labelKeypointWidth = (text) => { return 60; } else if (len < 14) { return 65; - } else { + } else if (len < 16){ return 75; + } else{ + return 115; } }; + export const skeletonPair = [ [0, 5], [0, 6], @@ -45,6 +49,7 @@ export const keypointsText = { 12: "right hip", }; + export const colorKeypointByIndexInSkeletonPair = (indexInSkeletonPair) => { switch (indexInSkeletonPair) { case 0: @@ -65,18 +70,30 @@ export const colorKeypointByIndexInSkeletonPair = (indexInSkeletonPair) => { } }; -// export const keypointsText = { -// nose: 0, -// "left eye": 1, -// "right eye": 2, -// "left ear": 3, -// "right ear": 4, -// "left shoulder": 5, -// "right shoulder": 6, -// "left elbow": 7, -// "right elbow": 8, -// "left wrist": 9, -// "right wrist": 10, -// "left hip": 11, -// "right hip": 12, -// }; +// ====================== SWAY POINTS ====================== // +// left sternum, left umbilicus, right sternum, right umbilicus +export const swayPointsIndex = [13, 14, 15, 16, 17, 18]; +export const swayPointsText = { + 13: "Left Sway - Sternum", + 14: "Left Sway - Umbilicus", + 15: "Right Sway - Sternum", + 16: "Right Sway - Umbilicus", + 17: "Neutral - Sternum", + 18: "Neutral - Umbilicus", +}; + +export const swaySkeletonPair = [ + [13, 14], // left sternum → left umbilicus + [15, 16], // right sternum → right umbilicus + [17, 18], // natural sternum → natural umbilicus +]; + +// maps colors to sway skeleton pairs +export const colorSwayPointByIndexInSkeletonPair = (pairIndex) => { + if(pairIndex === 0){ + return "blue"; + } else if (pairIndex === 1){ + return "green"; + } + return "red"; +};