From de10689433c292ee5eac5bb20a327b9c1d8b71f9 Mon Sep 17 00:00:00 2001 From: Hye Soo Yang <hyey@google.com> Date: Mon, 19 Oct 2020 04:40:07 -0700 Subject: [PATCH] Fix DecodeImage issue where decoding animated GIF fails if first frame does not fill the canvas. PiperOrigin-RevId: 337830066 Change-Id: Iecdf4cb7f6815f65e7abceb85b61e8d85d80fa9e --- tensorflow/core/BUILD | 2 + .../base_api/api_def_DecodeImage.pbtxt | 6 +++ tensorflow/core/lib/gif/gif_io.cc | 41 +++++++++++------- .../core/lib/gif/testdata/red_black.gif | Bin 0 -> 88 bytes tensorflow/core/lib/gif/testdata/squares.gif | Bin 0 -> 103 bytes tensorflow/python/ops/image_ops_impl.py | 6 +++ tensorflow/python/ops/image_ops_test.py | 28 ++++++++++++ 7 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 tensorflow/core/lib/gif/testdata/red_black.gif create mode 100644 tensorflow/core/lib/gif/testdata/squares.gif diff --git a/tensorflow/core/BUILD b/tensorflow/core/BUILD index adc59c67dce..943ead9d451 100644 --- a/tensorflow/core/BUILD +++ b/tensorflow/core/BUILD @@ -2128,6 +2128,8 @@ filegroup( # GIF data "lib/gif/testdata/lena.gif", "lib/gif/testdata/scan.gif", + "lib/gif/testdata/red_black.gif", + "lib/gif/testdata/squares.gif", # GIF data with optimization "lib/gif/testdata/optimized.gif", # BMP data diff --git a/tensorflow/core/api_def/base_api/api_def_DecodeImage.pbtxt b/tensorflow/core/api_def/base_api/api_def_DecodeImage.pbtxt index c534425eb24..7174c8d3daf 100644 --- a/tensorflow/core/api_def/base_api/api_def_DecodeImage.pbtxt +++ b/tensorflow/core/api_def/base_api/api_def_DecodeImage.pbtxt @@ -47,5 +47,11 @@ constructing your graph if you are intermixing GIF files with BMP, JPEG, and/or PNG files. Alternately, set the expand_animations argument of this function to False, in which case the op will return 3-dimensional tensors and will truncate animated GIF files to the first frame. + +*NOTE*: If the first frame of an animated GIF does not occupy the entire +canvas (maximum frame width x maximum frame height), then it fills the +unoccupied areas (in the first frame) with zeros (black). For frames after the +first frame that does not occupy the entire canvas, it uses the previous +frame to fill the unoccupied areas. END } diff --git a/tensorflow/core/lib/gif/gif_io.cc b/tensorflow/core/lib/gif/gif_io.cc index 659513d05ed..5fb47043654 100644 --- a/tensorflow/core/lib/gif/gif_io.cc +++ b/tensorflow/core/lib/gif/gif_io.cc @@ -85,7 +85,6 @@ uint8* Decode(const void* srcdata, int datasize, } int target_num_frames = gif_file->ImageCount; - if (!expand_animations) target_num_frames = 1; // Don't request more memory than needed for each frame, preventing OOM int max_frame_width = 0; @@ -101,6 +100,7 @@ uint8* Decode(const void* srcdata, int datasize, const int width = max_frame_width; const int height = max_frame_height; const int channel = 3; + if (!expand_animations) target_num_frames = 1; uint8* const dstdata = allocate_output(target_num_frames, width, height, channel); @@ -118,27 +118,36 @@ uint8* Decode(const void* srcdata, int datasize, if (img_desc->Left != 0 || img_desc->Top != 0 || img_desc->Width != width || img_desc->Height != height) { - // If the first frame does not fill the entire canvas then return error. + // If the first frame does not fill the entire canvas then fill the + // unoccupied canvas with zeros (black). if (k == 0) { - *error_string = "the first frame does not fill the canvas"; - return nullptr; + for (int i = 0; i < height; ++i) { + uint8* p_dst = this_dst + i * width * channel; + for (int j = 0; j < width; ++j) { + p_dst[j * channel + 0] = 0; + p_dst[j * channel + 1] = 0; + p_dst[j * channel + 2] = 0; + } + } + } else { + // Otherwise previous frame will be reused to fill the unoccupied + // canvas. + uint8* last_dst = dstdata + (k - 1) * width * channel * height; + for (int i = 0; i < height; ++i) { + uint8* p_dst = this_dst + i * width * channel; + uint8* l_dst = last_dst + i * width * channel; + for (int j = 0; j < width; ++j) { + p_dst[j * channel + 0] = l_dst[j * channel + 0]; + p_dst[j * channel + 1] = l_dst[j * channel + 1]; + p_dst[j * channel + 2] = l_dst[j * channel + 2]; + } + } } - // Otherwise previous frame will be reused to fill the unoccupied canvas. + imgLeft = std::max(imgLeft, 0); imgTop = std::max(imgTop, 0); imgRight = std::min(imgRight, width); imgBottom = std::min(imgBottom, height); - - uint8* last_dst = dstdata + (k - 1) * width * channel * height; - for (int i = 0; i < height; ++i) { - uint8* p_dst = this_dst + i * width * channel; - uint8* l_dst = last_dst + i * width * channel; - for (int j = 0; j < width; ++j) { - p_dst[j * channel + 0] = l_dst[j * channel + 0]; - p_dst[j * channel + 1] = l_dst[j * channel + 1]; - p_dst[j * channel + 2] = l_dst[j * channel + 2]; - } - } } ColorMapObject* color_map = this_image->ImageDesc.ColorMap diff --git a/tensorflow/core/lib/gif/testdata/red_black.gif b/tensorflow/core/lib/gif/testdata/red_black.gif new file mode 100644 index 0000000000000000000000000000000000000000..d32ddd3547d4c7601fa02acc725db8435e4a3fa4 GIT binary patch literal 88 zcmZ?wbhEHb6krfw_`m=H$22q)|8x7fh6Fo12DlpO889;fMHPRtaELJcXV3vD0V!r+ ll4<E*d3r75;+jpDr1bfu%4g?9Y7{N`;d}1XHV;MyYXFAd7x@4H literal 0 HcmV?d00001 diff --git a/tensorflow/core/lib/gif/testdata/squares.gif b/tensorflow/core/lib/gif/testdata/squares.gif new file mode 100644 index 0000000000000000000000000000000000000000..159f86355a8fd7af93a7774531c10134117337f1 GIT binary patch literal 103 zcmZ?wbhEHb6krfw_`tw$OhZF)+GoZ8+<vYh!Oo5Wu10zW%!~{S42nNlIE)znGw3ig sFf(ugAp;XfOaIE#Z~2q85t2ZKKyw6ul6*a2$;ETF-0I$J$I4(004eDk3IG5A literal 0 HcmV?d00001 diff --git a/tensorflow/python/ops/image_ops_impl.py b/tensorflow/python/ops/image_ops_impl.py index 8a767597f00..75e66d8f513 100644 --- a/tensorflow/python/ops/image_ops_impl.py +++ b/tensorflow/python/ops/image_ops_impl.py @@ -2964,6 +2964,12 @@ def decode_image(contents, function to `False`, in which case the op will return 3-dimensional tensors and will truncate animated GIF files to the first frame. + NOTE: If the first frame of an animated GIF does not occupy the entire + canvas (maximum frame width x maximum frame height), then it fills the + unoccupied areas (in the first frame) with zeros (black). For frames after the + first frame that does not occupy the entire canvas, it uses the previous + frame to fill the unoccupied areas. + Args: contents: 0-D `string`. The encoded image bytes. channels: An optional `int`. Defaults to `0`. Number of color channels for diff --git a/tensorflow/python/ops/image_ops_test.py b/tensorflow/python/ops/image_ops_test.py index 7f4a093ca40..a7c964d8a30 100644 --- a/tensorflow/python/ops/image_ops_test.py +++ b/tensorflow/python/ops/image_ops_test.py @@ -5756,6 +5756,34 @@ class DecodeImageTest(test_util.TensorFlowTestCase, parameterized.TestCase): img = image_ops.decode_and_crop_jpeg(img_bytes, [1, 1, 2, 2]) self.evaluate(img) + def testGifFramesWithDiffSize(self): + """Test decoding an animated GIF. + + This test verifies that `decode_image` op can decode animated GIFs whose + first frame does not fill the canvas. The unoccupied areas should be filled + with zeros (black). + + `squares.gif` is animated with two images of different sizes. It + alternates between a smaller image of size 10 x 10 and a larger image of + size 16 x 16. Because it starts animating with the smaller image, the first + frame does not fill the canvas. (Canvas size is equal to max frame width x + max frame height.) + + `red_black.gif` has just a single image in a GIF format. It is the same + image as the smaller image (size 10 x 10) of the two images in + `squares.gif`. The only difference is that its background (canvas - smaller + image) is pre-filled with zeros (black); it is the groundtruth. + """ + base = "tensorflow/core/lib/gif/testdata" + gif_bytes0 = io_ops.read_file(os.path.join(base, "squares.gif")) + image0 = image_ops.decode_image(gif_bytes0, dtype=dtypes.float32, + expand_animations=False) + gif_bytes1 = io_ops.read_file(os.path.join(base, "red_black.gif")) + image1 = image_ops.decode_image(gif_bytes1, dtype=dtypes.float32) + image1_0 = array_ops.gather(image1, 0) + image0, image1_0 = self.evaluate([image0, image1_0]) + self.assertAllEqual(image0, image1_0) + if __name__ == "__main__": googletest.main()