-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ROS output for the computer vision #10
Conversation
… values to configuration.h
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comments, otherwise looks good to me
@@ -5,6 +5,8 @@ | |||
#include "cv_bridge/cv_bridge.h" | |||
#include "led_state_parser.h" | |||
#include "keypoint_detector.h" | |||
#include <vector> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check include orders as defined per google stylesheet: https://google.github.io/styleguide/cppguide.html#Names_and_Order_of_Includes
time_stamper_cv/msg/Ledstate.msg
Outdated
bool is_valid | ||
int32 counter | ||
int32[] binary_state | ||
uint8[] intensity |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as intensity is an averaged value of many uint8's (has more resolution than just a single uint8), you could also use a float/double representation here.
|
||
for (int i = 0; i < led_parser_->getLedRow(ImageProcessorConfig::TOP_ROW).size(); i++) { | ||
double brightness = led_parser_->getLedBrightness(ImageProcessorConfig::TOP_ROW, i); | ||
msg.intensity.push_back((uint8_t) std::lround(brightness)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see comment in message defnition
img_pub_ = nh_.advertise<sensor_msgs::Image>("timestamper/image", 1); | ||
led_state_pub_ = nh_.advertise<time_stamper_cv::Ledstate>("timestamper/led_state", 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of namespacing the topic name, its probably better to just name them image and led_state and let the namespacing be done via roslaunch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Image" is ambiguous with topics from image_undistort, so I leave it like this
time_stamper_cv/msg/Ledstate.msg
Outdated
Header header | ||
bool is_valid | ||
int32 counter | ||
int32[] binary_state |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the binary state an int32 (and not a bool
)? What values can it have?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some ideas to improve code quality. On a functional level, I'm very happy with this. Thanks a lot!
for (const auto &led: led_parser_->getLedRow(ImageProcessorConfig::BOTTOM_ROW)) { | ||
cv::Point2f led_pos = LedStateParser::normalize(led); | ||
cv::circle(visualization_mat, led_pos, (int) 10, cv::Scalar(255, 0, 0)); | ||
} | ||
|
||
for (const auto &led: led_parser_->getLedRow("TopRow")) { | ||
for (const auto &led: led_parser_->getLedRow(ImageProcessorConfig::TOP_ROW)) { | ||
cv::Point2f led_pos = LedStateParser::normalize(led); | ||
cv::circle(visualization_mat, led_pos, (int) 10, cv::Scalar(255, 0, 0)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd recommend avoiding such code duplication. What about having a for-each loop construct over the two rows?
E.g.
for(auto row: {ImageProcessorConfig::BOTTOM_ROW, ImageProcessorConfig::TOP_ROW}) {
for(const auto &led: led_parser_->getLedRow(row)) {
}
}
Actually, even better would be to have some general support for iterating over all rows, because that is what you really want here, if I'm not mistaken. You don't really want this part of the code to depend on the fact that only there are two rows -- because it doesn't matter for it.
for (int i = 0; i < led_parser_->getLedRow(ImageProcessorConfig::TOP_ROW).size(); i++) { | ||
double brightness = led_parser_->getLedBrightness(ImageProcessorConfig::TOP_ROW, i); | ||
msg.intensity.push_back((uint8_t) std::lround(brightness)); | ||
msg.binary_state.push_back(led_parser_->isLedOn(ImageProcessorConfig::TOP_ROW, i)); | ||
} | ||
|
||
for (int i = 0; i < led_parser_->getLedRow(ImageProcessorConfig::BOTTOM_ROW).size(); i++) { | ||
double brightness = led_parser_->getLedBrightness(ImageProcessorConfig::BOTTOM_ROW, i); | ||
msg.binary_state.push_back(led_parser_->isLedOn(ImageProcessorConfig::BOTTOM_ROW, i)); | ||
msg.intensity.push_back((uint8_t) std::lround(brightness)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same duplication here, I believe.
* Message to be published | ||
* @return Led state with set values | ||
*/ | ||
time_stamper_cv::Ledstate getLedStateMessage(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I smell a problematic design here since this function doesn't have any input. I tried to understand why and found that the processImage
function of the LedStateParser changes its internal state. That isn't a good idea IMO. I don't have the space to explain it here but I'd highly recommend having a dedicated class representing the outcome of the an processImage
and that processImage
is a const
member of the LedStateParser
.
It is very uncommon for a parser to represent the parse outcome as internal state and there are many good reasons for that. I'm happy to explain one day in person. I think in very short, this is about separating data (led state, images) from actors/entities (e.g. a parser).
This function here, could then be a member of that outcome class iff its members would hold all relevant information. Otherwise consider passing such a parse outcome to it as an argument instead.
I mostly found a potential problem with the convex shape. I'm not sure what is_valid
actually does, but is it actually interesting to receive led parser results when the convex shape is invalid? It sounds to me that parsing doesn't really make sense when the shape is invalid. Or does it?
The process
function of the ImageProcessor could simply return both, the let-state and the Image message it currently returns (e.g. using a std::tuple or - probably better - a small struct holding both with proper names). More symmetric would probably be if the process
simply returns both messages: the output image and the led state message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd highly recommend having a dedicated class representing the outcome of the an processImage and that processImage is a const member of the LedStateParser
I`ll look into this next week. I've opened an new issue regarding this problem. (#11)
I mostly found a potential problem with the convex shape. I'm not sure what is_valid actually does, but is it actually interesting to receive led parser results when the convex shape is invalid? It sounds to me that parsing doesn't really make sense when the shape is invalid. Or does it?
This is a design choice, because there are some nodes, which require a message at a given rate regardless of being valid or not. is_valid
is a flag to indicate validity.
* Message to be published | ||
* @return Led state with set values | ||
*/ | ||
time_stamper_cv::Ledstate getLedStateMessage(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd reserve get
for getters. This function isn't a getter at all because it doesn't just get something that is already there. What about build
?
time_stamper_cv::Ledstate led_msg = image_processor_->getLedStateMessage(); | ||
led_msg.header = image.header; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find it problematic that getLedStateMessage
kind of claims to produce a complete message but the caller still has to patch in the head. What about going the other way around: the caller of getLedStateMessage
creates the message and hands passes it on in a mutable way to it (e.g non-const ref). That would of course require a renaming of the function, to e.g. fillInLedState
. I really don't like "output" parameters but returning something incomplete I find even worse :D. In an ideal world the message would be comprised of bigger junks, one of which this message would return. But that isn't really easy to achieve here, I believe.
That suggested pattern of a fillIn..
function would also solve the problem of the is_valid
: it could be set by the caller (i.e. here or in the process
function in case you move this into it).
Thanks for the great feedback! I addressed all requested changes (except the design of the LedStateParser) - please take a second look |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me! - I think this can be merged, and then you can concentrate on the refactor.
About refactoring: Its a good time pre-refactoring to think about a comprehensive set of unit tests, so that during refactoring you can always verify that functionality stays the same.
(I'll also add a comment in the other PR)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New changes look like a good improvement. Thanks for applying them. Some feedback that may help. More for the learning than for the code itself... Sorry, I can't help it :).
std::vector<std::string> res{}; | ||
|
||
std::map<std::string, LedRowConfig>::const_iterator it; | ||
for (it = led_row_config.begin() ; it != led_row_config.end(); it++) { | ||
res.push_back(it->first); | ||
} | ||
return res; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This you can greatly simplify, depending on your version of c++. I'd assume at least 11. Otherwise something is quite off here.
std::vector<std::string> res{}; | |
std::map<std::string, LedRowConfig>::const_iterator it; | |
for (it = led_row_config.begin() ; it != led_row_config.end(); it++) { | |
res.push_back(it->first); | |
} | |
return res; | |
std::vector<std::string> res{}; | |
for (const auto &item : led_row) { | |
res.push_back(item.first); | |
} | |
return ret; |
With c++17 you can destructure the key-value pair (item
) to make it nicer, for(const auto& [key, _] : led_rows) ...
.
With c++20 you could initialize from a view instead:
std::vector<std::string> res{}; | |
std::map<std::string, LedRowConfig>::const_iterator it; | |
for (it = led_row_config.begin() ; it != led_row_config.end(); it++) { | |
res.push_back(it->first); | |
} | |
return res; | |
const auto kv = std::views::keys(led_rows); | |
return { kv.begin(), kv.end() }; |
c++23 will probably further simplify :) (.to
). At least they are getting there :D.
Generally this pattern has a quite sub-optimal performance because it creates the same vector on each call. Not a sign. problem here, but you should know.
To address that I'd use a static variable and IIFE to initialize it. Here a c++20 compatible example but you can do even with c++11.
std::vector<std::string> res{}; | |
std::map<std::string, LedRowConfig>::const_iterator it; | |
for (it = led_row_config.begin() ; it != led_row_config.end(); it++) { | |
res.push_back(it->first); | |
} | |
return res; | |
const static auto row_names = []() { | |
const auto kv = std::views::keys(led_rows); | |
return std::vector<std::string>{ kv.begin(), kv.end() }; | |
}(); | |
return row_names; |
(IMPORTANT: this makes only sense if you change the return type to a const ref at the same time)
Another performance sub-optimality is the needless copy of a std::string. A vector of string_view
s could make that better since c++17.
But no need to do that -- optimization is important to have a good basic feeling for but should usually be done with care -- if it make the code more complicated because that is usually the more problematic cost foremost because it leads to bugs!
std::vector<std::string> rows = cfg_.rows(); | ||
std::for_each(rows.begin(), rows.end(), [this, &msg] (const std::string& name) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can still use a for-: loop saving you the rows
variable:
for(const auto& name: cfg_.rows()) {
...
}
|
||
std::vector<std::string> rows = cfg_.rows(); | ||
std::for_each(rows.begin(), rows.end(), [this, &msg] (const std::string& name) { | ||
for (int i = 0; i < led_parser_->getLedRow(name).size(); i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This lookup led_parser_->getLedRow(name)
indicates to me that actually, you should directly iterate over led_rows
map's items (ideally with c++17 structured binding) to avoid the ugly .first
and .second
..
… ImageProcessor::fillInLedStateMessage
I didn't know structured bindings existed, so thanks for pointing that out! I added the suggested changes above. |
Changes
Ledstate.msg