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:
-
+
+
+
##### *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
+
+
+#### 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
+
+
+### 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
+
+
+#### 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
+
+
+
+
+## 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
+
+- 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
+
+ Set Start Time
+ 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 */}
+
+
+
+ Set Start Time
+
+ {timeButtonsClicked.start && (
+

+ )}
+
+
+
+ Set End Time
+
+ {timeButtonsClicked.end && (
+

+ )}
+
+
+ {/* Set left and right boundary buttons */}
+ {swayPoints.map((value, index) => {
+ const isMarked = marked.includes(value);
+ return (
+
+
{
+ onSwayPoint(value);
+ setIsRemoveSwayPoint(false);
+ }}
+ disabled={isMarked || !timeButtonsClicked.start}
+ >
+ {swayPointsText[value]}
+
+ {isMarked && (
+

+ )}
+
+ );
+ })}
+ {/* Set save current sway boundary button */}
+
+
+ Save Current Sway Boundary
+
+
setIsRemoveSwayPoint(!isRemoveSwayPoint)}
+ >
+
+
+
+
+ );
+}
+
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";
+};