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