Home | History | Annotate | Download | only in tests
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.media.tests;
     17 
     18 import com.android.tradefed.log.LogUtil.CLog;
     19 import com.android.tradefed.util.Pair;
     20 
     21 import java.awt.image.BufferedImage;
     22 import java.io.File;
     23 import java.io.IOException;
     24 import java.util.ArrayList;
     25 
     26 import javax.imageio.ImageIO;
     27 
     28 /**
     29  * Class that analyzes a screenshot captured from AudioLoopback test. There is a wave form in the
     30  * screenshot that has specific colors (TARGET_COLOR). This class extracts those colors and analyzes
     31  * wave amplitude, duration and form and make a decision if it's a legitimate wave form or not.
     32  */
     33 public class AudioLoopbackImageAnalyzer {
     34 
     35     // General
     36     private static final int HORIZONTAL_THRESHOLD = 10;
     37     private static final int VERTICAL_THRESHOLD = 0;
     38     private static final int PRIMARY_WAVE_COLOR = 0xFF1E4A99;
     39     private static final int SECONDARY_WAVE_COLOR = 0xFF1D4998;
     40     private static final int[] TARGET_COLORS_TABLET =
     41             new int[] {PRIMARY_WAVE_COLOR, SECONDARY_WAVE_COLOR};
     42     private static final int[] TARGET_COLORS_PHONE = new int[] {PRIMARY_WAVE_COLOR};
     43 
     44     private static final float EXPERIMENTAL_WAVE_MAX_TABLET = 69.0f; // In percent of image height
     45     private static final float EXPERIMENTAL_WAVE_MAX_PHONE = 32.0f; // In percent of image height
     46 
     47     // Image
     48     private static final int TABLET_SCREEN_MIN_WIDTH = 1700;
     49     private static final int TABLET_SCREEN_MIN_HEIGHT = 2300;
     50 
     51     // Duration parameters
     52     // Max duration should not span more than 2 of the 11 sections in the graph
     53     // Min duration should not be less than 1/4 of a section
     54     private static final float SECTION_WIDTH_IN_PERCENT = 100 * 1 / 11; // In percent of image width
     55     private static final float DURATION_MIN = SECTION_WIDTH_IN_PERCENT / 4;
     56 
     57     // Amplitude
     58     // Required numbers of column for a response
     59     private static final int MIN_NUMBER_OF_COLUMNS = 4;
     60     // The difference between two amplitude columns should not be more than this
     61     private static final float MAX_ALLOWED_COLUMN_DECREASE = 0.50f;
     62     // Only check MAX_ALLOWED_COLUMN_DECREASE up to this number
     63     private static final float MIN_NUMBER_OF_DECREASING_COLUMNS = 8;
     64 
     65     enum Result {
     66         PASS,
     67         FAIL,
     68         UNKNOWN
     69     }
     70 
     71     private static class Amplitude {
     72         public int maxHeight = -1;
     73         public int zeroCounter = 0;
     74     }
     75 
     76     public static Pair<Result, String> analyzeImage(String imgFile) {
     77         final String FN_TAG = "AudioLoopbackImageAnalyzer.analyzeImage";
     78 
     79         BufferedImage img = null;
     80         try {
     81             final File f = new File(imgFile);
     82             img = ImageIO.read(f);
     83         } catch (final IOException e) {
     84             CLog.e(e);
     85             throw new RuntimeException("Error loading image file '" + imgFile + "'");
     86         }
     87 
     88         final int width = img.getWidth();
     89         final int height = img.getHeight();
     90 
     91         CLog.i("image width=" + width + ", height=" + height);
     92 
     93         // Compute thresholds and min/max values based on image witdh, height
     94         final float waveMax;
     95         final int[] targetColors;
     96         final int amplitudeCenterMaxDiff;
     97         final float maxDuration;
     98         final int minNrOfZeroesBetweenAmplitudes;
     99 
    100         if (width >= TABLET_SCREEN_MIN_WIDTH && height >= TABLET_SCREEN_MIN_HEIGHT) {
    101             CLog.i("Apply TABLET config values");
    102             waveMax = EXPERIMENTAL_WAVE_MAX_TABLET;
    103             amplitudeCenterMaxDiff = 40;
    104             minNrOfZeroesBetweenAmplitudes = 8;
    105             maxDuration = 3 * SECTION_WIDTH_IN_PERCENT;
    106             targetColors = TARGET_COLORS_TABLET;
    107         } else {
    108             waveMax = EXPERIMENTAL_WAVE_MAX_PHONE;
    109             amplitudeCenterMaxDiff = 20;
    110             minNrOfZeroesBetweenAmplitudes = 5;
    111             maxDuration = 2.5f * SECTION_WIDTH_IN_PERCENT;
    112             targetColors = TARGET_COLORS_PHONE;
    113         }
    114 
    115         // Amplitude
    116         // Max height should be about 80% of wave max.
    117         // Min height should be about 40% of wave max.
    118         final float AMPLITUDE_MAX_VALUE = waveMax * 0.8f;
    119         final float AMPLITUDE_MIN_VALUE = waveMax * 0.4f;
    120 
    121         final int[] vertical = new int[height];
    122         final int[] horizontal = new int[width];
    123 
    124         projectPixelsToXAxis(img, targetColors, horizontal, width, height);
    125         filter(horizontal, HORIZONTAL_THRESHOLD);
    126         final Pair<Integer, Integer> durationBounds = getBounds(horizontal);
    127         if (!boundsWithinRange(durationBounds, 0, width)) {
    128             final String fmt = "%1$s Upper/Lower bound along horizontal axis not found";
    129             final String err = String.format(fmt, FN_TAG);
    130             CLog.w(err);
    131             return new Pair<Result, String>(Result.FAIL, err);
    132         }
    133 
    134         projectPixelsToYAxis(img, targetColors, vertical, height, durationBounds);
    135         filter(vertical, VERTICAL_THRESHOLD);
    136         final Pair<Integer, Integer> amplitudeBounds = getBounds(vertical);
    137         if (!boundsWithinRange(durationBounds, 0, height)) {
    138             final String fmt = "%1$s: Upper/Lower bound along vertical axis not found";
    139             final String err = String.format(fmt, FN_TAG);
    140             CLog.w(err);
    141             return new Pair<Result, String>(Result.FAIL, err);
    142         }
    143 
    144         final int durationLeft = durationBounds.first.intValue();
    145         final int durationRight = durationBounds.second.intValue();
    146         final int amplitudeTop = amplitudeBounds.first.intValue();
    147         final int amplitudeBottom = amplitudeBounds.second.intValue();
    148 
    149         final float amplitude = (amplitudeBottom - amplitudeTop) * 100.0f / height;
    150         final float duration = (durationRight - durationLeft) * 100.0f / width;
    151 
    152         CLog.i("AudioLoopbackImageAnalyzer: Amplitude=" + amplitude + ", Duration=" + duration);
    153 
    154         Pair<Result, String> amplResult =
    155                 analyzeAmplitude(
    156                         vertical,
    157                         amplitude,
    158                         amplitudeTop,
    159                         amplitudeBottom,
    160                         AMPLITUDE_MIN_VALUE,
    161                         AMPLITUDE_MAX_VALUE,
    162                         amplitudeCenterMaxDiff);
    163         if (amplResult.first != Result.PASS) {
    164             return amplResult;
    165         }
    166 
    167         amplResult =
    168                 analyzeDuration(
    169                         horizontal,
    170                         duration,
    171                         durationLeft,
    172                         durationRight,
    173                         DURATION_MIN,
    174                         maxDuration,
    175                         MIN_NUMBER_OF_COLUMNS,
    176                         minNrOfZeroesBetweenAmplitudes);
    177         if (amplResult.first != Result.PASS) {
    178             return amplResult;
    179         }
    180 
    181         return new Pair<Result, String>(Result.PASS, "");
    182     }
    183 
    184     /**
    185      * Function to analyze the waveforms duration (how wide it stretches along x-axis) and to make
    186      * sure the waveform degrades nicely, i.e. the amplitude columns becomes smaller and smaller
    187      * over time.
    188      *
    189      * @param horizontal - int array with waveforms amplitude values
    190      * @param duration - calculated length of duration in percent of screen width
    191      * @param durationLeft - index for "horizontal" where waveform starts
    192      * @param durationRight - index for "horizontal" where waveform ends
    193      * @param durationMin - if duration is below this value, return FAIL and failure reason
    194      * @param durationMax - if duration exceed this value, return FAIL and failure reason
    195      * @param minNumberOfAmplitudes - min number of amplitudes (columns) in waveform to pass test
    196      * @param minNrOfZeroesBetweenAmplitudes - min number of required zeroes between amplitudes
    197      * @return - returns result status and failure reason, if any
    198      */
    199     private static Pair<Result, String> analyzeDuration(
    200             int[] horizontal,
    201             float duration,
    202             int durationLeft,
    203             int durationRight,
    204             final float durationMin,
    205             final float durationMax,
    206             final int minNumberOfAmplitudes,
    207             final int minNrOfZeroesBetweenAmplitudes) {
    208         // This is the tricky one; basically, there should be "columns" that starts
    209         // at "durationLeft", with the tallest column to the left and then column
    210         // height will drop until it fades completely after "durationRight".
    211         final String FN_TAG = "AudioLoopbackImageAnalyzer.analyzeDuration";
    212 
    213         if (duration < durationMin || duration > durationMax) {
    214             final String fmt = "%1$s: Duration outside range, value=%2$f, range=(%3$f,%4$f)";
    215             return handleError(fmt, FN_TAG, duration, durationMin, durationMax);
    216         }
    217 
    218         final ArrayList<Amplitude> amplitudes = new ArrayList<Amplitude>();
    219         Amplitude currentAmplitude = null;
    220         int zeroCounter = 0;
    221 
    222         for (int i = durationLeft; i < durationRight; i++) {
    223             final int v = horizontal[i];
    224             if (v == 0) {
    225                 zeroCounter++;
    226             } else {
    227                 CLog.i("index=" + i + ", v=" + v);
    228 
    229                 if (zeroCounter > minNrOfZeroesBetweenAmplitudes) {
    230                     // Found a new amplitude; update old amplitude
    231                     // with the "gap" count - i.e. nr of zeroes between the amplitudes
    232                     if (currentAmplitude != null) {
    233                         currentAmplitude.zeroCounter = zeroCounter;
    234                     }
    235 
    236                     // Create new Amplitude object
    237                     currentAmplitude = new Amplitude();
    238                     amplitudes.add(currentAmplitude);
    239                 }
    240 
    241                 // Reset counter
    242                 zeroCounter = 0;
    243 
    244                 if (currentAmplitude != null && v > currentAmplitude.maxHeight) {
    245                     currentAmplitude.maxHeight = horizontal[i];
    246                 }
    247             }
    248         }
    249 
    250         StringBuilder sb = new StringBuilder(128);
    251         int counter = 0;
    252         for (final Amplitude a : amplitudes) {
    253             CLog.i(
    254                     sb.append("Amplitude=")
    255                             .append(counter)
    256                             .append(", MaxHeight=")
    257                             .append(a.maxHeight)
    258                             .append(", ZeroesToNextColumn=")
    259                             .append(a.zeroCounter)
    260                             .toString());
    261             counter++;
    262             sb.setLength(0);
    263         }
    264 
    265         if (amplitudes.size() < minNumberOfAmplitudes) {
    266             final String fmt = "%1$s: Not enough amplitude columns, value=%2$d";
    267             return handleError(fmt, FN_TAG, amplitudes.size());
    268         }
    269 
    270         int currentColumnHeight = -1;
    271         int oldColumnHeight = -1;
    272         for (int i = 0; i < amplitudes.size(); i++) {
    273             if (i == 0) {
    274                 oldColumnHeight = amplitudes.get(i).maxHeight;
    275                 continue;
    276             }
    277 
    278             currentColumnHeight = amplitudes.get(i).maxHeight;
    279             if (oldColumnHeight > currentColumnHeight) {
    280                 // We want at least a good number of columns that declines nicely.
    281                 // After MIN_NUMBER_OF_DECREASING_COLUMNS, we don't really care that much
    282                 if (i < MIN_NUMBER_OF_DECREASING_COLUMNS
    283                         && currentColumnHeight < (oldColumnHeight * MAX_ALLOWED_COLUMN_DECREASE)) {
    284                     final String fmt =
    285                             "%1$s: Amplitude column heights declined too much, "
    286                                     + "old=%2$d, new=%3$d, column=%4$d";
    287                     return handleError(fmt, FN_TAG, oldColumnHeight, currentColumnHeight, i);
    288                 }
    289                 oldColumnHeight = currentColumnHeight;
    290             } else if (oldColumnHeight == currentColumnHeight) {
    291                 if (i < MIN_NUMBER_OF_DECREASING_COLUMNS) {
    292                     final String fmt =
    293                             "%1$s: Amplitude column heights are same, "
    294                                     + "old=%2$d, new=%3$d, column=%4$d";
    295                     return handleError(fmt, FN_TAG, oldColumnHeight, currentColumnHeight, i);
    296                 }
    297             } else {
    298                 final String fmt =
    299                         "%1$s: Amplitude column heights don't decline, "
    300                                 + "old=%2$d, new=%3$d, column=%4$d";
    301                 return handleError(fmt, FN_TAG, oldColumnHeight, currentColumnHeight, i);
    302             }
    303         }
    304 
    305         return new Pair<Result, String>(Result.PASS, "");
    306     }
    307 
    308     /**
    309      * Function to analyze the waveforms duration (how wide it stretches along x-axis) and to make
    310      * sure the waveform degrades nicely, i.e. the amplitude columns becomes smaller and smaller
    311      * over time.
    312      *
    313      * @param vertical - integer array with waveforms amplitude accumulated values
    314      * @param amplitude - calculated height of amplitude in percent of screen height
    315      * @param amplitudeTop - index in "vertical" array where waveform starts
    316      * @param amplitudeBottom - index in "vertical" array where waveform ends
    317      * @param amplitudeMin - if amplitude is below this value, return FAIL and failure reason
    318      * @param amplitudeMax - if amplitude exceed this value, return FAIL and failure reason
    319      * @param amplitudeCenterDiffThreshold - threshold to check that waveform is centered
    320      * @return - returns result status and failure reason, if any
    321      */
    322     private static Pair<Result, String> analyzeAmplitude(
    323             int[] vertical,
    324             float amplitude,
    325             int amplitudeTop,
    326             int amplitudeBottom,
    327             final float amplitudeMin,
    328             final float amplitudeMax,
    329             final int amplitudeCenterDiffThreshold) {
    330         final String FN_TAG = "AudioLoopbackImageAnalyzer.analyzeAmplitude";
    331 
    332         if (amplitude < amplitudeMin || amplitude > amplitudeMax) {
    333             final String fmt = "%1$s: Amplitude outside range, value=%2$f, range=(%3$f,%4$f)";
    334             final String err = String.format(fmt, FN_TAG, amplitude, amplitudeMin, amplitudeMax);
    335             CLog.w(err);
    336             return new Pair<Result, String>(Result.FAIL, err);
    337         }
    338 
    339         // Are the amplitude top/bottom centered around the centerline?
    340         final int amplitudeCenter = getAmplitudeCenter(vertical, amplitudeTop, amplitudeBottom);
    341         final int topDiff = amplitudeCenter - amplitudeTop;
    342         final int bottomDiff = amplitudeBottom - amplitudeCenter;
    343         final int diff = Math.abs(topDiff - bottomDiff);
    344 
    345         if (diff < amplitudeCenterDiffThreshold) {
    346             return new Pair<Result, String>(Result.PASS, "");
    347         }
    348 
    349         final String fmt =
    350                 "%1$s: Amplitude not centered topDiff=%2$d, bottomDiff=%3$d, "
    351                         + "center=%4$d, diff=%5$d";
    352         final String err = String.format(fmt, FN_TAG, topDiff, bottomDiff, amplitudeCenter, diff);
    353         CLog.w(err);
    354         return new Pair<Result, String>(Result.FAIL, err);
    355     }
    356 
    357     private static int getAmplitudeCenter(int[] vertical, int amplitudeTop, int amplitudeBottom) {
    358         int max = -1;
    359         int center = -1;
    360         for (int i = amplitudeTop; i < amplitudeBottom; i++) {
    361             if (vertical[i] > max) {
    362                 max = vertical[i];
    363                 center = i;
    364             }
    365         }
    366 
    367         return center;
    368     }
    369 
    370     private static void projectPixelsToXAxis(
    371             BufferedImage img,
    372             final int[] targetColors,
    373             int[] horizontal,
    374             final int width,
    375             final int height) {
    376         // "Flatten image" by projecting target colors horizontally,
    377         // counting number of found pixels in each column
    378         for (int y = 0; y < height; y++) {
    379             for (int x = 0; x < width; x++) {
    380                 final int color = img.getRGB(x, y);
    381                 for (final int targetColor : targetColors) {
    382                     if (color == targetColor) {
    383                         horizontal[x]++;
    384                         break;
    385                     }
    386                 }
    387             }
    388         }
    389     }
    390 
    391     private static void projectPixelsToYAxis(
    392             BufferedImage img,
    393             final int[] targetColors,
    394             int[] vertical,
    395             int height,
    396             Pair<Integer, Integer> horizontalMinMax) {
    397 
    398         final int min = horizontalMinMax.first.intValue();
    399         final int max = horizontalMinMax.second.intValue();
    400 
    401         // "Flatten image" by projecting target colors (between min/max) vertically,
    402         // counting number of found pixels in each row
    403 
    404         // Pass over y-axis, restricted to horizontalMin, horizontalMax
    405         for (int y = 0; y < height; y++) {
    406             for (int x = min; x <= max; x++) {
    407                 final int color = img.getRGB(x, y);
    408                 for (final int targetColor : targetColors) {
    409                     if (color == targetColor) {
    410                         vertical[y]++;
    411                         break;
    412                     }
    413                 }
    414             }
    415         }
    416     }
    417 
    418     private static Pair<Integer, Integer> getBounds(int[] array) {
    419         // Determine min, max
    420         int min = -1;
    421         for (int i = 0; i < array.length; i++) {
    422             if (array[i] > 0) {
    423                 min = i;
    424                 break;
    425             }
    426         }
    427 
    428         int max = -1;
    429         for (int i = array.length - 1; i >= 0; i--) {
    430             if (array[i] > 0) {
    431                 max = i;
    432                 break;
    433             }
    434         }
    435 
    436         return new Pair<Integer, Integer>(Integer.valueOf(min), Integer.valueOf(max));
    437     }
    438 
    439     private static void filter(int[] array, final int threshold) {
    440         // Filter horizontal array; set all values < threshold to 0
    441         for (int i = 0; i < array.length; i++) {
    442             final int v = array[i];
    443             if (v != 0 && v <= threshold) {
    444                 array[i] = 0;
    445             }
    446         }
    447     }
    448 
    449     private static boolean boundsWithinRange(Pair<Integer, Integer> bounds, int low, int high) {
    450         return low <= bounds.first.intValue()
    451                 && bounds.first.intValue() < high
    452                 && low <= bounds.second.intValue()
    453                 && bounds.second.intValue() < high;
    454     }
    455 
    456     private static Pair<Result, String> handleError(String fmt, String tag, int arg1) {
    457         final String err = String.format(fmt, tag, arg1);
    458         CLog.w(err);
    459         return new Pair<Result, String>(Result.FAIL, err);
    460     }
    461 
    462     private static Pair<Result, String> handleError(
    463             String fmt, String tag, int arg1, int arg2, int arg3) {
    464         final String err = String.format(fmt, tag, arg1, arg2, arg3);
    465         CLog.w(err);
    466         return new Pair<Result, String>(Result.FAIL, err);
    467     }
    468 
    469     private static Pair<Result, String> handleError(
    470             String fmt, String tag, float arg1, float arg2, float arg3) {
    471         final String err = String.format(fmt, tag, arg1, arg2, arg3);
    472         CLog.w(err);
    473         return new Pair<Result, String>(Result.FAIL, err);
    474     }
    475 }
    476