Add scale and compare methods to VideoFrame::UpdateRect
Add tests for different UpdateRect methods as they are no longer trivial
This change will enable providing useful update rects after scaling
is done.
Bug: webrtc:11058
Change-Id: I2311dbbbb5eca5cfaf845306674e6890050f80c6
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/159820
Commit-Queue: Ilya Nikolaevskiy <ilnik@webrtc.org>
Reviewed-by: Niels Moller <nisse@webrtc.org>
Reviewed-by: Stefan Holmer <stefan@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#29835}
diff --git a/api/video/video_frame.cc b/api/video/video_frame.cc
index 63902af..4f6bd86 100644
--- a/api/video/video_frame.cc
+++ b/api/video/video_frame.cc
@@ -60,6 +60,103 @@
return width == 0 && height == 0;
}
+VideoFrame::UpdateRect VideoFrame::UpdateRect::ScaleWithFrame(
+ int frame_width,
+ int frame_height,
+ int crop_x,
+ int crop_y,
+ int crop_width,
+ int crop_height,
+ int scaled_width,
+ int scaled_height) const {
+ RTC_DCHECK_GT(frame_width, 0);
+ RTC_DCHECK_GT(frame_height, 0);
+
+ RTC_DCHECK_GT(crop_width, 0);
+ RTC_DCHECK_GT(crop_height, 0);
+
+ RTC_DCHECK_LE(crop_width + crop_x, frame_width);
+ RTC_DCHECK_LE(crop_height + crop_y, frame_height);
+
+ RTC_DCHECK_GT(scaled_width, 0);
+ RTC_DCHECK_GT(scaled_height, 0);
+
+ // Check if update rect is out of the cropped area.
+ if (offset_x + width < crop_x || offset_x > crop_x + crop_width ||
+ offset_y + height < crop_y || offset_y > crop_y + crop_width) {
+ return {0, 0, 0, 0};
+ }
+
+ int x = offset_x - crop_x;
+ int w = width;
+ if (x < 0) {
+ w += x;
+ x = 0;
+ }
+ int y = offset_y - crop_y;
+ int h = height;
+ if (y < 0) {
+ h += y;
+ y = 0;
+ }
+
+ // Lower corner is rounded down.
+ x = x * scaled_width / crop_width;
+ y = y * scaled_height / crop_height;
+ // Upper corner is rounded up.
+ w = (w * scaled_width + crop_width - 1) / crop_width;
+ h = (h * scaled_height + crop_height - 1) / crop_height;
+
+ // Round to full 2x2 blocks due to possible subsampling in the pixel data.
+ if (x % 2) {
+ --x;
+ ++w;
+ }
+ if (y % 2) {
+ --y;
+ ++h;
+ }
+ if (w % 2) {
+ ++w;
+ }
+ if (h % 2) {
+ ++h;
+ }
+
+ // Expand the update rect by 2 pixels in each direction to include any
+ // possible scaling artifacts.
+ if (scaled_width != crop_width || scaled_height != crop_height) {
+ if (x > 0) {
+ x -= 2;
+ w += 2;
+ }
+ if (y > 0) {
+ y -= 2;
+ h += 2;
+ }
+ w += 2;
+ h += 2;
+ }
+
+ // Ensure update rect is inside frame dimensions.
+ if (x + w > scaled_width) {
+ w = scaled_width - x;
+ }
+ if (y + h > scaled_height) {
+ h = scaled_height - y;
+ }
+ RTC_DCHECK_GE(w, 0);
+ RTC_DCHECK_GE(h, 0);
+ if (w == 0 || h == 0) {
+ w = 0;
+ h = 0;
+ x = 0;
+ y = 0;
+ }
+
+ return {x, y, w, h};
+}
+
VideoFrame::Builder::Builder() = default;
VideoFrame::Builder::~Builder() = default;
diff --git a/api/video/video_frame.h b/api/video/video_frame.h
index 338e2fd..d16ef8c 100644
--- a/api/video/video_frame.h
+++ b/api/video/video_frame.h
@@ -49,6 +49,31 @@
void MakeEmptyUpdate();
bool IsEmpty() const;
+
+ // Per-member equality check. Empty rectangles with different offsets would
+ // be considered different.
+ bool operator==(const UpdateRect& other) const {
+ return other.offset_x == offset_x && other.offset_y == offset_y &&
+ other.width == width && other.height == height;
+ }
+
+ bool operator!=(const UpdateRect& other) const { return !(*this == other); }
+
+ // Scales update_rect given original frame dimensions.
+ // Cropping is applied first, then rect is scaled down.
+ // Update rect is snapped to 2x2 grid due to possible UV subsampling and
+ // then expanded by additional 2 pixels in each direction to accommodate any
+ // possible scaling artifacts.
+ // Note, close but not equal update_rects on original frame may result in
+ // the same scaled update rects.
+ UpdateRect ScaleWithFrame(int frame_width,
+ int frame_height,
+ int crop_x,
+ int crop_y,
+ int crop_width,
+ int crop_height,
+ int scaled_width,
+ int scaled_height) const;
};
// Interface for accessing elements of the encoded frame that was the base for
diff --git a/common_video/video_frame_unittest.cc b/common_video/video_frame_unittest.cc
index f7a27be7..6b2c97b 100644
--- a/common_video/video_frame_unittest.cc
+++ b/common_video/video_frame_unittest.cc
@@ -551,4 +551,146 @@
::testing::Values(VideoFrameBuffer::Type::kI420,
VideoFrameBuffer::Type::kI010)));
+TEST(TestUpdateRect, CanCompare) {
+ VideoFrame::UpdateRect a = {0, 0, 100, 200};
+ VideoFrame::UpdateRect b = {0, 0, 100, 200};
+ VideoFrame::UpdateRect c = {1, 0, 100, 200};
+ VideoFrame::UpdateRect d = {0, 1, 100, 200};
+ EXPECT_TRUE(a == b);
+ EXPECT_FALSE(a == c);
+ EXPECT_FALSE(a == d);
+}
+
+TEST(TestUpdateRect, ComputesIsEmpty) {
+ VideoFrame::UpdateRect a = {0, 0, 0, 0};
+ VideoFrame::UpdateRect b = {0, 0, 100, 200};
+ VideoFrame::UpdateRect c = {1, 100, 0, 0};
+ VideoFrame::UpdateRect d = {1, 100, 100, 200};
+ EXPECT_TRUE(a.IsEmpty());
+ EXPECT_FALSE(b.IsEmpty());
+ EXPECT_TRUE(c.IsEmpty());
+ EXPECT_FALSE(d.IsEmpty());
+}
+
+TEST(TestUpdateRectUnion, NonIntersecting) {
+ VideoFrame::UpdateRect a = {0, 0, 10, 20};
+ VideoFrame::UpdateRect b = {100, 200, 10, 20};
+ a.Union(b);
+ EXPECT_EQ(a, VideoFrame::UpdateRect({0, 0, 110, 220}));
+}
+
+TEST(TestUpdateRectUnion, Intersecting) {
+ VideoFrame::UpdateRect a = {0, 0, 10, 10};
+ VideoFrame::UpdateRect b = {5, 5, 30, 20};
+ a.Union(b);
+ EXPECT_EQ(a, VideoFrame::UpdateRect({0, 0, 35, 25}));
+}
+
+TEST(TestUpdateRectUnion, OneInsideAnother) {
+ VideoFrame::UpdateRect a = {0, 0, 100, 100};
+ VideoFrame::UpdateRect b = {5, 5, 30, 20};
+ a.Union(b);
+ EXPECT_EQ(a, VideoFrame::UpdateRect({0, 0, 100, 100}));
+}
+
+TEST(TestUpdateRectIntersect, NonIntersecting) {
+ VideoFrame::UpdateRect a = {0, 0, 10, 20};
+ VideoFrame::UpdateRect b = {100, 200, 10, 20};
+ a.Intersect(b);
+ EXPECT_EQ(a, VideoFrame::UpdateRect({0, 0, 0, 0}));
+}
+
+TEST(TestUpdateRectIntersect, Intersecting) {
+ VideoFrame::UpdateRect a = {0, 0, 10, 10};
+ VideoFrame::UpdateRect b = {5, 5, 30, 20};
+ a.Intersect(b);
+ EXPECT_EQ(a, VideoFrame::UpdateRect({5, 5, 5, 5}));
+}
+
+TEST(TestUpdateRectIntersect, OneInsideAnother) {
+ VideoFrame::UpdateRect a = {0, 0, 100, 100};
+ VideoFrame::UpdateRect b = {5, 5, 30, 20};
+ a.Intersect(b);
+ EXPECT_EQ(a, VideoFrame::UpdateRect({5, 5, 30, 20}));
+}
+
+TEST(TestUpdateRectScale, NoScale) {
+ const int width = 640;
+ const int height = 480;
+ VideoFrame::UpdateRect a = {100, 50, 100, 200};
+ VideoFrame::UpdateRect scaled =
+ a.ScaleWithFrame(width, height, 0, 0, width, height, width, height);
+ EXPECT_EQ(scaled, VideoFrame::UpdateRect({100, 50, 100, 200}));
+}
+
+TEST(TestUpdateRectScale, CropOnly) {
+ const int width = 640;
+ const int height = 480;
+ VideoFrame::UpdateRect a = {100, 50, 100, 200};
+ VideoFrame::UpdateRect scaled = a.ScaleWithFrame(
+ width, height, 10, 10, width - 20, height - 20, width - 20, height - 20);
+ EXPECT_EQ(scaled, VideoFrame::UpdateRect({90, 40, 100, 200}));
+}
+
+TEST(TestUpdateRectScale, CropOnlyToOddOffset) {
+ const int width = 640;
+ const int height = 480;
+ VideoFrame::UpdateRect a = {100, 50, 100, 200};
+ VideoFrame::UpdateRect scaled = a.ScaleWithFrame(
+ width, height, 5, 5, width - 10, height - 10, width - 10, height - 10);
+ EXPECT_EQ(scaled, VideoFrame::UpdateRect({94, 44, 102, 202}));
+}
+
+TEST(TestUpdateRectScale, ScaleByHalf) {
+ const int width = 640;
+ const int height = 480;
+ VideoFrame::UpdateRect a = {100, 60, 100, 200};
+ VideoFrame::UpdateRect scaled = a.ScaleWithFrame(
+ width, height, 0, 0, width, height, width / 2, height / 2);
+ // Scaled by half and +2 pixels in all directions.
+ EXPECT_EQ(scaled, VideoFrame::UpdateRect({48, 28, 54, 104}));
+}
+
+TEST(TestUpdateRectScale, CropToUnchangedRegionBelowUpdateRect) {
+ const int width = 640;
+ const int height = 480;
+ VideoFrame::UpdateRect a = {100, 60, 100, 200};
+ VideoFrame::UpdateRect scaled = a.ScaleWithFrame(
+ width, height, (width - 10) / 2, (height - 10) / 2, 10, 10, 10, 10);
+ // Update is out of the cropped frame.
+ EXPECT_EQ(scaled, VideoFrame::UpdateRect({0, 0, 0, 0}));
+}
+
+TEST(TestUpdateRectScale, CropToUnchangedRegionAboveUpdateRect) {
+ const int width = 640;
+ const int height = 480;
+ VideoFrame::UpdateRect a = {600, 400, 10, 10};
+ VideoFrame::UpdateRect scaled = a.ScaleWithFrame(
+ width, height, (width - 10) / 2, (height - 10) / 2, 10, 10, 10, 10);
+ // Update is out of the cropped frame.
+ EXPECT_EQ(scaled, VideoFrame::UpdateRect({0, 0, 0, 0}));
+}
+
+TEST(TestUpdateRectScale, CropInsideUpdate) {
+ const int width = 640;
+ const int height = 480;
+ VideoFrame::UpdateRect a = {300, 200, 100, 100};
+ VideoFrame::UpdateRect scaled = a.ScaleWithFrame(
+ width, height, (width - 10) / 2, (height - 10) / 2, 10, 10, 10, 10);
+ // Cropped frame is inside the update rect.
+ EXPECT_EQ(scaled, VideoFrame::UpdateRect({0, 0, 10, 10}));
+}
+
+TEST(TestUpdateRectScale, CropAndScaleByHalf) {
+ const int width = 640;
+ const int height = 480;
+ VideoFrame::UpdateRect a = {100, 60, 100, 200};
+ VideoFrame::UpdateRect scaled =
+ a.ScaleWithFrame(width, height, 10, 10, width - 20, height - 20,
+ (width - 20) / 2, (height - 20) / 2);
+ // Scaled by half and +3 pixels in all directions, because of odd offset after
+ // crop and scale.
+ EXPECT_EQ(scaled, VideoFrame::UpdateRect({42, 22, 56, 106}));
+}
+
} // namespace webrtc