changed config file
This commit is contained in:
parent
32056be80a
commit
4cdc6e0380
68
config.yml
68
config.yml
@ -1,57 +1,15 @@
|
|||||||
measurements:
|
measurements:
|
||||||
- name: arm_length
|
|
||||||
landmarks:
|
|
||||||
- 11
|
|
||||||
- 13
|
|
||||||
- 15
|
|
||||||
|
|
||||||
- name: leg_length
|
|
||||||
landmarks:
|
|
||||||
- 23
|
|
||||||
- 25
|
|
||||||
- 27
|
|
||||||
|
|
||||||
- name: shoulder_length
|
- name: shoulder_length
|
||||||
landmarks:
|
landmarks:
|
||||||
- 11
|
- left_shoulder
|
||||||
- 12
|
- right_shoulder
|
||||||
|
- name: arm_length
|
||||||
- name: neck_to_hip_length
|
landmarks:
|
||||||
landmarks:
|
- left_shoulder
|
||||||
- 11
|
- left_elbow
|
||||||
- 23
|
- left_wrist
|
||||||
|
- name: leg_length
|
||||||
#0 - nose
|
landmarks:
|
||||||
#1 - left eye (inner)
|
- left_hip
|
||||||
#2 - left eye
|
- left_knee
|
||||||
#3 - left eye (outer)
|
- left_ankle
|
||||||
#4 - right eye (inner)
|
|
||||||
#5 - right eye
|
|
||||||
#6 - right eye (outer)
|
|
||||||
#7 - left ear
|
|
||||||
#8 - right ear
|
|
||||||
#9 - mouth (left)
|
|
||||||
#10 - mouth (right)
|
|
||||||
#11 - left shoulder
|
|
||||||
#12 - right shoulder
|
|
||||||
#13 - left elbow
|
|
||||||
#14 - right elbow
|
|
||||||
#15 - left wrist
|
|
||||||
#16 - right wrist
|
|
||||||
#17 - left pinky
|
|
||||||
#18 - right pinky
|
|
||||||
#19 - left index
|
|
||||||
#20 - right index
|
|
||||||
#21 - left thumb
|
|
||||||
#22 - right thumb
|
|
||||||
#23 - left hip
|
|
||||||
#24 - right hip
|
|
||||||
#25 - left knee
|
|
||||||
#26 - right knee
|
|
||||||
#27 - left ankle
|
|
||||||
#28 - right ankle
|
|
||||||
#29 - left heel
|
|
||||||
#30 - right heel
|
|
||||||
#31 - left foot index
|
|
||||||
#32 - right foot index
|
|
||||||
|
|
||||||
|
10
dockerfile
10
dockerfile
@ -1,10 +0,0 @@
|
|||||||
FROM python:3.10-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY . /app
|
|
||||||
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y
|
|
||||||
|
|
||||||
ENTRYPOINT ["sh", "-c", "python3 landmarks.py \"$@\" 2>&1 | grep -vE '^(WARNING:|I0000|INFO:)' | awk '{print}'", "--"]
|
|
218
landmarks.js
Normal file
218
landmarks.js
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const ArgumentParser = require('argparse');
|
||||||
|
const cv = require('opencv');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
const { Pose, POSE_LANDMARKS } = require('@mediapipe/pose');
|
||||||
|
|
||||||
|
const logging = console;
|
||||||
|
const warnings = console;
|
||||||
|
|
||||||
|
class Landmarker {
|
||||||
|
static resizedHeight = 256;
|
||||||
|
static resizedWidth = 256;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.args = this.parseArgs();
|
||||||
|
this.measurements = this.loadLandmarks();
|
||||||
|
if (!this.args.frontImage) {
|
||||||
|
throw new Error("Front image needs to be passed");
|
||||||
|
}
|
||||||
|
if (!this.args.sideImage) {
|
||||||
|
throw new Error("Side image needs to be passed");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.frontImage = cv.imread(this.args.frontImage);
|
||||||
|
this.sideImage = cv.imread(this.args.sideImage);
|
||||||
|
|
||||||
|
this.frontImageResized = this.frontImage.resize(Landmarker.resizedHeight, Landmarker.resizedWidth);
|
||||||
|
this.sideImageResized = this.sideImage.resize(Landmarker.resizedHeight, Landmarker.resizedWidth);
|
||||||
|
|
||||||
|
this.distances = {};
|
||||||
|
|
||||||
|
this.personHeight = this.args.personHeight;
|
||||||
|
this.pixelHeight = this.args.pixelHeight;
|
||||||
|
|
||||||
|
this.pose = new Pose({
|
||||||
|
locateFile: (file) => {
|
||||||
|
return `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.landmarksIndices = [
|
||||||
|
POSE_LANDMARKS.LEFT_SHOULDER,
|
||||||
|
POSE_LANDMARKS.RIGHT_SHOULDER,
|
||||||
|
POSE_LANDMARKS.LEFT_ELBOW,
|
||||||
|
POSE_LANDMARKS.RIGHT_ELBOW,
|
||||||
|
POSE_LANDMARKS.LEFT_WRIST,
|
||||||
|
POSE_LANDMARKS.RIGHT_WRIST,
|
||||||
|
POSE_LANDMARKS.LEFT_HIP,
|
||||||
|
POSE_LANDMARKS.RIGHT_HIP,
|
||||||
|
POSE_LANDMARKS.LEFT_KNEE,
|
||||||
|
POSE_LANDMARKS.RIGHT_KNEE,
|
||||||
|
POSE_LANDMARKS.LEFT_ANKLE,
|
||||||
|
POSE_LANDMARKS.RIGHT_ANKLE
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
loadLandmarks() {
|
||||||
|
const file = fs.readFileSync(this.args.yamlFile, 'utf8');
|
||||||
|
const landmarksData = yaml.load(file);
|
||||||
|
const measurements = {};
|
||||||
|
for (const measurement of landmarksData.measurements) {
|
||||||
|
measurements[measurement.name] = measurement.landmarks;
|
||||||
|
}
|
||||||
|
return measurements;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseArgs() {
|
||||||
|
const parser = new ArgumentParser();
|
||||||
|
parser.add_argument('--front', { dest: 'frontImage', type: 'str', help: 'Front image' });
|
||||||
|
parser.add_argument('--side', { dest: 'sideImage', type: 'str', help: 'Side image' });
|
||||||
|
parser.add_argument('--poseDetectionConfidence', { dest: 'poseDetectionConfidence', default: 0.5, type: 'float', help: 'Confidence score for pose detection' });
|
||||||
|
parser.add_argument('--poseTrackingConfidence', { dest: 'poseTrackingConfidence', default: 0.5, type: 'float', help: 'Confidence score for pose tracking' });
|
||||||
|
parser.add_argument('--personHeight', { dest: 'personHeight', type: 'int', help: 'Person height in cm' });
|
||||||
|
parser.add_argument('--pixelHeight', { dest: 'pixelHeight', type: 'int', help: 'Pixel height of person' });
|
||||||
|
parser.add_argument('--measurement', { dest: 'measurement', nargs: '+', type: 'str', help: 'Type of measurement' });
|
||||||
|
parser.add_argument('--yamlFile', { dest: 'yamlFile', type: 'str', help: 'Path to the YAML file containing landmarks' });
|
||||||
|
return parser.parse_args();
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
await this.pose.initialize();
|
||||||
|
const { frontResults, sideResults } = await this.processImages();
|
||||||
|
|
||||||
|
this.getCenterTopPoint(sideResults);
|
||||||
|
|
||||||
|
const table = [];
|
||||||
|
if (this.args.measurement) {
|
||||||
|
for (const m of this.args.measurement) {
|
||||||
|
if (!this.measurements[m]) {
|
||||||
|
throw new Error("Incorrect input (input not present in config.yml)");
|
||||||
|
} else {
|
||||||
|
const distance = this.calculateDistanceBetweenLandmarks(frontResults, m);
|
||||||
|
table.push([m, distance]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const m in this.measurements) {
|
||||||
|
const distance = this.calculateDistanceBetweenLandmarks(frontResults, m);
|
||||||
|
table.push([m, distance]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.table(table);
|
||||||
|
|
||||||
|
this.pose.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async processImages() {
|
||||||
|
const frontResults = await this.pose.estimatePoses(this.frontImageResized);
|
||||||
|
const sideResults = await this.pose.estimatePoses(this.sideImageResized);
|
||||||
|
|
||||||
|
this.sideImageKeypoints = this.sideImageResized.copy();
|
||||||
|
this.frontImageKeypoints = this.frontImageResized.copy();
|
||||||
|
|
||||||
|
if (frontResults[0].landmarks) {
|
||||||
|
this.drawLandmarks(this.frontImageKeypoints, frontResults[0].landmarks, this.landmarksIndices);
|
||||||
|
}
|
||||||
|
if (sideResults[0].landmarks) {
|
||||||
|
this.drawLandmarks(this.sideImageKeypoints, sideResults[0].landmarks, this.landmarksIndices);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
frontResults: frontResults[0],
|
||||||
|
sideResults: sideResults[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pixelToMetricRatio() {
|
||||||
|
this.pixelHeight = this.pixelDistance * 2;
|
||||||
|
const pixelToMetricRatio = this.personHeight / this.pixelHeight;
|
||||||
|
logging.debug("pixelToMetricRatio %s", pixelToMetricRatio);
|
||||||
|
return pixelToMetricRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawLandmarks(image, landmarks, indices) {
|
||||||
|
for (const idx of indices) {
|
||||||
|
const landmark = landmarks[idx];
|
||||||
|
const h = image.rows;
|
||||||
|
const w = image.cols;
|
||||||
|
const cx = Math.round(landmark.x * w);
|
||||||
|
const cy = Math.round(landmark.y * h);
|
||||||
|
this.circle(image, cx, cy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
circle(image, cx, cy) {
|
||||||
|
return image.drawCircle(new cv.Point(cx, cy), 2, new cv.Vec(255, 0, 0), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateDistanceBetweenLandmarks(frontResults, measurementName) {
|
||||||
|
if (!frontResults.landmarks) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const landmarks = frontResults.landmarks;
|
||||||
|
const landmarkNames = this.measurements[measurementName];
|
||||||
|
|
||||||
|
let totalDistance = 0;
|
||||||
|
for (let i = 0; i < landmarkNames.length - 1; i++) {
|
||||||
|
const current = landmarks[landmarkNames[i]];
|
||||||
|
const next = landmarks[landmarkNames[i + 1]];
|
||||||
|
const pixelDistance = this.euclideanDistance(
|
||||||
|
current.x * Landmarker.resizedWidth,
|
||||||
|
current.y * Landmarker.resizedHeight,
|
||||||
|
next.x * Landmarker.resizedWidth,
|
||||||
|
next.y * Landmarker.resizedHeight
|
||||||
|
);
|
||||||
|
const realDistance = pixelDistance * this.pixelToMetricRatio();
|
||||||
|
totalDistance += realDistance;
|
||||||
|
}
|
||||||
|
return totalDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
euclideanDistance(x1, y1, x2, y2) {
|
||||||
|
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCenterTopPoint(sideResults) {
|
||||||
|
const grayImage = this.sideImageKeypoints.cvtColor(cv.COLOR_BGR2GRAY);
|
||||||
|
const blurredImage = grayImage.gaussianBlur(new cv.Size(5, 5), 0);
|
||||||
|
const roi = blurredImage.getRegion(new cv.Rect(0, 0, this.sideImageResized.cols, Math.floor(this.sideImageResized.rows / 2)));
|
||||||
|
this.edges = roi.canny(50, 150);
|
||||||
|
const contours = this.edges.findContours(cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);
|
||||||
|
|
||||||
|
let xt, yt;
|
||||||
|
this.topmostPoint = null;
|
||||||
|
|
||||||
|
if (contours.length > 0) {
|
||||||
|
const largestContour = contours.sort((a, b) => b.area - a.area)[0];
|
||||||
|
this.topmostPoint = largestContour.minBy(pt => pt.y);
|
||||||
|
xt = this.topmostPoint.x;
|
||||||
|
yt = this.topmostPoint.y;
|
||||||
|
|
||||||
|
this.circle(this.sideImageKeypoints, xt, yt);
|
||||||
|
}
|
||||||
|
|
||||||
|
let xc, yc;
|
||||||
|
if (sideResults.landmarks) {
|
||||||
|
const leftHip = sideResults.landmarks[POSE_LANDMARKS.LEFT_HIP];
|
||||||
|
const rightHip = sideResults.landmarks[POSE_LANDMARKS.RIGHT_HIP];
|
||||||
|
const centerPoint = [
|
||||||
|
(leftHip.x + rightHip.x) / 2,
|
||||||
|
(leftHip.y + rightHip.y) / 2
|
||||||
|
];
|
||||||
|
xc = Math.round(centerPoint[0] * this.sideImageResized.cols);
|
||||||
|
yc = Math.round(centerPoint[1] * this.sideImageResized.rows);
|
||||||
|
this.circle(this.sideImageKeypoints, xc, yc);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pixelDistance = this.euclideanDistance(xt, yt, xc, yc);
|
||||||
|
this.pixelDistance *= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const landmarker = new Landmarker();
|
||||||
|
await landmarker.run();
|
||||||
|
})();
|
154
landmarks.py
154
landmarks.py
@ -18,6 +18,41 @@ warnings.filterwarnings(
|
|||||||
module="google.protobuf",
|
module="google.protobuf",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LANDMARK_NAME_TO_INDEX = {
|
||||||
|
"nose": 0,
|
||||||
|
"left_eye_inner": 1,
|
||||||
|
"left_eye": 2,
|
||||||
|
"left_eye_outer": 3,
|
||||||
|
"right_eye_inner": 4,
|
||||||
|
"right_eye": 5,
|
||||||
|
"right_eye_outer": 6,
|
||||||
|
"left_ear": 7,
|
||||||
|
"right_ear": 8,
|
||||||
|
"mouth_left": 9,
|
||||||
|
"mouth_right": 10,
|
||||||
|
"left_shoulder": 11,
|
||||||
|
"right_shoulder": 12,
|
||||||
|
"left_elbow": 13,
|
||||||
|
"right_elbow": 14,
|
||||||
|
"left_wrist": 15,
|
||||||
|
"right_wrist": 16,
|
||||||
|
"left_pinky": 17,
|
||||||
|
"right_pinky": 18,
|
||||||
|
"left_index": 19,
|
||||||
|
"right_index": 20,
|
||||||
|
"left_thumb": 21,
|
||||||
|
"right_thumb": 22,
|
||||||
|
"left_hip": 23,
|
||||||
|
"right_hip": 24,
|
||||||
|
"left_knee": 25,
|
||||||
|
"right_knee": 26,
|
||||||
|
"left_ankle": 27,
|
||||||
|
"right_ankle": 28,
|
||||||
|
"left_heel": 29,
|
||||||
|
"right_heel": 30,
|
||||||
|
"left_foot_index": 31,
|
||||||
|
"right_foot_index": 32,
|
||||||
|
}
|
||||||
|
|
||||||
class Landmarker:
|
class Landmarker:
|
||||||
|
|
||||||
@ -50,18 +85,18 @@ class Landmarker:
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.landmarks_indices = [
|
self.landmarks_indices = [
|
||||||
pose.PoseLandmark.LEFT_SHOULDER.value,
|
LANDMARK_NAME_TO_INDEX["left_shoulder"],
|
||||||
pose.PoseLandmark.RIGHT_SHOULDER.value,
|
LANDMARK_NAME_TO_INDEX["right_shoulder"],
|
||||||
pose.PoseLandmark.LEFT_ELBOW.value,
|
LANDMARK_NAME_TO_INDEX["left_elbow"],
|
||||||
pose.PoseLandmark.RIGHT_ELBOW.value,
|
LANDMARK_NAME_TO_INDEX["right_elbow"],
|
||||||
pose.PoseLandmark.LEFT_WRIST.value,
|
LANDMARK_NAME_TO_INDEX["left_wrist"],
|
||||||
pose.PoseLandmark.RIGHT_WRIST.value,
|
LANDMARK_NAME_TO_INDEX["right_wrist"],
|
||||||
pose.PoseLandmark.LEFT_HIP.value,
|
LANDMARK_NAME_TO_INDEX["left_hip"],
|
||||||
pose.PoseLandmark.RIGHT_HIP.value,
|
LANDMARK_NAME_TO_INDEX["right_hip"],
|
||||||
pose.PoseLandmark.LEFT_KNEE.value,
|
LANDMARK_NAME_TO_INDEX["left_knee"],
|
||||||
pose.PoseLandmark.RIGHT_KNEE.value,
|
LANDMARK_NAME_TO_INDEX["right_knee"],
|
||||||
pose.PoseLandmark.LEFT_ANKLE.value,
|
LANDMARK_NAME_TO_INDEX["left_ankle"],
|
||||||
pose.PoseLandmark.RIGHT_ANKLE.value,
|
LANDMARK_NAME_TO_INDEX["right_ankle"],
|
||||||
]
|
]
|
||||||
|
|
||||||
def load_landmarks(self):
|
def load_landmarks(self):
|
||||||
@ -69,7 +104,7 @@ class Landmarker:
|
|||||||
landmarks_data = yaml.safe_load(file)
|
landmarks_data = yaml.safe_load(file)
|
||||||
measurements = {}
|
measurements = {}
|
||||||
for measurement in landmarks_data["measurements"]:
|
for measurement in landmarks_data["measurements"]:
|
||||||
measurements[measurement["name"]] = measurement["landmarks"]
|
measurements[measurement["name"]] = [LANDMARK_NAME_TO_INDEX[l] for l in measurement["landmarks"]]
|
||||||
return measurements
|
return measurements
|
||||||
|
|
||||||
def parse_args(self):
|
def parse_args(self):
|
||||||
@ -257,66 +292,41 @@ class Landmarker:
|
|||||||
]
|
]
|
||||||
self.edges = cv2.Canny(roi, 50, 150)
|
self.edges = cv2.Canny(roi, 50, 150)
|
||||||
contours, _ = cv2.findContours(
|
contours, _ = cv2.findContours(
|
||||||
self.edges,
|
self.edges.copy(),
|
||||||
cv2.RETR_EXTERNAL,
|
cv2.RETR_TREE,
|
||||||
cv2.CHAIN_APPROX_SIMPLE,
|
cv2.CHAIN_APPROX_SIMPLE,
|
||||||
)
|
)
|
||||||
xt, yt = None, None
|
max_contour = max(contours, key=cv2.contourArea)
|
||||||
self.topmost_point = None
|
rect = cv2.minAreaRect(max_contour)
|
||||||
|
box = cv2.boxPoints(rect)
|
||||||
|
box = sorted(
|
||||||
|
list(box),
|
||||||
|
key=lambda p: p[1],
|
||||||
|
)
|
||||||
|
top_point = min(
|
||||||
|
box[0],
|
||||||
|
box[1],
|
||||||
|
key=lambda p: p[0],
|
||||||
|
)
|
||||||
|
|
||||||
if contours:
|
left_hip = side_results.pose_landmarks.landmark[LANDMARK_NAME_TO_INDEX["left_hip"]]
|
||||||
largest_contour = max(
|
right_hip = side_results.pose_landmarks.landmark[LANDMARK_NAME_TO_INDEX["right_hip"]]
|
||||||
contours,
|
|
||||||
key=cv2.contourArea,
|
center_x = (left_hip.x + right_hip.x) / 2
|
||||||
)
|
center_y = (left_hip.y + right_hip.y) / 2
|
||||||
self.topmost_point = tuple(largest_contour[largest_contour[:, :, 1].argmin()][0])
|
|
||||||
xt, yt = self.topmost_point
|
center_x, center_y = (
|
||||||
|
int(center_x * self.resized_width),
|
||||||
cv2.circle(
|
int(center_y * self.resized_height),
|
||||||
self.side_image_keypoints,
|
)
|
||||||
(xt, yt),
|
|
||||||
2,
|
self.pixel_distance = self.euclidean_distance(
|
||||||
(255, 255, 0),
|
top_point[0],
|
||||||
-1,
|
top_point[1],
|
||||||
)
|
center_x,
|
||||||
xc, yc = None, None
|
center_y,
|
||||||
landmarks = side_results.pose_landmarks.landmark
|
)
|
||||||
|
|
||||||
if side_results.pose_landmarks:
|
|
||||||
left_hip = landmarks[pose.PoseLandmark.LEFT_HIP.value]
|
|
||||||
right_hip = landmarks[pose.PoseLandmark.RIGHT_HIP.value]
|
|
||||||
center_point = (
|
|
||||||
(left_hip.x + right_hip.x) / 2,
|
|
||||||
(left_hip.y + right_hip.y) / 2,
|
|
||||||
)
|
|
||||||
center_point = (
|
|
||||||
int(center_point[0] * self.side_image_resized.shape[1]),
|
|
||||||
int(center_point[1] * self.side_image_resized.shape[0]),
|
|
||||||
)
|
|
||||||
xc, yc = center_point
|
|
||||||
self.circle(
|
|
||||||
self.side_image_keypoints,
|
|
||||||
xc,
|
|
||||||
yc,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.pixel_distance = self.euclidean_distance(xc, yc, xt, yt)
|
|
||||||
logging.debug(
|
|
||||||
"top_center_pixel_distance: %s",
|
|
||||||
self.pixel_distance,
|
|
||||||
)
|
|
||||||
self.pixel_height = self.pixel_distance * 2
|
|
||||||
logging.debug(
|
|
||||||
"pixel height: %s ",
|
|
||||||
self.pixel_height,
|
|
||||||
)
|
|
||||||
self.distance = self.euclidean_distance(xc, yc, xt, yt) * self.pixel_to_metric_ratio()
|
|
||||||
return self.distance
|
|
||||||
|
|
||||||
|
|
||||||
l = Landmarker()
|
|
||||||
try:
|
|
||||||
l.run()
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
landmarker = Landmarker()
|
||||||
|
landmarker.run()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user