Skip to content

Commit 30bf91b

Browse files
committed
Add an example client
1 parent 0819b66 commit 30bf91b

File tree

5 files changed

+137
-86
lines changed

5 files changed

+137
-86
lines changed

CMakeLists.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,24 @@ endforeach()
1717

1818
find_package(ament_cmake REQUIRED)
1919

20+
include_directories(include)
21+
2022
add_executable(openai_server src/openai_server.cpp)
2123
ament_target_dependencies(openai_server ${THIS_PACKAGE_INCLUDE_DEPENDS})
2224
target_link_libraries(openai_server b64 CURL::libcurl nlohmann_json
2325
${OpenCV_LIBS})
2426

27+
add_executable(example_client src/example_client.cpp)
28+
ament_target_dependencies(example_client ${THIS_PACKAGE_INCLUDE_DEPENDS})
29+
target_link_libraries(example_client ${OpenCV_LIBS})
30+
2531
install(
26-
TARGETS openai_server
32+
TARGETS example_client openai_server
2733
ARCHIVE DESTINATION lib
2834
LIBRARY DESTINATION lib
2935
RUNTIME DESTINATION lib/ros2_openai_server)
3036

37+
install(DIRECTORY include DESTINATION include/ros2_openai_server)
3138
install(
3239
DIRECTORY test_data
3340
DESTINATION DESTINATION

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ Here's an example that returns a full string.
2222

2323
`ros2 service call /openai_string_response ai_msgs/srv/StringResponse prompt:\ "are you a pirate?"`
2424

25+
There's an example client which sends an image of a wooden table and prompts whether it is indeed a wooden table:
26+
27+
`ros2 run ros2_openai_server example_client`
28+
2529
## Citation
2630

2731
If you use this work, please cite it like so:
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#pragma once
2+
3+
#include <b64/encode.h>
4+
#include <cv_bridge/cv_bridge.h>
5+
#include <iostream>
6+
#include <opencv2/opencv.hpp>
7+
#include <optional>
8+
9+
namespace openai_server
10+
{
11+
/**
12+
* @brief Convert jpeg to base64 encoding. There are 2 possible input types.
13+
*
14+
* @param image_path path to image on disk
15+
* @param image_cv_mat an image
16+
* @return image encoded as string. If there was an error, it will be empty.
17+
*/
18+
std::string convert_image_to_base_64(const std::optional<const std::string>& image_path,
19+
const std::optional<const cv::Mat>& image_cv_mat)
20+
{
21+
if (!image_path && !image_cv_mat)
22+
{
23+
std::cerr << "Both inputs to convert_image_to_base_64() are nullopt. One valid input is required." << std::endl;
24+
return "";
25+
}
26+
27+
cv::Mat image;
28+
if (image_path)
29+
{
30+
image = cv::imread(*image_path, cv::IMREAD_COLOR);
31+
if (image.empty())
32+
{
33+
std::cerr << "Could not read the image: " << *image_path << std::endl;
34+
return "";
35+
}
36+
}
37+
else
38+
{
39+
image = image_cv_mat.value();
40+
if (image.empty())
41+
{
42+
std::cerr << "Passed image was empty" << std::endl;
43+
return "";
44+
}
45+
}
46+
47+
// Encode image to JPEG format
48+
std::vector<uchar> buf;
49+
cv::imencode(".jpg", image, buf);
50+
51+
// Convert the buffer to a char array
52+
std::string encoded_data(buf.begin(), buf.end());
53+
54+
// Encode to Base64 using libb64
55+
base64::encoder E;
56+
std::ostringstream os;
57+
std::istringstream is(encoded_data);
58+
E.encode(is, os);
59+
60+
return os.str();
61+
}
62+
} // namespace openai_server

src/example_client.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#include "ai_msgs/srv/bool_response.hpp"
2+
#include "rclcpp/rclcpp.hpp"
3+
4+
#include <ament_index_cpp/get_package_share_directory.hpp>
5+
#include <chrono>
6+
#include <cstdlib>
7+
#include <cv_bridge/cv_bridge.h>
8+
#include <memory>
9+
#include <opencv2/opencv.hpp>
10+
11+
using namespace std::chrono_literals;
12+
13+
int main(int argc, char** argv)
14+
{
15+
rclcpp::init(argc, argv);
16+
17+
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("open_ai_client");
18+
rclcpp::Client<ai_msgs::srv::BoolResponse>::SharedPtr client =
19+
node->create_client<ai_msgs::srv::BoolResponse>("openai_bool_response");
20+
21+
auto request = std::make_shared<ai_msgs::srv::BoolResponse::Request>();
22+
request->prompt = "Is this a wooden table? Please respond in one word, yes or no.";
23+
24+
// Add an image of a wooden table to the OpenAI request
25+
std::string pkg_share_directory = ament_index_cpp::get_package_share_directory("ros2_openai_server");
26+
std::string image_path = pkg_share_directory + "/test_data/wood_table.jpg";
27+
cv::Mat img = cv::imread(image_path, cv::IMREAD_COLOR);
28+
sensor_msgs::msg::Image::SharedPtr msg = cv_bridge::CvImage(std_msgs::msg::Header(), "bgr8", img).toImageMsg();
29+
request->image = *msg;
30+
31+
while (!client->wait_for_service(5s))
32+
{
33+
if (!rclcpp::ok())
34+
{
35+
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
36+
return 0;
37+
}
38+
}
39+
40+
auto result = client->async_send_request(request);
41+
// Wait for the result.
42+
if (rclcpp::spin_until_future_complete(node, result) == rclcpp::FutureReturnCode::SUCCESS)
43+
{
44+
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Response: %b", result.get()->response);
45+
}
46+
else
47+
{
48+
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service");
49+
}
50+
51+
return 0;
52+
}

src/openai_server.cpp

Lines changed: 11 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,18 @@
1+
#include "ros2_openai_server/convert_image_to_base_64.hpp"
2+
13
#include "ai_msgs/srv/bool_response.hpp"
24
#include "ai_msgs/srv/string_response.hpp"
35
#include "rclcpp/rclcpp.hpp"
46
#include "sensor_msgs/msg/image.hpp"
57

6-
#include <b64/encode.h>
8+
#include <ament_index_cpp/get_package_share_directory.hpp>
79
#include <curl/curl.h>
8-
#include <cv_bridge/cv_bridge.h>
910
#include <nlohmann/json.hpp>
10-
#include <opencv2/opencv.hpp>
1111
#include <sensor_msgs/image_encodings.hpp>
1212
#include <stdlib.h>
1313

14-
// Uncomment this to use the test image
15-
#define HARD_CODED_IMAGE_TESTING
16-
1714
namespace openai_server
1815
{
19-
20-
namespace
21-
{
22-
/**
23-
* @brief Convert jpeg to base64 encoding. There are 2 possible input types.
24-
*
25-
* @param image_path path to image on disk
26-
* @param image_cv_mat an image
27-
* @return image encoded as string. If there was an error, it will be empty.
28-
*/
29-
std::string convert_image_to_base_64(const std::optional<const std::string>& image_path,
30-
const std::optional<const cv::Mat>& image_cv_mat)
31-
{
32-
if (!image_path && !image_cv_mat)
33-
{
34-
std::cerr << "Both inputs to convert_image_to_base_64() are nullopt. One valid input is required." << std::endl;
35-
return "";
36-
}
37-
38-
cv::Mat image;
39-
if (image_path)
40-
{
41-
image = cv::imread(*image_path, cv::IMREAD_COLOR);
42-
if (image.empty())
43-
{
44-
std::cerr << "Could not read the image: " << *image_path << std::endl;
45-
return "";
46-
}
47-
}
48-
else
49-
{
50-
image = image_cv_mat.value();
51-
if (image.empty())
52-
{
53-
std::cerr << "Passed image was empty" << std::endl;
54-
return "";
55-
}
56-
}
57-
58-
// Encode image to JPEG format
59-
std::vector<uchar> buf;
60-
cv::imencode(".jpg", image, buf);
61-
62-
// Convert the buffer to a char array
63-
std::string encoded_data(buf.begin(), buf.end());
64-
65-
// Encode to Base64 using libb64
66-
base64::encoder E;
67-
std::ostringstream os;
68-
std::istringstream is(encoded_data);
69-
E.encode(is, os);
70-
71-
return os.str();
72-
}
73-
} // namespace
74-
7516
class OpenAIServer : public rclcpp::Node
7617
{
7718
public:
@@ -141,38 +82,23 @@ class OpenAIServer : public rclcpp::Node
14182
request_data["messages"][0]["role"] = "user";
14283
// Attach the image, if any
14384
// See https://platform.openai.com/docs/guides/vision
144-
if (
145-
#ifdef HARD_CODED_IMAGE_TESTING
146-
1
147-
#endif
148-
#ifndef HARD_CODED_IMAGE_TESTING
149-
0
150-
#endif
151-
)
152-
{
153-
std::string encoded_image = convert_image_to_base_64(
154-
std::optional<const std::string>(
155-
"/home/andy/ws_nav2/install/ros2_openai_server/share/ros2_openai_server/test_data/wood_table.jpg"),
156-
std::nullopt);
157-
request_data["messages"][0]["content"][0]["type"] = "text";
158-
request_data["messages"][0]["content"][0]["text"] = prompt;
159-
request_data["messages"][0]["content"][1]["type"] = "image_url";
160-
request_data["messages"][0]["content"][1]["image_url"]["url"] = ("data:image/jpg;base64," + encoded_image);
161-
}
162-
else if (image.height > 0)
85+
if (image.height > 0)
16386
{
164-
cv_bridge::CvImagePtr cv_ptr;
87+
std::string encoded_image;
16588
try
16689
{
167-
cv_ptr = cv_bridge::toCvCopy(image, image.encoding);
168-
std::string encoded_image =
169-
convert_image_to_base_64(std::nullopt, std::optional<const cv::Mat>(cv_ptr->image));
90+
cv_bridge::CvImagePtr cv_ptr = cv_bridge::toCvCopy(image, image.encoding);
91+
encoded_image = convert_image_to_base_64(std::nullopt, std::optional<const cv::Mat>(cv_ptr->image));
17092
}
17193
catch (cv_bridge::Exception& e)
17294
{
17395
RCLCPP_ERROR(this->get_logger(), "cv_bridge exception: %s", e.what());
17496
return false;
17597
}
98+
request_data["messages"][0]["content"][0]["type"] = "text";
99+
request_data["messages"][0]["content"][0]["text"] = prompt;
100+
request_data["messages"][0]["content"][1]["type"] = "image_url";
101+
request_data["messages"][0]["content"][1]["image_url"]["url"] = ("data:image/jpg;base64," + encoded_image);
176102
}
177103
else // No image
178104
{

0 commit comments

Comments
 (0)