1 /***********************************************************************
2 RainMaker - Class to detect objects moving through a given range of
3 depths in a depth image sequence to trigger rainfall on virtual terrain.
4 Copyright (c) 2012-2015 Oliver Kreylos
6 This file is part of the Augmented Reality Sandbox (SARndbox).
8 The Augmented Reality Sandbox is free software; you can redistribute it
9 and/or modify it under the terms of the GNU General Public License as
10 published by the Free Software Foundation; either version 2 of the
11 License, or (at your option) any later version.
13 The Augmented Reality Sandbox is distributed in the hope that it will be
14 useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 General Public License for more details.
18 You should have received a copy of the GNU General Public License along
19 with the Augmented Reality Sandbox; if not, write to the Free Software
20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 ***********************************************************************/
23 #include "RainMaker.h"
25 #include <Misc/FunctionCalls.h>
26 #include <Geometry/HVector.h>
27 #include <Geometry/Plane.h>
29 #include "FindBlobs.h"
32 class BlobProperty
<unsigned short> { // Class to calculate the 3D centroid of a blob in depth image space
35 double pxs
, pys
, pzs
; // Accumulated components of centroid
36 size_t numPixels
; // Number of accumulated pixels
38 /* Constructors and destructors: */
41 : pxs(0.0), pys(0.0), pzs(0.0),
46 void addPixel(unsigned int x
, unsigned int y
, const unsigned short& pixelValue
) {
49 pzs
+= double(pixelValue
);
52 void merge(const BlobProperty
& other
) {
56 numPixels
+= other
.numPixels
;
58 size_t getNumPixels(void) const {
61 Geometry::Point
<double, 3> calcCentroid(void)
62 const { // Returns the centroid of the blob in depth image space
63 return Geometry::Point
<double, 3>(pxs
/ double(numPixels
), pys
/ double(numPixels
),
64 pzs
/ double(numPixels
));
69 class BlobProperty
<float> { // Class to calculate the 3D centroid of a blob in depth image space
72 double pxs
, pys
, pzs
; // Accumulated components of centroid
73 size_t numPixels
; // Number of accumulated pixels
75 /* Constructors and destructors: */
78 : pxs(0.0), pys(0.0), pzs(0.0),
83 void addPixel(unsigned int x
, unsigned int y
, const float& pixelValue
) {
86 pzs
+= double(pixelValue
);
89 void merge(const BlobProperty
& other
) {
93 numPixels
+= other
.numPixels
;
95 size_t getNumPixels(void) const {
98 Geometry::Point
<double, 3> calcCentroid(void)
99 const { // Returns the centroid of the blob in depth image space
100 return Geometry::Point
<double, 3>(pxs
/ double(numPixels
), pys
/ double(numPixels
),
101 pzs
/ double(numPixels
));
105 class ValidPixelProperty
{ // Functor class to identify valid pixels in raw depth frames
108 float minPlane
[4]; // Plane equation of the lower bound of valid depth values in depth image space
109 float maxPlane
[4]; // Plane equation of the upper bound of valid depth values in depth image space
110 Geometry::Matrix
<float, 3, 4>
111 colorDepthHomography
; // Homography from 3D depth image space into 2D color image space
112 unsigned int colorSize
[2]; // Width and height of color frames
113 const unsigned char* colorFrame
; // The current color frame
115 /* Constructors and destructors: */
117 ValidPixelProperty(const float sMinPlane
[4], const float sMaxPlane
[4],
118 const Geometry::Matrix
<float, 3, 4>& sColorDepthHomography
, const unsigned int sColorSize
[2])
119 : colorDepthHomography(sColorDepthHomography
),
121 /* Copy the min and max plane equations: */
122 for(int i
= 0; i
< 4; ++i
)
123 minPlane
[i
] = sMinPlane
[i
];
124 for(int i
= 0; i
< 4; ++i
)
125 maxPlane
[i
] = sMaxPlane
[i
];
127 /* Copy the color image size: */
128 for(int i
= 0; i
< 2; ++i
)
129 colorSize
[i
] = sColorSize
[i
];
134 void setColorFrame(const unsigned char*
135 newColorFrame
) { // Sets the color frame for the next blob extraction
136 colorFrame
= newColorFrame
;
138 bool operator()(unsigned int x
, unsigned int y
, const unsigned short& pixel
) const {
139 return operator()(x
, y
, float(pixel
));
141 bool operator()(unsigned int x
, unsigned int y
, const float& pixel
) const {
142 /* Plug the pixel into the plane equations to determine its validity: */
143 float px
= float(x
) + 0.5f
;
144 float py
= float(y
) + 0.5f
;
146 float minD
= minPlane
[0] * px
+ minPlane
[1] * py
+ minPlane
[2] * pz
+ minPlane
[3];
147 float maxD
= maxPlane
[0] * px
+ maxPlane
[1] * py
+ maxPlane
[2] * pz
+ maxPlane
[3];
148 if(minD
< 0.0f
|| maxD
> 0.0f
)
153 /* Project the pixel into the color frame: */
154 Geometry::ComponentArray
<float, 3> colorPos
= colorDepthHomography
*
155 Geometry::ComponentArray
<float, 4>(px
, py
, pz
, 1.0f
);
156 int cx
= int(Math::floor(colorPos
[0] / colorPos
[2]));
157 int cy
= int(Math::floor(colorPos
[1] / colorPos
[2]));
158 if(cx
< 0 || cx
>= colorSize
[0] || cy
< 0 || cy
>= colorSize
[1])
163 /* Check if the pixel is mostly black-ish: */
164 const unsigned char* rgb
= colorFrame
+ ((cy
* colorSize
[0] + cx
) * 3);
165 return rgb
[0] < 64U && rgb
[1] < 64U && rgb
[2] < 64U;
169 /* Normalize the pixel's color: */
170 const unsigned char* rgb
= colorFrame
+ ((cy
* colorSize
[0] + cx
) * 3);
171 unsigned char max
= rgb
[0];
172 for(int i
= 1; i
< 3; ++i
)
176 for(int i
= 0; i
< 3; ++i
)
177 rgb0
[i
] = float(rgb
[i
]) / float(max
);
179 /* Check if the color is red-ish: */
180 return rgb0
[0] >= 0.8f
&& rgb0
[1] < 0.25f
&& rgb0
[2] < 0.25f
;
192 /**************************
193 Methods of class RainMaker:
194 **************************/
196 template <class DepthPixelParam
>
198 void RainMaker::extractBlobs(const Kinect::FrameBuffer
& depthFrame
, const ValidPixelProperty
& vpp
,
199 RainMaker::BlobList
& blobsCc
) {
200 /* Extract raw blobs from the depth frame: */
201 std::vector
< ::Blob
<DepthPixelParam
> > blobsDic
= findBlobs(depthSize
,
202 depthFrame
.getData
<DepthPixelParam
>(), vpp
);
204 /* Transform all blobs larger than the threshold to camera space: */
205 blobsCc
.reserve(blobsDic
.size());
206 for(typename
std::vector
< ::Blob
<DepthPixelParam
> >::const_iterator bIt
= blobsDic
.begin();
207 bIt
!= blobsDic
.end(); ++bIt
)
208 if(bIt
->max
[0] - bIt
->min
[0] >= minBlobSize
&& bIt
->max
[1] - bIt
->min
[1] >= minBlobSize
) {
210 Point centroidDic
= bIt
->blobProperty
.calcCentroid();
211 blobCc
.centroid
= depthProjection
.transform(centroidDic
);
213 /* Estimate the radius of the blob in camera space (this is admittedly ad-hoc): */
214 double radiusDic
= double(bIt
->max
[0] - bIt
->min
[0]) * 0.5;
215 if(radiusDic
> (bIt
->max
[1] - bIt
->min
[1]) * 0.5) {
216 radiusDic
= (bIt
->max
[1] - bIt
->min
[1]) * 0.5;
217 blobCc
.radius
= Geometry::dist(depthProjection
.transform(Point(centroidDic
[0],
218 centroidDic
[1] + radiusDic
, centroidDic
[2])), blobCc
.centroid
);
220 blobCc
.radius
= Geometry::dist(depthProjection
.transform(Point(centroidDic
[0] + radiusDic
,
221 centroidDic
[1], centroidDic
[2])), blobCc
.centroid
);
223 /* Store the blob: */
224 blobsCc
.push_back(blobCc
);
228 void* RainMaker::detectionThreadMethod(void) {
229 unsigned int lastInputDepthFrameVersion
= 0;
230 unsigned int lastInputColorFrameVersion
= 0;
232 /* Create a pixel validity decider: */
233 ValidPixelProperty
vpp(minPlane
, maxPlane
, colorDepthHomography
, colorSize
);
236 Kinect::FrameBuffer depthFrame
, colorFrame
;
238 Threads::MutexCond::Lock
inputLock(inputCond
);
240 /* Wait until a new depth and color frame arrive, or the program shuts down: */
241 while(runDetectionThread
&& (lastInputDepthFrameVersion
== inputDepthFrameVersion
242 || lastInputColorFrameVersion
== inputColorFrameVersion
))
243 inputCond
.wait(inputLock
);
245 /* Bail out if the program is shutting down: */
246 if(!runDetectionThread
)
249 /* Work on the new frames: */
250 depthFrame
= inputDepthFrame
;
251 colorFrame
= inputColorFrame
;
252 lastInputDepthFrameVersion
= inputDepthFrameVersion
;
253 lastInputColorFrameVersion
= inputColorFrameVersion
;
256 if(outputBlobsFunction
!= 0) {
257 /* Set the most recent color frame in the pixel validator: */
258 vpp
.setColorFrame(colorFrame
.getData
<unsigned char>());
260 /* Detect all objects in the depth frame between the min and max planes: */
263 extractBlobs
<float>(depthFrame
, vpp
, blobsCc
);
265 extractBlobs
<unsigned short>(depthFrame
, vpp
, blobsCc
);
267 /* Call the callback function: */
268 (*outputBlobsFunction
)(blobsCc
);
275 RainMaker::RainMaker(const unsigned int sDepthSize
[2], const unsigned int sColorSize
[2],
276 const RainMaker::PTransform
& sDepthProjection
, const RainMaker::PTransform
& sColorProjection
,
277 const RainMaker::Plane
& basePlane
, double minElevation
, double maxElevation
, int sMinBlobSize
)
278 : depthIsFloat(false),
279 outputBlobsFunction(0) {
280 /* Remember the frame sizes: */
281 for(int i
= 0; i
< 2; ++i
)
282 depthSize
[i
] = sDepthSize
[i
];
283 for(int i
= 0; i
< 2; ++i
)
284 colorSize
[i
] = sColorSize
[i
];
286 /* Remember the depth and color projections: */
287 depthProjection
= sDepthProjection
;
288 colorProjection
= sColorProjection
;
290 /* Calculate the direct homography from depth image space to color image space: */
291 PTransform hom
= PTransform::scale(PTransform::Scale(double(colorSize
[0]), double(colorSize
[1]),
292 1.0)); // Go to color image space
293 hom
*= colorProjection
; // Go to color texture space
295 /* Remove the superfluous z component row: */
296 for(int i
= 0; i
< 2; ++i
)
297 for(int j
= 0; j
< 4; ++j
)
298 colorDepthHomography(i
, j
) = float(hom
.getMatrix()(i
, j
));
299 for(int j
= 0; j
< 4; ++j
)
300 colorDepthHomography(2, j
) = float(hom
.getMatrix()(3, j
));
302 /* Initialize the input frame slot: */
303 inputDepthFrameVersion
= 0;
304 inputColorFrameVersion
= 0;
306 /* Calculate the equations of the minimum and maximum elevation planes in camera space: */
307 PTransform::HVector
minPlaneCc(basePlane
.getNormal());
308 minPlaneCc
[3] = -(basePlane
.getOffset() + minElevation
* basePlane
.getNormal().mag());
309 PTransform::HVector
maxPlaneCc(basePlane
.getNormal());
310 maxPlaneCc
[3] = -(basePlane
.getOffset() + maxElevation
* basePlane
.getNormal().mag());
312 /* Transform the plane equations to depth image space and flip and swap the min and max planes because elevation increases opposite to raw depth: */
313 PTransform::HVector
minPlaneDic(depthProjection
.getMatrix().transposeMultiply(minPlaneCc
));
314 double minPlaneScale
= -1.0 / Geometry::mag(minPlaneDic
.toVector());
315 for(int i
= 0; i
< 4; ++i
)
316 maxPlane
[i
] = float(minPlaneDic
[i
] * minPlaneScale
);
317 PTransform::HVector
maxPlaneDic(depthProjection
.getMatrix().transposeMultiply(maxPlaneCc
));
318 double maxPlaneScale
= -1.0 / Geometry::mag(maxPlaneDic
.toVector());
319 for(int i
= 0; i
< 4; ++i
)
320 minPlane
[i
] = float(maxPlaneDic
[i
] * maxPlaneScale
);
322 /* Initialize the blob detector: */
323 minBlobSize
= sMinBlobSize
;
325 /* Start the object detection thread: */
326 runDetectionThread
= true;
327 detectionThread
.start(this, &RainMaker::detectionThreadMethod
);
330 RainMaker::~RainMaker(void) {
331 /* Shut down the object detection thread: */
333 Threads::MutexCond::Lock
inputLock(inputCond
);
334 runDetectionThread
= false;
337 detectionThread
.join();
339 /* Release all allocated resources: */
340 delete outputBlobsFunction
;
343 void RainMaker::setDepthIsFloat(bool newDepthIsFloat
) {
344 depthIsFloat
= newDepthIsFloat
;
347 void RainMaker::setOutputBlobsFunction(RainMaker::OutputBlobsFunction
* newOutputBlobsFunction
) {
348 delete outputBlobsFunction
;
349 outputBlobsFunction
= newOutputBlobsFunction
;
352 void RainMaker::receiveRawDepthFrame(const Kinect::FrameBuffer
& newDepthFrame
) {
353 Threads::MutexCond::Lock
inputLock(inputCond
);
355 /* Store the new buffer in the input buffer: */
356 inputDepthFrame
= newDepthFrame
;
357 ++inputDepthFrameVersion
;
359 /* Signal the background thread: */
363 void RainMaker::receiveRawColorFrame(const Kinect::FrameBuffer
& newColorFrame
) {
364 Threads::MutexCond::Lock
inputLock(inputCond
);
366 /* Store the new buffer in the input buffer: */
367 inputColorFrame
= newColorFrame
;
368 ++inputColorFrameVersion
;
370 /* Signal the background thread: */