forked from justadudewhohacks/opencv4nodejs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhandGestureRecognition0.js
191 lines (165 loc) · 6.06 KB
/
handGestureRecognition0.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
const cv = require('../');
const { grabFrames } = require('./utils');
// segmenting by skin color (has to be adjusted)
const skinColorUpper = hue => new cv.Vec(hue, 0.8 * 255, 0.6 * 255);
const skinColorLower = hue => new cv.Vec(hue, 0.1 * 255, 0.05 * 255);
const makeHandMask = (img) => {
// filter by skin color
const imgHLS = img.cvtColor(cv.COLOR_BGR2HLS);
const rangeMask = imgHLS.inRange(skinColorLower(0), skinColorUpper(15));
// remove noise
const blurred = rangeMask.blur(new cv.Size(10, 10));
const thresholded = blurred.threshold(200, 255, cv.THRESH_BINARY);
return thresholded;
};
const getHandContour = (handMask) => {
const mode = cv.RETR_EXTERNAL;
const method = cv.CHAIN_APPROX_SIMPLE;
const contours = handMask.findContours(mode, method);
// largest contour
return contours.sort((c0, c1) => c1.area - c0.area)[0];
};
// returns distance of two points
const ptDist = (pt1, pt2) => pt1.sub(pt2).norm();
// returns center of all points
const getCenterPt = pts => pts.reduce(
(sum, pt) => sum.add(pt),
new cv.Point(0, 0)
).div(pts.length);
// get the polygon from a contours hull such that there
// will be only a single hull point for a local neighborhood
const getRoughHull = (contour, maxDist) => {
// get hull indices and hull points
const hullIndices = contour.convexHullIndices();
const contourPoints = contour.getPoints();
const hullPointsWithIdx = hullIndices.map(idx => ({
pt: contourPoints[idx],
contourIdx: idx
}));
const hullPoints = hullPointsWithIdx.map(ptWithIdx => ptWithIdx.pt);
// group all points in local neighborhood
const ptsBelongToSameCluster = (pt1, pt2) => ptDist(pt1, pt2) < maxDist;
const { labels } = cv.partition(hullPoints, ptsBelongToSameCluster);
const pointsByLabel = new Map();
labels.forEach(l => pointsByLabel.set(l, []));
hullPointsWithIdx.forEach((ptWithIdx, i) => {
const label = labels[i];
pointsByLabel.get(label).push(ptWithIdx);
});
// map points in local neighborhood to most central point
const getMostCentralPoint = (pointGroup) => {
// find center
const center = getCenterPt(pointGroup.map(ptWithIdx => ptWithIdx.pt));
// sort ascending by distance to center
return pointGroup.sort(
(ptWithIdx1, ptWithIdx2) => ptDist(ptWithIdx1.pt, center) - ptDist(ptWithIdx2.pt, center)
)[0];
};
const pointGroups = Array.from(pointsByLabel.values());
// return contour indeces of most central points
return pointGroups.map(getMostCentralPoint).map(ptWithIdx => ptWithIdx.contourIdx);
};
const getHullDefectVertices = (handContour, hullIndices) => {
const defects = handContour.convexityDefects(hullIndices);
const handContourPoints = handContour.getPoints();
// get neighbor defect points of each hull point
const hullPointDefectNeighbors = new Map(hullIndices.map(idx => [idx, []]));
defects.forEach((defect) => {
const startPointIdx = defect.at(0);
const endPointIdx = defect.at(1);
const defectPointIdx = defect.at(2);
hullPointDefectNeighbors.get(startPointIdx).push(defectPointIdx);
hullPointDefectNeighbors.get(endPointIdx).push(defectPointIdx);
});
return Array.from(hullPointDefectNeighbors.keys())
// only consider hull points that have 2 neighbor defects
.filter(hullIndex => hullPointDefectNeighbors.get(hullIndex).length > 1)
// return vertex points
.map((hullIndex) => {
const defectNeighborsIdx = hullPointDefectNeighbors.get(hullIndex);
return ({
pt: handContourPoints[hullIndex],
d1: handContourPoints[defectNeighborsIdx[0]],
d2: handContourPoints[defectNeighborsIdx[1]]
});
});
};
const filterVerticesByAngle = (vertices, maxAngleDeg) =>
vertices.filter((v) => {
const sq = x => x * x;
const a = v.d1.sub(v.d2).norm();
const b = v.pt.sub(v.d1).norm();
const c = v.pt.sub(v.d2).norm();
const angleDeg = Math.acos(((sq(b) + sq(c)) - sq(a)) / (2 * b * c)) * (180 / Math.PI);
return angleDeg < maxAngleDeg;
});
const blue = new cv.Vec(255, 0, 0);
const green = new cv.Vec(0, 255, 0);
const red = new cv.Vec(0, 0, 255);
// main
const delay = 20;
grabFrames('../data/hand-gesture.mp4', delay, (frame) => {
const resizedImg = frame.resizeToMax(640);
const handMask = makeHandMask(resizedImg);
const handContour = getHandContour(handMask);
if (!handContour) {
return;
}
const maxPointDist = 25;
const hullIndices = getRoughHull(handContour, maxPointDist);
// get defect points of hull to contour and return vertices
// of each hull point to its defect points
const vertices = getHullDefectVertices(handContour, hullIndices);
// fingertip points are those which have a sharp angle to its defect points
const maxAngleDeg = 60;
const verticesWithValidAngle = filterVerticesByAngle(vertices, maxAngleDeg);
const result = resizedImg.copy();
// draw bounding box and center line
resizedImg.drawContours(
[handContour],
blue,
{ thickness: 2 }
);
// draw points and vertices
verticesWithValidAngle.forEach((v) => {
resizedImg.drawLine(
v.pt,
v.d1,
{ color: green, thickness: 2 }
);
resizedImg.drawLine(
v.pt,
v.d2,
{ color: green, thickness: 2 }
);
resizedImg.drawEllipse(
new cv.RotatedRect(v.pt, new cv.Size(20, 20), 0),
{ color: red, thickness: 2 }
);
result.drawEllipse(
new cv.RotatedRect(v.pt, new cv.Size(20, 20), 0),
{ color: red, thickness: 2 }
);
});
// display detection result
const numFingersUp = verticesWithValidAngle.length;
result.drawRectangle(
new cv.Point(10, 10),
new cv.Point(70, 70),
{ color: green, thickness: 2 }
);
const fontScale = 2;
result.putText(
String(numFingersUp),
new cv.Point(20, 60),
cv.FONT_ITALIC,
fontScale,
{ color: green, thickness: 2 }
);
const { rows, cols } = result;
const sideBySide = new cv.Mat(rows, cols * 2, cv.CV_8UC3);
result.copyTo(sideBySide.getRegion(new cv.Rect(0, 0, cols, rows)));
resizedImg.copyTo(sideBySide.getRegion(new cv.Rect(cols, 0, cols, rows)));
cv.imshow('handMask', handMask);
cv.imshow('result', sideBySide);
});