Skip to content

Commit 7017862

Browse files
authored
Merge pull request #1461 from luxonis/feature/point_cloud_data_bindings_update
Feature/point cloud data bindings update
2 parents 5db9c98 + d4a6055 commit 7017862

File tree

7 files changed

+212
-0
lines changed

7 files changed

+212
-0
lines changed

bindings/python/src/pipeline/datatype/PointCloudDataBindings.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,39 @@ void bind_pointclouddata(pybind11::module& m, void* pCallstack) {
117117
.def("getTimestamp", &PointCloudData::Buffer::getTimestamp, DOC(dai, Buffer, getTimestamp))
118118
.def("getTimestampDevice", &PointCloudData::Buffer::getTimestampDevice, DOC(dai, Buffer, getTimestampDevice))
119119
.def("getSequenceNum", &PointCloudData::Buffer::getSequenceNum, DOC(dai, Buffer, getSequenceNum))
120+
.def("setPoints", [](py::object& obj, py::array_t<float>& arr) {
121+
if (arr.ndim() != 2 || arr.shape(1) != 3) {
122+
throw std::runtime_error("Input must be a 2D numpy array of points with the shape of (N, 3)");
123+
}
124+
dai::PointCloudData& data = obj.cast<dai::PointCloudData&>();
125+
std::vector<Point3f> points;
126+
points.reserve(arr.shape(0));
127+
auto ra = arr.unchecked();
128+
for(int i = 0; i < arr.shape(0); i++) {
129+
points.emplace_back(ra(i, 0), ra(i, 1), ra(i, 2));
130+
}
131+
data.setPoints(points);
132+
})
133+
.def("setPointsRGB", [](py::object& obj, py::array_t<float>& points, py::array_t<uint8_t>& colors) {
134+
if (points.ndim() != 2 || points.shape(1) != 3) {
135+
throw std::runtime_error("Points input must be a 2D numpy array of points with the shape of (N, 3)");
136+
}
137+
if (colors.ndim() != 2 || colors.shape(1) != 4) {
138+
throw std::runtime_error("Colors input must be a 2D numpy array of colors with the shape of (N, 4)");
139+
}
140+
if (points.shape(0) != colors.shape(0)) {
141+
throw std::runtime_error("Points and Colors must have the same number of rows");
142+
}
143+
dai::PointCloudData& data = obj.cast<dai::PointCloudData&>();
144+
std::vector<Point3fRGBA> pointsRGBA;
145+
pointsRGBA.reserve(points.shape(0));
146+
auto ra1 = points.unchecked();
147+
auto ra2 = colors.unchecked();
148+
for(int i = 0; i < points.shape(0); i++) {
149+
pointsRGBA.emplace_back(ra1(i, 0), ra1(i, 1), ra1(i, 2), ra2(i, 0), ra2(i, 1), ra2(i, 2), ra2(i, 3));
150+
}
151+
data.setPointsRGB(pointsRGBA);
152+
})
120153
.def("setWidth", &PointCloudData::setWidth, DOC(dai, PointCloudData, setWidth))
121154
.def("setHeight", &PointCloudData::setHeight, DOC(dai, PointCloudData, setHeight))
122155
.def("setSize",

bindings/python/tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ set(PYBIND11_TEST_FILES
1313
"utf8_support_test.py"
1414
# "dai_path_conversion_test.py" requires device
1515
"nndata_tensor_test.py"
16+
"pcldata_set_points_test.py"
1617
"messsage_queue_test.py"
1718
"imgframe_test.py"
1819
"inherited_messages_test.py"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import depthai as dai
2+
import numpy as np
3+
4+
def test_pcldata_set_points():
5+
pointCloudData = dai.PointCloudData()
6+
7+
# Points
8+
points = np.random.rand(1000, 3).astype(np.float32)
9+
pointCloudData.setPoints(points)
10+
11+
assert(pointCloudData.isColor() == False)
12+
assert(isinstance(pointCloudData.getPoints(), np.ndarray))
13+
assert(pointCloudData.getPoints().dtype == points.dtype)
14+
assert(np.array_equal(pointCloudData.getPoints(), points))
15+
16+
# RGB Points
17+
colors = np.random.rand(1000, 4).astype(np.uint8)
18+
pointCloudData.setPointsRGB(points, colors)
19+
20+
assert(pointCloudData.isColor() == True)
21+
assert(isinstance(pointCloudData.getPoints(), np.ndarray))
22+
assert(isinstance(pointCloudData.getPointsRGB()[0], np.ndarray))
23+
assert(isinstance(pointCloudData.getPointsRGB()[1], np.ndarray))
24+
assert(pointCloudData.getPoints().dtype == points.dtype)
25+
assert(pointCloudData.getPointsRGB()[0].dtype == points.dtype)
26+
assert(pointCloudData.getPointsRGB()[1].dtype == colors.dtype)
27+
assert(np.array_equal(pointCloudData.getPoints(), points))
28+
assert(np.array_equal(pointCloudData.getPointsRGB()[0], points))
29+
assert(np.array_equal(pointCloudData.getPointsRGB()[1], colors))
30+
31+
32+
if __name__ == '__main__':
33+
test_pcldata_set_points()

examples/cpp/RGBD/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,8 @@ endif()
1717

1818
if(DEPTHAI_ENABLE_REMOTE_CONNECTION)
1919
dai_add_example(visualizer_rgbd_autocreate visualizer_rgbd_autocreate.cpp ON OFF)
20+
endif()
21+
22+
if(DEPTHAI_ENABLE_REMOTE_CONNECTION)
23+
dai_add_example(rgbd_pcl_processing rgbd_pcl_processing.cpp ON OFF)
2024
endif()
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
2+
#include <csignal>
3+
#include <depthai/remote_connection/RemoteConnection.hpp>
4+
#include <iostream>
5+
6+
#include "depthai/depthai.hpp"
7+
8+
9+
class CustomPCLProcessingNode : public dai::NodeCRTP<dai::node::ThreadedHostNode, CustomPCLProcessingNode> {
10+
public:
11+
constexpr static const char* NAME = "CustomPCLProcessingNode";
12+
constexpr static const float thresholdDistance = 3000.0f;
13+
14+
public:
15+
Input inputPCL{*this, {.name = "inPCL", .types = {{dai::DatatypeEnum::PointCloudData, true}}}};
16+
Output outputPCL{*this, {.name = "outPCL", .types = {{dai::DatatypeEnum::PointCloudData, true}}}};
17+
18+
void run() override {
19+
while(isRunning()) {
20+
auto pclIn = inputPCL.get<dai::PointCloudData>();
21+
auto pclOut = std::make_shared<dai::PointCloudData>();
22+
if(pclIn != nullptr) {
23+
const auto& pclData = pclIn->getPointsRGB();
24+
std::vector<dai::Point3fRGBA> updatedPoints;
25+
const float thresholdSquared = thresholdDistance * thresholdDistance;
26+
updatedPoints.reserve(pclData.size());
27+
for (const auto& point : pclData) {
28+
const float distance = point.x*point.x + point.y*point.y + point.z*point.z;
29+
if (distance <= thresholdSquared) {
30+
updatedPoints.emplace_back(point.x, point.y, point.z,
31+
point.r, point.g, point.b, point.a);
32+
}
33+
}
34+
updatedPoints.shrink_to_fit();
35+
pclOut->setPointsRGB(updatedPoints);
36+
outputPCL.send(pclOut);
37+
}
38+
}
39+
}
40+
};
41+
42+
// Signal handling for clean shutdown
43+
static bool isRunning = true;
44+
void signalHandler(int signum) {
45+
isRunning = false;
46+
}
47+
48+
int main() {
49+
using namespace std;
50+
// Default port values
51+
int webSocketPort = 8765;
52+
int httpPort = 8080;
53+
54+
// Register signal handler
55+
std::signal(SIGINT, signalHandler);
56+
57+
// Create RemoteConnection
58+
dai::RemoteConnection remoteConnector(dai::RemoteConnection::DEFAULT_ADDRESS, webSocketPort, true, httpPort);
59+
// Create pipeline
60+
dai::Pipeline pipeline;
61+
auto rgbd = pipeline.create<dai::node::RGBD>()->build(true, dai::node::StereoDepth::PresetMode::DEFAULT);
62+
auto customNode = pipeline.create<CustomPCLProcessingNode>();
63+
64+
rgbd->pcl.link(customNode->inputPCL);
65+
66+
remoteConnector.addTopic("pcl", rgbd->pcl);
67+
remoteConnector.addTopic("processed_pcl", customNode->outputPCL);
68+
pipeline.start();
69+
remoteConnector.registerPipeline(pipeline);
70+
auto device = pipeline.getDefaultDevice();
71+
device->setIrLaserDotProjectorIntensity(0.7);
72+
// Main loop
73+
while(isRunning && pipeline.isRunning()) {
74+
int key = remoteConnector.waitKey(1);
75+
if(key == 'q') {
76+
std::cout << "Got 'q' key from the remote connection!" << std::endl;
77+
break;
78+
}
79+
}
80+
81+
std::cout << "Pipeline stopped." << std::endl;
82+
return 0;
83+
}

examples/python/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ dai_set_example_test_labels(visualizer_rgbd_autocreate ondevice rvc2 usb rvc4 ci
239239
add_python_example(rgbd_o3d RGBD/rgbd_o3d.py)
240240
dai_set_example_test_labels(rgbd_o3d ondevice rvc2_all rvc4 ci nowindows)
241241

242+
add_python_example(rgbd_pcl_processing RGBD/rgbd_pcl_processing.py)
243+
dai_set_example_test_labels(rgbd_pcl_processing ondevice rvc2_all rvc4 ci nowindows)
244+
242245
add_python_example(person_crop_out Misc/DigitalZoom/person_crop_out.py)
243246
dai_set_example_test_labels(person_crop_out ondevice rvc2_all rvc4 rvc4rgb ci)
244247

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import depthai as dai
2+
import numpy as np
3+
from argparse import ArgumentParser
4+
5+
6+
class CustomPCLProcessingNode(dai.node.ThreadedHostNode):
7+
def __init__(self):
8+
dai.node.ThreadedHostNode.__init__(self)
9+
self.inputPCL = self.createInput()
10+
self.outputPCL = self.createOutput()
11+
self.thresholdDistance = 3000.0
12+
13+
def run(self):
14+
while self.isRunning():
15+
try:
16+
inPointCloud = self.inputPCL.get()
17+
except dai.MessageQueue.QueueException:
18+
return # Pipeline closed
19+
if inPointCloud is not None:
20+
outPointCloud = dai.PointCloudData()
21+
points, colors = inPointCloud.getPointsRGB()
22+
23+
mask = np.linalg.norm(points, axis=1) < self.thresholdDistance
24+
updatedPoints = points[mask]
25+
updatedColors = colors[mask]
26+
27+
outPointCloud.setPointsRGB(updatedPoints, updatedColors)
28+
self.outputPCL.send(outPointCloud)
29+
30+
parser = ArgumentParser()
31+
parser.add_argument("--webSocketPort", type=int, default=8765)
32+
parser.add_argument("--httpPort", type=int, default=8080)
33+
args = parser.parse_args()
34+
35+
with dai.Pipeline() as p:
36+
remoteConnector = dai.RemoteConnection(
37+
webSocketPort=args.webSocketPort, httpPort=args.httpPort
38+
)
39+
rgbd = p.create(dai.node.RGBD).build(True, dai.node.StereoDepth.PresetMode.DEFAULT)
40+
customNode = p.create(CustomPCLProcessingNode)
41+
42+
# Link rgbd.pcl to the input of CustomPCLProcessingNode
43+
rgbd.pcl.link(customNode.inputPCL)
44+
45+
remoteConnector.addTopic("pcl", rgbd.pcl, "common")
46+
remoteConnector.addTopic("processed_pcl", customNode.outputPCL, "common")
47+
48+
p.start()
49+
remoteConnector.registerPipeline(p)
50+
51+
while p.isRunning():
52+
key = remoteConnector.waitKey(1)
53+
if key == ord("q"):
54+
print("Got q key from the remote connection!")
55+
break

0 commit comments

Comments
 (0)