mediav2 CTS: Add film grain validation test for av1 decoders

Bug: 308861892
Test: atest android.mediav2.cts.Av1FilmGrainValidationTest

Change-Id: Ib4bd74ad4cae1d6fb3ef710cb70a06191cc38045
diff --git a/tests/media/common/src/android/mediav2/common/cts/DecodeStreamToYuv.java b/tests/media/common/src/android/mediav2/common/cts/DecodeStreamToYuv.java
index bd03bd5..452f838 100644
--- a/tests/media/common/src/android/mediav2/common/cts/DecodeStreamToYuv.java
+++ b/tests/media/common/src/android/mediav2/common/cts/DecodeStreamToYuv.java
@@ -168,7 +168,7 @@
         mCodec.releaseOutputBuffer(bufferIndex, false);
     }
 
-    static YUVImage getImage(Image image) {
+    public static YUVImage getImage(Image image) {
         YUVImage yuvImage = new YUVImage();
         int format = image.getFormat();
         assertTrue("unexpected image format",
@@ -234,6 +234,10 @@
         return yuvImage;
     }
 
+    public static ArrayList<byte[]> unWrapYUVImage(YUVImage image) {
+        return image.mData;
+    }
+
     void writeImage(Image image) {
         YUVImage yuvImage = getImage(image);
         try (FileOutputStream outputStream = new FileOutputStream(mOutputFile, mOutputCount != 0)) {
diff --git a/tests/media/common/src/android/mediav2/common/cts/VideoErrorManager.java b/tests/media/common/src/android/mediav2/common/cts/VideoErrorManager.java
index d22ef21..28bd32a 100644
--- a/tests/media/common/src/android/mediav2/common/cts/VideoErrorManager.java
+++ b/tests/media/common/src/android/mediav2/common/cts/VideoErrorManager.java
@@ -17,8 +17,10 @@
 package android.mediav2.common.cts;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import android.util.Log;
+import android.util.Pair;
 
 import com.android.compatibility.common.util.Preconditions;
 
@@ -84,6 +86,43 @@
         mFramesPSNR = new ArrayList<>();
     }
 
+    public static <T> Pair<Double, Integer> computeFrameVariance(int width, int height, T luma) {
+        final int bSize = 16;
+        assertTrue("chosen block size is too large with respect to image dimensions",
+                width > bSize && height > bSize);
+        double varianceSum = 0;
+        int blocks = 0;
+        for (int i = 0; i < height - bSize; i += bSize) {
+            for (int j = 0; j < width - bSize; j += bSize) {
+                long sse = 0, sum = 0;
+                int offset = i * width + j;
+                for (int p = 0; p < bSize; p++) {
+                    for (int q = 0; q < bSize; q++) {
+                        int sample;
+                        if (luma instanceof byte[]) {
+                            sample = ((byte[]) luma)[offset + p * width + q];
+                        } else if (luma instanceof short[]) {
+                            sample = ((short[]) luma)[offset + p * width + q];
+                        } else {
+                            throw new IllegalArgumentException("Unsupported data type");
+                        }
+                        sum += sample;
+                        sse += sample * sample;
+                    }
+                }
+                double meanOfSquares = ((double) sse) / (bSize * bSize);
+                double mean = ((double) sum) / (bSize * bSize);
+                double squareOfMean = mean * mean;
+                double blockVariance = (meanOfSquares - squareOfMean);
+                assertTrue("variance can't be negative", blockVariance >= 0.0f);
+                varianceSum += blockVariance;
+                assertTrue("caution overflow", varianceSum >= 0.0);
+                blocks++;
+            }
+        }
+        return Pair.create(varianceSum, blocks);
+    }
+
     static double computeMSE(byte[] data0, byte[] data1, int bytesPerSample) {
         assertEquals(data0.length, data1.length);
         int length = data0.length / bytesPerSample;
diff --git a/tests/media/src/android/mediav2/cts/Av1FilmGrainValidationTest.java b/tests/media/src/android/mediav2/cts/Av1FilmGrainValidationTest.java
new file mode 100644
index 0000000..f096422
--- /dev/null
+++ b/tests/media/src/android/mediav2/cts/Av1FilmGrainValidationTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.mediav2.cts;
+
+import static android.mediav2.common.cts.DecodeStreamToYuv.getImage;
+import static android.mediav2.common.cts.DecodeStreamToYuv.unWrapYUVImage;
+import static android.mediav2.common.cts.VideoErrorManager.computeFrameVariance;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.ImageFormat;
+import android.media.Image;
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+import android.mediav2.common.cts.CodecDecoderTestBase;
+import android.mediav2.common.cts.ImageSurface;
+import android.mediav2.common.cts.OutputManager;
+import android.util.Log;
+import android.util.Pair;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * The test verifies if film grain effect is applied to the output of av1 decoder.
+ * <p>
+ * An av1 test clip with film grain enabled is decoded using av1 decoders present on the device.
+ * For a select few frames, their variance is computed. This metric is compared against a
+ * reference variance value which is obtained by decoding the same clip with film grain filter
+ * disabled. The test value is expected to be larger than the reference value by a threshold margin.
+ */
+@RunWith(Parameterized.class)
+public class Av1FilmGrainValidationTest extends CodecDecoderTestBase {
+    private static final String LOG_TAG = Av1FilmGrainValidationTest.class.getSimpleName();
+    private static final String MEDIA_DIR = WorkDir.getMediaDirString();
+    private static final double TOLERANCE = 0.95;
+
+    private static class FrameMetadata {
+        public final int mFrameIndex;
+        public final double mVarWithoutFilmGrain;
+        public final double mVarWithFilmGrain;
+
+        FrameMetadata(int frameIndex, double varWithoutFilmGrain, double varWithFilmGrain) {
+            mFrameIndex = frameIndex;
+            mVarWithoutFilmGrain = varWithoutFilmGrain;
+            mVarWithFilmGrain = varWithFilmGrain;
+        }
+    }
+
+    private final Map<Integer, FrameMetadata> mFrameVarList;
+
+    public Av1FilmGrainValidationTest(String decoder, String mediaType, String testFile,
+            Map<Integer, FrameMetadata> frameVarList, String allTestParams) {
+        super(decoder, mediaType, MEDIA_DIR + testFile, allTestParams);
+        mFrameVarList = frameVarList;
+    }
+
+    @Parameterized.Parameters(name = "{index}_{0}_{1}")
+    public static Collection<Object[]> input() {
+        final List<Object[]> exhaustiveArgsList = new ArrayList<>(Arrays.asList(new Object[][]{
+                {MediaFormat.MIMETYPE_VIDEO_AV1, "crowd_run_854x480_1mbps_av1_40fg.mp4",
+                        Map.ofEntries(
+                                Map.entry(3, new FrameMetadata(3, 2385.037126, 2417.27729)),
+                                Map.entry(10, new FrameMetadata(10, 2342.782887, 2384.061935)),
+                                Map.entry(55, new FrameMetadata(55, 2204.930969, 2240.242686)),
+                                Map.entry(70, new FrameMetadata(70, 2286.123113, 2319.397801)),
+                                Map.entry(92, new FrameMetadata(92, 2365.240550, 2396.21792)),
+                                Map.entry(122, new FrameMetadata(122, 2253.176403, 2287.724775)),
+                                Map.entry(131, new FrameMetadata(131, 2144.242954, 2177.774318)),
+                                Map.entry(139, new FrameMetadata(139, 2158.459979, 2191.296022)),
+                                Map.entry(151, new FrameMetadata(151, 2180.469236, 2214.399074)),
+                                Map.entry(169, new FrameMetadata(169, 2356.567787, 2390.686406)),
+                                Map.entry(174, new FrameMetadata(174, 2360.921796, 2391.112868)),
+                                Map.entry(178, new FrameMetadata(178, 2398.315402, 2432.657012)),
+                                Map.entry(202, new FrameMetadata(202, 2492.972204, 2523.962709)),
+                                Map.entry(209, new FrameMetadata(209, 2527.618761, 2563.03917)),
+                                Map.entry(216, new FrameMetadata(216, 2451.719897, 2483.775592)),
+                                Map.entry(240, new FrameMetadata(240, 2480.633388, 2520.155191)),
+                                Map.entry(273, new FrameMetadata(273, 2574.790839, 2607.216427)),
+                                Map.entry(277, new FrameMetadata(277, 2556.909117, 2590.407241)),
+                                Map.entry(278, new FrameMetadata(278, 2532.411330, 2567.341203)),
+                                Map.entry(280, new FrameMetadata(280, 2381.975912, 2417.329294)),
+                                Map.entry(285, new FrameMetadata(285, 2530.698041, 2560.922845)),
+                                Map.entry(302, new FrameMetadata(302, 2433.898254, 2464.906672)),
+                                Map.entry(304, new FrameMetadata(304, 2346.091436, 2384.606821)),
+                                Map.entry(311, new FrameMetadata(311, 2391.406914, 2421.758312)),
+                                Map.entry(341, new FrameMetadata(341, 2356.578383, 2390.325355)),
+                                Map.entry(343, new FrameMetadata(343, 2484.964095, 2515.606202)),
+                                Map.entry(351, new FrameMetadata(351, 2511.545498, 2544.680129)),
+                                Map.entry(386, new FrameMetadata(386, 2510.254054, 2541.890124)),
+                                Map.entry(408, new FrameMetadata(408, 2511.183279, 2545.874258)),
+                                Map.entry(424, new FrameMetadata(424, 2563.492336, 2598.342548))
+                        )},
+        }));
+        return prepareParamList(exhaustiveArgsList, false, false, true, false);
+    }
+
+    protected void dequeueOutput(int bufferIndex, MediaCodec.BufferInfo info) {
+        if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+            mSawOutputEOS = true;
+        }
+        if (ENABLE_LOGS) {
+            Log.v(LOG_TAG, "output: id: " + bufferIndex + " flags: " + info.flags + " size: "
+                    + info.size + " timestamp: " + info.presentationTimeUs);
+        }
+        if (info.size > 0 && (info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
+            mOutputBuff.saveOutPTS(info.presentationTimeUs);
+            mOutputCount++;
+        }
+        mCodec.releaseOutputBuffer(bufferIndex, mSurface != null);
+        if (info.size > 0) {
+            try (Image image = mImageSurface.getImage(1000)) {
+                assertNotNull("no image received from surface \n" + mTestConfig + mTestEnv, image);
+                if (mFrameVarList.containsKey(mOutputCount - 1)) {
+                    MediaFormat format = getOutputFormat();
+                    ArrayList<byte[]> data = unWrapYUVImage(getImage(image));
+                    Pair<Double, Integer> var =
+                            computeFrameVariance(getWidth(format), getHeight(format), data.get(0));
+                    double frameVariance = var.first / var.second;
+                    FrameMetadata metadata = mFrameVarList.get(mOutputCount - 1);
+                    double refVariance = metadata.mVarWithoutFilmGrain + (
+                            (metadata.mVarWithFilmGrain - metadata.mVarWithoutFilmGrain)
+                                    * TOLERANCE);
+                    String msg = String.format(Locale.getDefault(),
+                            "FilmGrain filter not applied. For frame %d, received variance %f, "
+                                    + "expected min variance %f, no-filmgrain ref variance %f, "
+                                    + "film grain ref variance %f \n",
+                            metadata.mFrameIndex, frameVariance, refVariance,
+                            metadata.mVarWithoutFilmGrain, metadata.mVarWithFilmGrain);
+                    assertTrue(msg + mTestConfig + mTestEnv, frameVariance >= refVariance);
+                }
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    /**
+     * Check description of class {@link Av1FilmGrainValidationTest}
+     */
+    @Test
+    public void testAv1FilmGrainRequirement() throws Exception {
+        MediaFormat format = setUpSource(mTestFile);
+        mImageSurface = new ImageSurface();
+        setUpSurface(getWidth(format), getHeight(format), ImageFormat.YUV_420_888, 1, null);
+        mOutputBuff = new OutputManager();
+        mCodec = MediaCodec.createByCodecName(mCodecName);
+        configureCodec(format, true, true, false);
+        mCodec.start();
+        doWork(Integer.MAX_VALUE);
+        queueEOS();
+        waitForAllOutputs();
+        mCodec.stop();
+        mCodec.release();
+        mExtractor.release();
+    }
+}
diff --git a/tests/videocodec/src/android/videocodec/cts/VideoDecodeEditEncodeTest.java b/tests/videocodec/src/android/videocodec/cts/VideoDecodeEditEncodeTest.java
index 802e642..715c776 100644
--- a/tests/videocodec/src/android/videocodec/cts/VideoDecodeEditEncodeTest.java
+++ b/tests/videocodec/src/android/videocodec/cts/VideoDecodeEditEncodeTest.java
@@ -24,6 +24,7 @@
 import static android.mediav2.common.cts.CodecEncoderTestBase.muxOutput;
 import static android.mediav2.common.cts.CodecTestBase.ComponentClass.HARDWARE;
 import static android.mediav2.common.cts.CodecTestBase.Q_DEQ_TIMEOUT_US;
+import static android.mediav2.common.cts.VideoErrorManager.computeFrameVariance;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -41,6 +42,7 @@
 import android.mediav2.common.cts.OutputSurface;
 import android.mediav2.common.cts.RawResource;
 import android.util.Log;
+import android.util.Pair;
 
 import androidx.annotation.NonNull;
 
@@ -676,9 +678,6 @@
     private double computeVariance(RawResource yuv) throws IOException {
         Preconditions.assertTestFileExists(yuv.mFileName);
         assertEquals("has support for 8 bit clips only", 1, yuv.mBytesPerSample);
-        final int bSize = 16;
-        assertTrue("chosen block size is too large with respect to image dimensions",
-                yuv.mWidth > bSize && yuv.mHeight > bSize);
         double variance = 0;
         int blocks = 0;
         try (RandomAccessFile refStream = new RandomAccessFile(new File(yuv.mFileName), "r")) {
@@ -691,27 +690,9 @@
                 if (bytesReadRef == -1) break;
                 assertEquals("bad, reading unaligned frame size", bytesReadRef, ySize);
                 refStream.skipBytes(uvSize);
-                for (int i = 0; i < yuv.mHeight - bSize; i += bSize) {
-                    for (int j = 0; j < yuv.mWidth - bSize; j += bSize) {
-                        long sse = 0, sum = 0;
-                        int offset = i * yuv.mWidth + j;
-                        for (int p = 0; p < bSize; p++) {
-                            for (int q = 0; q < bSize; q++) {
-                                int sample = luma[offset + p * yuv.mWidth + q];
-                                sum += sample;
-                                sse += sample * sample;
-                            }
-                        }
-                        double meanOfSquares = ((double) sse) / (bSize * bSize);
-                        double mean = ((double) sum) / (bSize * bSize);
-                        double squareOfMean = mean * mean;
-                        double blockVariance = (meanOfSquares - squareOfMean);
-                        assertTrue("variance can't be negative", blockVariance >= 0.0f);
-                        variance += blockVariance;
-                        assertTrue("caution overflow", variance >= 0.0);
-                        blocks++;
-                    }
-                }
+                Pair<Double, Integer> var = computeFrameVariance(yuv.mWidth, yuv.mHeight, luma);
+                variance += var.first;
+                blocks += var.second;
             }
             return variance / blocks;
         }