diff --git a/openpilot/tools/cabana/core/settings.h b/openpilot/tools/cabana/core/settings.h index 1b9e2bcfc4..6032ee5665 100644 --- a/openpilot/tools/cabana/core/settings.h +++ b/openpilot/tools/cabana/core/settings.h @@ -21,6 +21,7 @@ struct CabanaSettingsState { bool multiple_lines_hex = false; bool log_livestream = true; bool suppress_defined_signals = false; + bool crop_video = true; std::string log_path; std::string last_dir; std::string last_route_dir; diff --git a/openpilot/tools/cabana/settings.cc b/openpilot/tools/cabana/settings.cc index 17a6611539..7f8e6473c2 100644 --- a/openpilot/tools/cabana/settings.cc +++ b/openpilot/tools/cabana/settings.cc @@ -488,6 +488,7 @@ void settingsOp(Store &s, SettingOperation op) { op(s, "log_path", settings.log_path); op(s, "drag_direction", (int &)settings.drag_direction); op(s, "suppress_defined_signals", settings.suppress_defined_signals); + op(s, "crop_video", settings.crop_video); op(s, "recent_dbc_file", settings.recent_dbc_file); op(s, "active_msg_id", settings.active_msg_id); op(s, "selected_msg_ids", settings.selected_msg_ids); diff --git a/openpilot/tools/cabana/ui/icons.h b/openpilot/tools/cabana/ui/icons.h index 5c97666da4..7a61d02c09 100644 --- a/openpilot/tools/cabana/ui/icons.h +++ b/openpilot/tools/cabana/ui/icons.h @@ -8,6 +8,8 @@ constexpr const char ARROW_DOWN_LEFT_SQUARE[] = "\xef\x84\x9c"; constexpr const char ARROW_UP_RIGHT_SQUARE[] = "\xef\x85\x82"; constexpr const char CHEVRON_LEFT[] = "\xef\x8a\x84"; constexpr const char CHEVRON_RIGHT[] = "\xef\x8a\x85"; +constexpr const char ASPECT_RATIO[] = "\xef\x85\x90"; +constexpr const char ASPECT_RATIO_FILL[] = "\xef\x85\x8f"; constexpr const char DASH[] = "\xef\x8b\xaa"; constexpr const char DASH_SQUARE[] = "\xef\x8b\xa8"; constexpr const char EXCLAMATION_TRIANGLE[] = "\xef\x8c\xba"; diff --git a/openpilot/tools/cabana/ui/mainwin.cc b/openpilot/tools/cabana/ui/mainwin.cc index 80a6157d54..a2969defcc 100644 --- a/openpilot/tools/cabana/ui/mainwin.cc +++ b/openpilot/tools/cabana/ui/mainwin.cc @@ -156,6 +156,7 @@ void MainWindow::drawMenuBar() { ImGui::Separator(); if (ImGui::MenuItem("Reset Window Layout")) { messages_visible_ = video_visible_ = true; + video_splitter_ratio_ = -1.0f; reset_layout_ = true; } ImGui::EndMenu(); diff --git a/openpilot/tools/cabana/ui/widgets/cameraview.cc b/openpilot/tools/cabana/ui/widgets/cameraview.cc index 78ca15116b..6ffd099fe9 100644 --- a/openpilot/tools/cabana/ui/widgets/cameraview.cc +++ b/openpilot/tools/cabana/ui/widgets/cameraview.cc @@ -9,6 +9,7 @@ #include "common/yuv.h" #include "tools/cabana/utils/util.h" +#include "tools/cabana/settings.h" namespace { constexpr GLenum GL_LINEAR_MIPMAP_LINEAR_ = 0x2703; @@ -107,21 +108,12 @@ void CameraWidget::paint() { frame_updated_ = false; } - // Scale for aspect ratio - float widget_ratio = (float)width() / height(); - float frame_ratio = (float)rgb_frame_.width / rgb_frame_.height; - int w = std::lround(width() * std::min(frame_ratio / widget_ratio, 1.0f)); - int h = std::lround(height() * std::min(widget_ratio / frame_ratio, 1.0f)); - ImVec2 video_min(rect_.Min.x + (int)(width() - w) / 2, rect_.Min.y + (int)(height() - h) / 2); - ImVec2 video_max(video_min.x + w, video_min.y + h); - - ImVec2 uv0(0, 0), uv1(1, 1); + VideoPlacement placement = videoPlacement(rect_, frameAspectRatio(), settings.crop_video); if (active_stream_type_ == VISION_STREAM_CABIN) { // mirror cabin camera horizontally - uv0.x = 1; - uv1.x = 0; + std::swap(placement.uv0.x, placement.uv1.x); } - p->AddImage(frame_texture_.ref(), video_min, video_max, uv0, uv1); + p->AddImage(frame_texture_.ref(), placement.min, placement.max, placement.uv0, placement.uv1); } void CameraWidget::vipcThread() { diff --git a/openpilot/tools/cabana/ui/widgets/cameraview.h b/openpilot/tools/cabana/ui/widgets/cameraview.h index 381e13dc50..2812aea86e 100644 --- a/openpilot/tools/cabana/ui/widgets/cameraview.h +++ b/openpilot/tools/cabana/ui/widgets/cameraview.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include #include #include @@ -16,6 +18,35 @@ #include "tools/cabana/core/observable.h" #include "msgq/visionipc/visionipc_client.h" +// Center-crop the source to fill the destination without stretching. +inline ImVec2 videoFillUv(const ImVec2 &size, float aspect_ratio) { + const float ratio = size.x / size.y / aspect_ratio; + return ImVec2(0.5f * (1.0f - std::min(ratio, 1.0f)), + 0.5f * (1.0f - std::min(1.0f / ratio, 1.0f))); +} + +struct VideoPlacement { + ImVec2 min; + ImVec2 max; + ImVec2 uv0; + ImVec2 uv1; +}; + +inline VideoPlacement videoPlacement(const ImRect &rect, float source_aspect_ratio, bool crop) { + VideoPlacement placement{rect.Min, rect.Max, ImVec2(0, 0), ImVec2(1, 1)}; + if (crop) { + placement.uv0 = videoFillUv(rect.GetSize(), source_aspect_ratio); + placement.uv1 = ImVec2(1.0f - placement.uv0.x, 1.0f - placement.uv0.y); + } else { + const float widget_aspect_ratio = rect.GetWidth() / rect.GetHeight(); + const int width = std::lround(rect.GetWidth() * std::min(source_aspect_ratio / widget_aspect_ratio, 1.0f)); + const int height = std::lround(rect.GetHeight() * std::min(widget_aspect_ratio / source_aspect_ratio, 1.0f)); + placement.min = ImVec2(rect.Min.x + (int)(rect.GetWidth() - width) / 2, rect.Min.y + (int)(rect.GetHeight() - height) / 2); + placement.max = ImVec2(placement.min.x + width, placement.min.y + height); + } + return placement; +} + // tightly packed RGBA pixels struct RgbImage { int width = 0; diff --git a/openpilot/tools/cabana/ui/widgets/videowidget.cc b/openpilot/tools/cabana/ui/widgets/videowidget.cc index 9a54351ab6..7cabac840a 100644 --- a/openpilot/tools/cabana/ui/widgets/videowidget.cc +++ b/openpilot/tools/cabana/ui/widgets/videowidget.cc @@ -197,10 +197,14 @@ void VideoWidget::drawPlaybackController() { item.in_menu = false; return item; }; + const char *aspect_ratio_icon = settings.crop_video ? icon::ASPECT_RATIO_FILL : icon::ASPECT_RATIO; + items.push_back({toolbarButtonWidth(aspect_ratio_icon), [&]() { + if (toolButton("crop_video", aspect_ratio_icon, "Crop to fill")) cropVideoClicked(); + }, "Crop to fill", [this]() { cropVideoClicked(); }}); if (!can->liveStreaming()) { + items.push_back(separator()); items.push_back({toolbarButtonWidth(loop_icon), [&]() { if (toolButton("loop", loop_icon, "Loop playback")) loopPlaybackClicked(); }, "Loop playback", [this]() { loopPlaybackClicked(); }}); - items.push_back(separator()); } items.push_back({speed_width, [&]() { drawSpeedDropdown(speed_width); }}); if (!can->liveStreaming()) { @@ -313,6 +317,11 @@ void VideoWidget::loopPlaybackClicked() { getReplay()->setLoop(!getReplay()->loop()); } +void VideoWidget::cropVideoClicked() { + settings.crop_video = !settings.crop_video; + settings.changed(); +} + void VideoWidget::timeRangeChanged() { const auto time_range = can->timeRange(); if (can->liveStreaming()) { @@ -551,14 +560,9 @@ const RgbImage *StreamCameraView::thumbnailAt(double sec) { void StreamCameraView::drawScrubThumbnail(ImDrawList *p, double sec) { p->AddRectFilled(rect().Min, rect().Max, IM_COL32(0, 0, 0, 255)); if (const RgbImage *image = thumbnailAt(sec)) { - // scale to the widget size, keeping the aspect ratio - const float scale = std::min(width() / image->width, height() / image->height); - const ImVec2 scaled_size(std::floor(image->width * scale), std::floor(image->height * scale)); - const ImVec2 center = rect().GetCenter(); - const ImVec2 thumb_min(center.x - (int)(scaled_size.x / 2), center.y - (int)(scaled_size.y / 2)); - ImRect thumb_rect(thumb_min, ImVec2(thumb_min.x + scaled_size.x, thumb_min.y + scaled_size.y)); - p->AddImage(big_thumbnail_texture_.ref(), thumb_rect.Min, thumb_rect.Max); - drawTime(p, thumb_rect, sec); + const VideoPlacement placement = videoPlacement(rect(), (float)image->width / image->height, settings.crop_video); + p->AddImage(big_thumbnail_texture_.ref(), placement.min, placement.max, placement.uv0, placement.uv1); + drawTime(p, rect(), sec); } } diff --git a/openpilot/tools/cabana/ui/widgets/videowidget.h b/openpilot/tools/cabana/ui/widgets/videowidget.h index d873c3ed10..90058850c3 100644 --- a/openpilot/tools/cabana/ui/widgets/videowidget.h +++ b/openpilot/tools/cabana/ui/widgets/videowidget.h @@ -105,6 +105,7 @@ private: void drawSpeedDropdown(float width); void drawSpeedMenuItems(); void loopPlaybackClicked(); + void cropVideoClicked(); void vipcAvailableStreamsUpdated(std::set streams); void showRouteInfo();