Spaces:
Running
Running
Upload holistic.js
Browse files- static/js/holistic.js +276 -0
static/js/holistic.js
ADDED
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import DeviceDetector from "https://cdn.skypack.dev/[email protected]";
|
2 |
+
// 用法: testSupport({client?: string, os?: string}[])
|
3 |
+
// Client 和 os 是正则表达式。
|
4 |
+
// 参见: https://cdn.jsdelivr.net/npm/[email protected]/README.md
|
5 |
+
// 了解 client 和 os 的合法值
|
6 |
+
testSupport([
|
7 |
+
{ client: 'Chrome' },
|
8 |
+
]);
|
9 |
+
|
10 |
+
function testSupport(supportedDevices) {
|
11 |
+
const deviceDetector = new DeviceDetector();
|
12 |
+
const detectedDevice = deviceDetector.parse(navigator.userAgent);
|
13 |
+
let isSupported = false;
|
14 |
+
for (const device of supportedDevices) {
|
15 |
+
if (device.client !== undefined) {
|
16 |
+
const re = new RegExp(`^${device.client}$`);
|
17 |
+
if (!re.test(detectedDevice.client.name)) {
|
18 |
+
continue;
|
19 |
+
}
|
20 |
+
}
|
21 |
+
if (device.os !== undefined) {
|
22 |
+
const re = new RegExp(`^${device.os}$`);
|
23 |
+
if (!re.test(detectedDevice.os.name)) {
|
24 |
+
continue;
|
25 |
+
}
|
26 |
+
}
|
27 |
+
isSupported = true;
|
28 |
+
break;
|
29 |
+
}
|
30 |
+
if (!isSupported) {
|
31 |
+
alert(`此演示在 ${detectedDevice.client.name}/${detectedDevice.os.name} 上运行时 ` +
|
32 |
+
`目前不能很好地支持,继续使用需自担风险。`);
|
33 |
+
}
|
34 |
+
}
|
35 |
+
|
36 |
+
const controls = window;
|
37 |
+
const mpHolistic = window;
|
38 |
+
const drawingUtils = window;
|
39 |
+
const config = { locateFile: (file) => {
|
40 |
+
return `https://cdn.jsdelivr.net/npm/@mediapipe/holistic@` +
|
41 |
+
`${mpHolistic.VERSION}/${file}`;
|
42 |
+
} };
|
43 |
+
|
44 |
+
// 我们的输入帧将来自这里。
|
45 |
+
const videoElement = document.getElementsByClassName('input_video')[0];
|
46 |
+
const canvasElement = document.getElementsByClassName('output_canvas')[0];
|
47 |
+
const controlsElement = document.getElementsByClassName('control-panel')[0];
|
48 |
+
const canvasCtx = canvasElement.getContext('2d');
|
49 |
+
|
50 |
+
// 我们稍后会将这个添加到控制面板中,但我们会在这里保存它,
|
51 |
+
// 以便每次图形运行时都可以调用 tick()。
|
52 |
+
const fpsControl = new controls.FPS();
|
53 |
+
|
54 |
+
// 优化:在隐藏动画完成后关闭动画旋转器。
|
55 |
+
const spinner = document.querySelector('.loading');
|
56 |
+
spinner.ontransitionend = () => {
|
57 |
+
spinner.style.display = 'none';
|
58 |
+
};
|
59 |
+
|
60 |
+
function removeElements(landmarks, elements) {
|
61 |
+
for (const element of elements) {
|
62 |
+
delete landmarks[element];
|
63 |
+
}
|
64 |
+
}
|
65 |
+
|
66 |
+
function removeLandmarks(results) {
|
67 |
+
if (results.poseLandmarks) {
|
68 |
+
removeElements(results.poseLandmarks, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 16, 17, 18, 19, 20, 21, 22]);
|
69 |
+
}
|
70 |
+
}
|
71 |
+
|
72 |
+
function connect(ctx, connectors) {
|
73 |
+
const canvas = ctx.canvas;
|
74 |
+
for (const connector of connectors) {
|
75 |
+
const from = connector[0];
|
76 |
+
const to = connector[1];
|
77 |
+
if (from && to) {
|
78 |
+
if (from.visibility && to.visibility &&
|
79 |
+
(from.visibility < 0.1 || to.visibility < 0.1)) {
|
80 |
+
continue;
|
81 |
+
}
|
82 |
+
ctx.beginPath();
|
83 |
+
ctx.moveTo(from.x * canvas.width, from.y * canvas.height);
|
84 |
+
ctx.lineTo(to.x * canvas.width, to.y * canvas.height);
|
85 |
+
ctx.stroke();
|
86 |
+
}
|
87 |
+
}
|
88 |
+
}
|
89 |
+
|
90 |
+
let activeEffect = 'mask';
|
91 |
+
|
92 |
+
function onResults(results) {
|
93 |
+
// 隐藏旋转器。
|
94 |
+
document.body.classList.add('loaded');
|
95 |
+
|
96 |
+
// 移除我们不想绘制的关键点。
|
97 |
+
removeLandmarks(results);
|
98 |
+
|
99 |
+
// 更新帧率。
|
100 |
+
fpsControl.tick();
|
101 |
+
|
102 |
+
// 绘制叠加层。
|
103 |
+
canvasCtx.save();
|
104 |
+
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
|
105 |
+
|
106 |
+
if (results.segmentationMask) {
|
107 |
+
canvasCtx.drawImage(results.segmentationMask, 0, 0, canvasElement.width, canvasElement.height);
|
108 |
+
|
109 |
+
// 仅覆盖现有像素。
|
110 |
+
if (activeEffect === 'mask' || activeEffect === 'both') {
|
111 |
+
canvasCtx.globalCompositeOperation = 'source-in';
|
112 |
+
// 这可以是颜色、纹理或其他...
|
113 |
+
canvasCtx.fillStyle = '#00FF007F';
|
114 |
+
canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height);
|
115 |
+
}
|
116 |
+
else {
|
117 |
+
canvasCtx.globalCompositeOperation = 'source-out';
|
118 |
+
canvasCtx.fillStyle = '#0000FF7F';
|
119 |
+
canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height);
|
120 |
+
}
|
121 |
+
|
122 |
+
// 仅覆盖缺失的像素。
|
123 |
+
canvasCtx.globalCompositeOperation = 'destination-atop';
|
124 |
+
canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
|
125 |
+
canvasCtx.globalCompositeOperation = 'source-over';
|
126 |
+
}
|
127 |
+
else {
|
128 |
+
canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
|
129 |
+
}
|
130 |
+
|
131 |
+
// 连接肘部到手部。首先做这个,这样其他图形将绘制在这些标记之上。
|
132 |
+
canvasCtx.lineWidth = 5;
|
133 |
+
if (results.poseLandmarks) {
|
134 |
+
if (results.rightHandLandmarks) {
|
135 |
+
canvasCtx.strokeStyle = 'white';
|
136 |
+
connect(canvasCtx, [[
|
137 |
+
results.poseLandmarks[mpHolistic.POSE_LANDMARKS.RIGHT_ELBOW],
|
138 |
+
results.rightHandLandmarks[0]
|
139 |
+
]]);
|
140 |
+
}
|
141 |
+
if (results.leftHandLandmarks) {
|
142 |
+
canvasCtx.strokeStyle = 'white';
|
143 |
+
connect(canvasCtx, [[
|
144 |
+
results.poseLandmarks[mpHolistic.POSE_LANDMARKS.LEFT_ELBOW],
|
145 |
+
results.leftHandLandmarks[0]
|
146 |
+
]]);
|
147 |
+
}
|
148 |
+
}
|
149 |
+
|
150 |
+
// 姿势...
|
151 |
+
drawingUtils.drawConnectors(canvasCtx, results.poseLandmarks, mpHolistic.POSE_CONNECTIONS, { color: 'white' });
|
152 |
+
drawingUtils.drawLandmarks(canvasCtx, Object.values(mpHolistic.POSE_LANDMARKS_LEFT)
|
153 |
+
.map(index => results.poseLandmarks[index]), { visibilityMin: 0.65, color: 'white', fillColor: 'rgb(255,138,0)' });
|
154 |
+
drawingUtils.drawLandmarks(canvasCtx, Object.values(mpHolistic.POSE_LANDMARKS_RIGHT)
|
155 |
+
.map(index => results.poseLandmarks[index]), { visibilityMin: 0.65, color: 'white', fillColor: 'rgb(0,217,231)' });
|
156 |
+
|
157 |
+
// 手...
|
158 |
+
drawingUtils.drawConnectors(canvasCtx, results.rightHandLandmarks, mpHolistic.HAND_CONNECTIONS, { color: 'white' });
|
159 |
+
drawingUtils.drawLandmarks(canvasCtx, results.rightHandLandmarks, {
|
160 |
+
color: 'white',
|
161 |
+
fillColor: 'rgb(0,217,231)',
|
162 |
+
lineWidth: 2,
|
163 |
+
radius: (data) => {
|
164 |
+
return drawingUtils.lerp(data.from.z, -0.15, .1, 10, 1);
|
165 |
+
}
|
166 |
+
});
|
167 |
+
drawingUtils.drawConnectors(canvasCtx, results.leftHandLandmarks, mpHolistic.HAND_CONNECTIONS, { color: 'white' });
|
168 |
+
drawingUtils.drawLandmarks(canvasCtx, results.leftHandLandmarks, {
|
169 |
+
color: 'white',
|
170 |
+
fillColor: 'rgb(255,138,0)',
|
171 |
+
lineWidth: 2,
|
172 |
+
radius: (data) => {
|
173 |
+
return drawingUtils.lerp(data.from.z, -0.15, .1, 10, 1);
|
174 |
+
}
|
175 |
+
});
|
176 |
+
|
177 |
+
// 面部...
|
178 |
+
drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_TESSELATION, { color: '#C0C0C070', lineWidth: 1 });
|
179 |
+
drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_RIGHT_EYE, { color: 'rgb(0,217,231)' });
|
180 |
+
drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_RIGHT_EYEBROW, { color: 'rgb(0,217,231)' });
|
181 |
+
drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_LEFT_EYE, { color: 'rgb(255,138,0)' });
|
182 |
+
drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_LEFT_EYEBROW, { color: 'rgb(255,138,0)' });
|
183 |
+
drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_FACE_OVAL, { color: '#E0E0E0', lineWidth: 5 });
|
184 |
+
drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_LIPS, { color: '#E0E0E0', lineWidth: 5 });
|
185 |
+
|
186 |
+
canvasCtx.restore();
|
187 |
+
}
|
188 |
+
|
189 |
+
const holistic = new mpHolistic.Holistic(config);
|
190 |
+
holistic.onResults(onResults);
|
191 |
+
|
192 |
+
// 呈现一个控制面板,用户可以通过它操作解决方案选项。
|
193 |
+
new controls
|
194 |
+
.ControlPanel(controlsElement, {
|
195 |
+
selfieMode: true,
|
196 |
+
modelComplexity: 1,
|
197 |
+
smoothLandmarks: true,
|
198 |
+
enableSegmentation: false,
|
199 |
+
smoothSegmentation: true,
|
200 |
+
minDetectionConfidence: 0.5,
|
201 |
+
minTrackingConfidence: 0.5,
|
202 |
+
effect: 'background',
|
203 |
+
})
|
204 |
+
.add([
|
205 |
+
new controls.StaticText({ title: 'MediaPipe 全身姿态检测' }),
|
206 |
+
fpsControl,
|
207 |
+
new controls.Toggle({ title: '自拍模式', field: 'selfieMode' }),
|
208 |
+
new controls.SourcePicker({
|
209 |
+
onSourceChanged: () => {
|
210 |
+
// 重置,因为在源更改之间重置时,姿势会给出更好的结果。
|
211 |
+
holistic.reset();
|
212 |
+
},
|
213 |
+
onFrame: async (input, size) => {
|
214 |
+
const aspect = size.height / size.width;
|
215 |
+
let width, height;
|
216 |
+
if (window.innerWidth > window.innerHeight) {
|
217 |
+
height = window.innerHeight;
|
218 |
+
width = height / aspect;
|
219 |
+
}
|
220 |
+
else {
|
221 |
+
width = window.innerWidth;
|
222 |
+
height = width * aspect;
|
223 |
+
}
|
224 |
+
canvasElement.width = width;
|
225 |
+
canvasElement.height = height;
|
226 |
+
await holistic.send({ image: input });
|
227 |
+
},
|
228 |
+
}),
|
229 |
+
new controls.Slider({
|
230 |
+
title: '模型复杂度',
|
231 |
+
field: 'modelComplexity',
|
232 |
+
discrete: ['轻量', '完整', '重度'],
|
233 |
+
}),
|
234 |
+
new controls.Toggle({ title: '平滑关键点', field: 'smoothLandmarks' }),
|
235 |
+
new controls.Toggle({ title: '启用分割', field: 'enableSegmentation' }),
|
236 |
+
new controls.Toggle({ title: '平滑分割', field: 'smoothSegmentation' }),
|
237 |
+
new controls.Slider({
|
238 |
+
title: '最小检测置信度',
|
239 |
+
field: 'minDetectionConfidence',
|
240 |
+
range: [0, 1],
|
241 |
+
step: 0.01
|
242 |
+
}),
|
243 |
+
new controls.Slider({
|
244 |
+
title: '最小跟踪置信度',
|
245 |
+
field: 'minTrackingConfidence',
|
246 |
+
range: [0, 1],
|
247 |
+
step: 0.01
|
248 |
+
}),
|
249 |
+
new controls.Slider({
|
250 |
+
title: '效果',
|
251 |
+
field: 'effect',
|
252 |
+
discrete: { 'background': '背景', 'mask': '前景' },
|
253 |
+
}),
|
254 |
+
])
|
255 |
+
.on(x => {
|
256 |
+
const options = x;
|
257 |
+
videoElement.classList.toggle('selfie', options.selfieMode);
|
258 |
+
activeEffect = x['effect'];
|
259 |
+
holistic.setOptions(options);
|
260 |
+
});
|
261 |
+
|
262 |
+
// 添加窗口大小调整事件监听器,以确保画布大小随窗口变化而调整
|
263 |
+
window.addEventListener('resize', () => {
|
264 |
+
const aspect = videoElement.videoHeight / videoElement.videoWidth;
|
265 |
+
let width, height;
|
266 |
+
if (window.innerWidth > window.innerHeight) {
|
267 |
+
height = window.innerHeight;
|
268 |
+
width = height / aspect;
|
269 |
+
}
|
270 |
+
else {
|
271 |
+
width = window.innerWidth;
|
272 |
+
height = width * aspect;
|
273 |
+
}
|
274 |
+
canvasElement.width = width;
|
275 |
+
canvasElement.height = height;
|
276 |
+
});
|