OpenPoseAdapter.cpp
Go to the documentation of this file.
1/*
2 * This file is part of ArmarX.
3 *
4 * ArmarX is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2 as
6 * published by the Free Software Foundation.
7 *
8 * ArmarX is distributed in the hope that it will be useful, but
9 * WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * @package visionx::MonocularOpenPoseEstimation
17 * @author Fabian Peller <fabian.peller@kit.edu>
18 * @date 2020
19 * @copyright http://www.gnu.org/licenses/gpl-2.0.txt
20 * GNU General Public License
21 */
22
23// Header
24#include "OpenPoseAdapter.h"
25
26// OpenPose
27#include <openpose/pose/poseParameters.hpp>
28#include <openpose/pose/poseParametersRender.hpp>
29
30// IVT
31#include <Image/ImageProcessor.h>
32#include <Image/IplImageAdaptor.h>
33#include <Image/PrimitivesDrawer.h>
34
35// ArmarX
40
41using namespace visionx;
42using namespace armarx;
43
44namespace
45{
46 // This is a workaround because in OpenPose 1.4, this function was called `intRound`, whereas
47 // in OpenPose 1.5.1 it was renamed to `positiveIntRound`. This function is now defined in this
48 // implementation as long as we use several OpenPose versions. As soon as we move to 1.5.1 we
49 // can remove this and use 'op::positiveIntRound` instead.
50 template <typename T>
51 inline int
52 positiveIntRound(const T a)
53 {
54 return int(a + 0.5f);
55 }
56} // namespace
57
59{
60 setupOpenPoseEnvironment();
61}
62
63// Initialize all variables to interact with openpose
64void
65OpenPoseAdapter::setupOpenPoseEnvironment()
66{
67 std::lock_guard l(op_running_mutex);
68
69 TIMING_START(setupOpenPose);
70 ARMARX_INFO_S << "setting up open pose environment";
71
72 auto op_model_pose_conv = ConvertToOPCustomType(settings.model_pose);
73 poseModel = op::flagsToPoseModel(op_model_pose_conv);
74
75 auto op_net_resolution_conv = ConvertToOPCustomType(settings.net_resolution);
76 auto op_output_resolution_conv = ConvertToOPCustomType(settings.output_resolution);
77
78 const op::Point<int> netInputSize = op::flagsToPoint(op_net_resolution_conv, "-1x368");
79 const op::Point<int> outputSize = op::flagsToPoint(op_output_resolution_conv, "-1x-1");
80
81#if (OPENPOSE_VERSION_MAJOR > 1) || (OPENPOSE_VERSION_MAJOR == 1 && OPENPOSE_VERSION_MINOR > 7) || \
82 (OPENPOSE_VERSION_MAJOR == 1 && OPENPOSE_VERSION_MINOR == 7 && OPENPOSE_VERSION_PATCH > 0)
83 scaleAndSizeExtractor = std::make_shared<op::ScaleAndSizeExtractor>(
84 netInputSize,
85 -1, // unclear what this value should be from openpose documentation
86 outputSize,
89#else
90 scaleAndSizeExtractor = std::make_shared<op::ScaleAndSizeExtractor>(
91 netInputSize, outputSize, settings.scale_number, settings.scale_gap);
92#endif
93 cvMatToOpInput = std::make_shared<op::CvMatToOpInput>(poseModel);
94 cvMatToOpOutput.reset(new op::CvMatToOpOutput());
95 opOutputToCvMat.reset(new op::OpOutputToCvMat());
96
97 std::string modelFolder = settings.model_folder;
99 modelFolder,
100 modelFolder,
101 {"/usr/share/OpenPose/", "/usr/local/share/OpenPose/", OPENPOSE_MODELS});
102 if (!found)
103 {
104 ARMARX_ERROR << "Could not find model folder at " << modelFolder;
105 return;
106 }
107 if (!modelFolder.empty() && *modelFolder.rbegin() != '/')
108 {
109 modelFolder += "/";
110 }
111 ARMARX_INFO << "Found model path at: " << modelFolder;
113 std::make_shared<op::PoseExtractorCaffe>(poseModel, modelFolder, settings.num_gpu_start);
114 poseExtractorCaffe->initializationOnThread();
115
116 TIMING_END(setupOpenPose);
117}
118
119// run openpose and return the openpose representation of keypoints
120op::Array<float>
121OpenPoseAdapter::getOpenposeKeypoints(const CByteImage* inputImage)
122{
123 std::lock_guard l(op_running_mutex);
124
125 // Step 1 - Convert image to cv::Mat
126 IplImage* iplImage = IplImageAdaptor::Adapt(inputImage);
127 cv::Mat opInputImage = cv::cvarrToMat(iplImage);
128 const op::Point<int> imageSize{opInputImage.cols, opInputImage.rows};
129
130 // Step 2 - Get desired scale sizes
131 std::vector<double> scaleInputToNetInputs;
132 std::vector<op::Point<int>> netInputSizes;
133 double scaleInputToOutput;
134 op::Point<int> outputResolution;
135 std::tie(scaleInputToNetInputs, netInputSizes, scaleInputToOutput, outputResolution) =
136 scaleAndSizeExtractor->extract(imageSize);
137
138 // Step 3 - Format input image to OpenPose input and output formats
139 auto inputImage_conv = ConvertToOPCustomType(opInputImage);
140 const auto netInputArray =
141 cvMatToOpInput->createArray(inputImage_conv, scaleInputToNetInputs, netInputSizes);
142
143 // Step 4 - Estimate poseKeypoints
144 poseExtractorCaffe->forwardPass(netInputArray, imageSize, scaleInputToNetInputs);
145 const op::Array<float> poseKeypoints = poseExtractorCaffe->getPoseKeypoints();
146
147 return poseKeypoints;
148}
149
150// convert the openpose representation of keypoints into our ice version
151HumanPose2DMap
152OpenPoseAdapter::convert2DKeypointsToIce(const op::Array<float>& op_keypoints,
153 const CByteImage* rgbImage) const
154{
155 ARMARX_CHECK_NOT_NULL(rgbImage);
156
157 HumanPose2DMap ice_entities;
158 const std::map<unsigned int, std::string> poseBodyPartMapping =
159 op::getPoseBodyPartMapping(poseModel);
160
161 if (op_keypoints.getSize().empty())
162 {
163 return ice_entities;
164 }
165
166 int entities = op_keypoints.getSize().at(0);
167 int points = op_keypoints.getSize().at(1);
168
169 ARMARX_CHECK_EXPRESSION(poseBodyPartMapping.size() > 0);
170 ARMARX_CHECK_EXPRESSION(points == static_cast<int>(poseBodyPartMapping.size()) - 1);
171
172 for (int i = 0; i < entities; i++)
173 {
174 std::string entity_id = std::to_string(i);
175
176 ice_entities[entity_id] = HumanPose2D();
177 Keypoint2DMap ice_keypoint_map;
178 for (int id = 0; id < points; id++)
179 {
180 Keypoint2D keypoint;
181 float x = op_keypoints.at({i, id, 0});
182 float y = op_keypoints.at({i, id, 1});
183 float c = op_keypoints.at({i, id, 2});
184 if (x == 0.0f && y == 0.0f && c == 0.0f)
185 {
186 continue; // Invalid keypoint from openpose
187 }
188 keypoint.id = id;
189 keypoint.label = poseBodyPartMapping.at(id);
190 keypoint.x = x;
191 keypoint.y = y;
192 keypoint.confidence = c;
193 keypoint.dominantColor =
194 getDominantColorOfPatch(rgbImage, Vec2d{x, y}, rgbImage->width / 50);
195 ice_keypoint_map[keypoint.label] = keypoint;
196 }
197
198 if (ice_keypoint_map.size() >= settings.minimum_number_of_valid_keypoints_per_entitiy)
199 {
200 ice_entities[entity_id].keypointMap = ice_keypoint_map;
201 }
202 }
203
204 return ice_entities;
205}
206
207armarx::DrawColor24Bit
209 const Vec2d& point,
210 int windowSize) const
211{
212 if (point.x < 0 || point.y < 0 || point.x >= image.width || point.y >= image.height)
213 return DrawColor24Bit{0, 0, 0}; // Black
214 int divisor = 256 / 3; // split into 3*3*3 bins
215 typedef std::tuple<Ice::Byte, Ice::Byte, Ice::Byte> RGBTuple;
216 std::map<RGBTuple, int> histogram;
217 int halfWindowSize = static_cast<int>(windowSize * 0.5);
218 int left = std::max<int>(0, static_cast<int>(point.x) - halfWindowSize);
219 int top = std::max<int>(0, static_cast<int>(point.y) - halfWindowSize);
220 int right = std::min<int>(image.width, static_cast<int>(point.x) + halfWindowSize);
221 int bottom = std::min<int>(image.height, static_cast<int>(point.y) + halfWindowSize);
222
223 for (int x = left; x < right; x++)
224 {
225 for (int y = top; y < bottom; y++)
226 {
227 int pixelPos = (y * image.width + x) * 3;
228 auto tuple = std::make_tuple<Ice::Byte, Ice::Byte, Ice::Byte>(
229 static_cast<Ice::Byte>(image.pixels[pixelPos] / divisor),
230 static_cast<Ice::Byte>(image.pixels[pixelPos + 1] / divisor),
231 static_cast<Ice::Byte>(image.pixels[pixelPos + 2] / divisor));
232 if (histogram.count(tuple))
233 {
234 histogram.at(tuple)++;
235 }
236 else
237 {
238 histogram[tuple] = 1;
239 }
240 }
241 }
242
243 float maxHistogramValue = 0;
244 RGBTuple dominantColor;
245 for (auto& pair : histogram)
246 {
247 if (pair.second > maxHistogramValue)
248 {
249 dominantColor = pair.first;
250 maxHistogramValue = pair.second;
251 }
252 }
253 auto rgb = DrawColor24Bit{static_cast<Ice::Byte>(std::get<0>(dominantColor) * divisor),
254 static_cast<Ice::Byte>(std::get<1>(dominantColor) * divisor),
255 static_cast<Ice::Byte>(std::get<2>(dominantColor) * divisor)};
256 return rgb;
257}
258
259void
260OpenPoseAdapter::render2DResultImage(const CByteImage& inputImage,
261 const op::Array<float>& keypoints,
262 CByteImage& resultImage,
263 float renderThreshold)
264{
265 //ARMARX_CHECK_EXPRESSION(inputImage.width == resultImage.width);
266 //ARMARX_CHECK_EXPRESSION(inputImage.height == resultImage.height);
267 resultImage.Set(inputImage.width, inputImage.height, inputImage.type);
268 ::ImageProcessor::CopyImage(&inputImage, &resultImage);
269
270 if (keypoints.getSize().empty())
271 {
272 return;
273 }
274
275 const std::vector<unsigned int>& posePartPairs = op::getPoseBodyPartPairsRender(poseModel);
276
277 // Get colors for points and lines from openpose
278 std::vector<float> keypointColors = op::getPoseColors(poseModel);
279
280 const auto thicknessCircleRatio = 1.f / 75.f;
281 const auto thicknessLineRatioWRTCircle = 0.75f;
282 const auto area = inputImage.width * inputImage.height;
283 const int numberKeypoints = keypoints.getSize(1);
284
285 for (int person = 0; person < keypoints.getSize(0); person++)
286 {
287 const auto personRectangle = op::getKeypointsRectangle(keypoints, person, 0.1f);
288 if (personRectangle.area() > 0)
289 {
290 // define size-dependent variables
291 const auto ratioAreas = op::fastMin(
292 1.f,
293 op::fastMax(personRectangle.width / static_cast<float>(inputImage.width),
294 personRectangle.height / static_cast<float>(inputImage.height)));
295 const auto thicknessRatio =
296 op::fastMax(positiveIntRound<float>(static_cast<float>(std::sqrt(area)) *
297 thicknessCircleRatio * ratioAreas),
298 2);
299 // Negative thickness in ivt::drawCircle means that a filled circle is to be drawn.
300 const auto thicknessCircle = op::fastMax(1, (ratioAreas > 0.05f ? thicknessRatio : -1));
301 const auto thicknessLine =
302 op::fastMax(1, positiveIntRound(thicknessRatio * thicknessLineRatioWRTCircle));
303 const auto radius = thicknessRatio / 2;
304
305 // Draw Lines
306 for (unsigned int i = 0; i < posePartPairs.size(); i = i + 2)
307 {
308 const int index1 = (person * numberKeypoints + static_cast<int>(posePartPairs[i])) *
309 keypoints.getSize(2);
310 const int index2 =
311 (person * numberKeypoints + static_cast<int>(posePartPairs[i + 1])) *
312 keypoints.getSize(2);
313
314 float x1 = keypoints[index1 + 0];
315 float y1 = keypoints[index1 + 1];
316 float x2 = keypoints[index2 + 0];
317 float y2 = keypoints[index2 + 1];
318
319 if (!(x1 == 0.0f && y1 == 0.0f) && !(x2 == 0.0f && y2 == 0.0f))
320 {
321 if (keypoints[index1 + 2] > renderThreshold &&
322 keypoints[index2 + 2] > renderThreshold)
323 {
324 unsigned int colorIndex = posePartPairs[i + 1] * 3;
325 ::PrimitivesDrawer::DrawLine(
326 &resultImage,
327 Vec2d({x1, y1}),
328 Vec2d({x2, y2}),
329 static_cast<int>(keypointColors[colorIndex + 2]),
330 static_cast<int>(keypointColors[colorIndex + 1]),
331 static_cast<int>(keypointColors[colorIndex + 0]),
332 thicknessLine);
333 }
334 }
335 }
336
337 // Draw points
338 for (int i = 0; i < numberKeypoints; i++)
339 {
340 const int index = (person * numberKeypoints + i) * keypoints.getSize(2);
341 float x = keypoints[index + 0];
342 float y = keypoints[index + 1];
343
344 if (!(x == 0.0f && y == 0.0f) && keypoints[index + 2] > renderThreshold)
345 {
346 unsigned int colorIndex = static_cast<unsigned int>(i * 3);
347
348 ::PrimitivesDrawer::DrawCircle(&resultImage,
349 x,
350 y,
351 radius,
352 static_cast<int>(keypointColors[colorIndex + 2]),
353 static_cast<int>(keypointColors[colorIndex + 1]),
354 static_cast<int>(keypointColors[colorIndex + 0]),
355 thicknessCircle);
356 }
357 }
358 }
359 }
360}
uint8_t index
constexpr T c
static bool getAbsolutePath(const std::string &relativeFilename, std::string &storeAbsoluteFilename, const std::vector< std::string > &additionalSearchPaths={}, bool verbose=true)
OpenPoseSettings settings
op::Array< float > getOpenposeKeypoints(const CByteImage *imageBuffer)
std::shared_ptr< op::CvMatToOpInput > cvMatToOpInput
std::shared_ptr< op::ScaleAndSizeExtractor > scaleAndSizeExtractor
std::shared_ptr< op::PoseExtractorCaffe > poseExtractorCaffe
std::shared_ptr< op::OpOutputToCvMat > opOutputToCvMat
OpenPoseAdapter(const OpenPoseSettings &)
void render2DResultImage(const CByteImage &inputImage, const op::Array< float > &keypoints, CByteImage &resultImage, float renderThreshold)
armarx::DrawColor24Bit getDominantColorOfPatch(const CByteImage &image, const Vec2d &point, int windowSize) const
HumanPose2DMap convert2DKeypointsToIce(const op::Array< float > &op_keypoints, const CByteImage *rgbImage) const
static std::string ConvertToOPCustomType(const std::string &x)
std::shared_ptr< op::CvMatToOpOutput > cvMatToOpOutput
#define ARMARX_CHECK_EXPRESSION(expression)
This macro evaluates the expression and if it turns out to be false it will throw an ExpressionExcept...
#define ARMARX_CHECK_NOT_NULL(ptr)
This macro evaluates whether ptr is not null and if it turns out to be false it will throw an Express...
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:181
#define ARMARX_ERROR
The logging level for unexpected behaviour, that must be fixed.
Definition Logging.h:196
#define ARMARX_INFO_S
Definition Logging.h:202
#define TIMING_START(name)
Helper macro to do timing tests.
Definition TimeUtil.h:289
#define TIMING_END(name)
Prints duration.
Definition TimeUtil.h:306
This file offers overloads of toIce() and fromIce() functions for STL container types.
ArmarX headers.