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 VERTICAL_THRESHOLD = 0;
     37     private static final int PRIMARY_WAVE_COLOR = 0xFF1E4A99;
     38     private static final int SECONDARY_WAVE_COLOR = 0xFF1D4998;
     39     private static final int[] TARGET_COLORS_TABLET =
     40             new int[] {PRIMARY_WAVE_COLOR, SECONDARY_WAVE_COLOR};
     41     private static final int[] TARGET_COLORS_PHONE =
     42             new int[] {PRIMARY_WAVE_COLOR, SECONDARY_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.42f;
     62     // Only check MAX_ALLOWED_COLUMN_DECREASE up to this number
     63     private static final float MIN_NUMBER_OF_DECREASING_COLUMNS = 8;
     64     // Minimum space between two amplitude columns
     65     private static final int MIN_SPACE_BETWEEN_TWO_COLUMNS = 4;
     66     private static final int MIN_SPACE_BETWEEN_TWO_COLUMNS_TABLET = 5;
     67 
     68     enum Result {
     69         PASS,
     70         FAIL,
     71         UNKNOWN
     72     }
     73 
     74     private static class Amplitude {
     75         public int maxHeight = -1;
     76         public int zeroCounter = 0;
     77     }
     78 
     79     public static Pair<Result, String> analyzeImage(String imgFile) {
     80         final String FN_TAG = "AudioLoopbackImageAnalyzer.analyzeImage";
     81 
     82         BufferedImage img = null;
     83         try {
     84             final File f = new File(imgFile);
     85             img = ImageIO.read(f);
     86         } catch (final IOException e) {
     87             CLog.e(e);
     88             throw new RuntimeException("Error loading image file '" + imgFile + "'");
     89         }
     90 
     91         final int width = img.getWidth();
     92         final int height = img.getHeight();
     93 
     94         CLog.i("image width=" + width + ", height=" + height);
     95 
     96         // Compute thresholds and min/max values based on image witdh, height
     97         final float waveMax;
     98         final int[] targetColors;
     99         final int amplitudeCenterMaxDiff;
    100         final float maxDuration;
    101         final int minNrOfZeroesBetweenAmplitudes;
    102         final int horizontalStart; //ignore anything left of this bound
    103         int horizontalThreshold = 10;
    104 
    105         if (width >= TABLET_SCREEN_MIN_WIDTH && height >= TABLET_SCREEN_MIN_HEIGHT) {
    106             CLog.i("Apply TABLET config values");
    107             waveMax = EXPERIMENTAL_WAVE_MAX_TABLET;
    108             amplitudeCenterMaxDiff = 40;
    109             maxDuration = 5 * SECTION_WIDTH_IN_PERCENT;
    110             targetColors = TARGET_COLORS_TABLET;
    111             horizontalStart = Math.round(1.7f * SECTION_WIDTH_IN_PERCENT * width / 100.0f);
    112             horizontalThreshold = 40;
    113             minNrOfZeroesBetweenAmplitudes = MIN_SPACE_BETWEEN_TWO_COLUMNS_TABLET;
    114         } else {
    115             waveMax = EXPERIMENTAL_WAVE_MAX_PHONE;
    116             amplitudeCenterMaxDiff = 20;
    117             maxDuration = 2.5f * SECTION_WIDTH_IN_PERCENT;
    118             targetColors = TARGET_COLORS_PHONE;
    119             horizontalStart = 0;
    120             minNrOfZeroesBetweenAmplitudes = MIN_SPACE_BETWEEN_TWO_COLUMNS;
    121         }
    122 
    123         // Amplitude
    124         // Max height should be about 80% of wave max.
    125         // Min height should be about 40% of wave max.
    126         final float AMPLITUDE_MAX_VALUE = waveMax * 0.8f;
    127         final float AMPLITUDE_MIN_VALUE = waveMax * 0.4f;
    128 
    129         final int[] vertical = new int[height];
    130         final int[] horizontal = new int[width];
    131 
    132         projectPixelsToXAxis(img, targetColors, horizontal, width, height);
    133         filter(horizontal, horizontalThreshold);
    134         final Pair<Integer, Integer> durationBounds = getBounds(horizontal, horizontalStart, -1);
    135         if (!boundsWithinRange(durationBounds, 0, width)) {
    136             final String fmt = "%1$s Upper/Lower bound along horizontal axis not found";
    137             final String err = String.format(fmt, FN_TAG);
    138             CLog.w(err);
    139             return new Pair<Result, String>(Result.FAIL, err);
    140         }
    141 
    142         projectPixelsToYAxis(img, targetColors, vertical, height, durationBounds);
    143         filter(vertical, VERTICAL_THRESHOLD);
    144         final Pair<Integer, Integer> amplitudeBounds = getBounds(vertical, -1, -1);
    145         if (!boundsWithinRange(durationBounds, 0, height)) {
    146             final String fmt = "%1$s: Upper/Lower bound along vertical axis not found";
    147             final String err = String.format(fmt, FN_TAG);
    148             CLog.w(err);
    149             return new Pair<Result, String>(Result.FAIL, err);
    150         }
    151 
    152         final int durationLeft = durationBounds.first.intValue();
    153         final int durationRight = durationBounds.second.intValue();
    154         final int amplitudeTop = amplitudeBounds.first.intValue();
    155         final int amplitudeBottom = amplitudeBounds.second.intValue();
    156 
    157         final float amplitude = (amplitudeBottom - amplitudeTop) * 100.0f / height;
    158         final float duration = (durationRight - durationLeft) * 100.0f / width;
    159 
    160         CLog.i("AudioLoopbackImageAnalyzer: Amplitude=" + amplitude + ", Duration=" + duration);
    161 
    162         Pair<Result, String> amplResult =
    163                 analyzeAmplitude(
    164                         vertical,
    165                         amplitude,
    166                         amplitudeTop,
    167                         amplitudeBottom,
    168                         AMPLITUDE_MIN_VALUE,
    169                         AMPLITUDE_MAX_VALUE,
    170                         amplitudeCenterMaxDiff);
    171         if (amplResult.first != Result.PASS) {
    172             return amplResult;
    173         }
    174 
    175         amplResult =
    176                 analyzeDuration(
    177                         horizontal,
    178                         duration,
    179                         durationLeft,
    180                         durationRight,
    181                         DURATION_MIN,
    182                         maxDuration,
    183                         MIN_NUMBER_OF_COLUMNS,
    184                         minNrOfZeroesBetweenAmplitudes);
    185         if (amplResult.first != Result.PASS) {
    186             return amplResult;
    187         }
    188 
    189         return new Pair<Result, String>(Result.PASS, "");
    190     }
    191 
    192     /**
    193      * Function to analyze the waveforms duration (how wide it stretches along x-axis) and to make
    194      * sure the waveform degrades nicely, i.e. the amplitude columns becomes smaller and smaller
    195      * over time.
    196      *
    197      * @param horizontal - int array with waveforms amplitude values
    198      * @param duration - calculated length of duration in percent of screen width
    199      * @param durationLeft - index for "horizontal" where waveform starts
    200      * @param durationRight - index for "horizontal" where waveform ends
    201      * @param durationMin - if duration is below this value, return FAIL and failure reason
    202      * @param durationMax - if duration exceed this value, return FAIL and failure reason
    203      * @param minNumberOfAmplitudes - min number of amplitudes (columns) in waveform to pass test
    204      * @param minNrOfZeroesBetweenAmplitudes - min number of required zeroes between amplitudes
    205      * @return - returns result status and failure reason, if any
    206      */
    207     private static Pair<Result, String> analyzeDuration(
    208             int[] horizontal,
    209             float duration,
    210             int durationLeft,
    211             int durationRight,
    212             final float durationMin,
    213             final float durationMax,
    214             final int minNumberOfAmplitudes,
    215             final int minNrOfZeroesBetweenAmplitudes) {
    216         // This is the tricky one; basically, there should be "columns" that starts
    217         // at "durationLeft", with the tallest column to the left and then column
    218         // height will drop until it fades completely after "durationRight".
    219         final String FN_TAG = "AudioLoopbackImageAnalyzer.analyzeDuration";
    220 
    221         if (duration < durationMin || duration > durationMax) {
    222             final String fmt = "%1$s: Duration outside range, value=%2$f, range=(%3$f,%4$f)";
    223             return handleError(fmt, FN_TAG, duration, durationMin, durationMax);
    224         }
    225 
    226         final ArrayList<Amplitude> amplitudes = new ArrayList<Amplitude>();
    227         Amplitude currentAmplitude = new Amplitude();
    228         amplitudes.add(currentAmplitude);
    229         int zeroCounter = 0;
    230 
    231         // Create amplitude objects that track largest amplitude within a "group" in the array.
    232         // Example array:
    233         //      [ 202, 530, 420, 12, 0, 0, 0, 0, 0, 0, 0, 236, 423, 262, 0, 0, 0, 0, 0, 0, 0, 0 ]
    234         // We would get two amplitude objects with amplitude 530 and 423. Each amplitude object
    235         // will also get the number of zeroes to the next amplitude, i.e. 7 and 8 respectively.
    236         for (int i = durationLeft; i < durationRight; i++) {
    237             final int v = horizontal[i];
    238             if (v == 0) {
    239                 // Count how many consecutive zeroes we have
    240                 zeroCounter++;
    241                 continue;
    242             }
    243 
    244             CLog.i("index=" + i + ", v=" + v);
    245 
    246             if (zeroCounter >= minNrOfZeroesBetweenAmplitudes) {
    247                 // Found a new amplitude; update old amplitude
    248                 // with the "gap" count - i.e. nr of zeroes between the amplitudes
    249                 if (currentAmplitude != null) {
    250                     currentAmplitude.zeroCounter = zeroCounter;
    251                 }
    252 
    253                 // Create new Amplitude object
    254                 currentAmplitude = new Amplitude();
    255                 amplitudes.add(currentAmplitude);
    256             }
    257 
    258             // Reset counter
    259             zeroCounter = 0;
    260 
    261             if (currentAmplitude != null && v > currentAmplitude.maxHeight) {
    262                 currentAmplitude.maxHeight = v;
    263             }
    264         }
    265 
    266         StringBuilder sb = new StringBuilder(128);
    267         int counter = 0;
    268         for (final Amplitude a : amplitudes) {
    269             CLog.i(
    270                     sb.append("Amplitude=")
    271                             .append(counter)
    272                             .append(", MaxHeight=")
    273                             .append(a.maxHeight)
    274                             .append(", ZeroesToNextColumn=")
    275                             .append(a.zeroCounter)
    276                             .toString());
    277             counter++;
    278             sb.setLength(0);
    279         }
    280 
    281         if (amplitudes.size() < minNumberOfAmplitudes) {
    282             final String fmt = "%1$s: Not enough amplitude columns, value=%2$d";
    283             return handleError(fmt, FN_TAG, amplitudes.size());
    284         }
    285 
    286         int currentColumnHeight = -1;
    287         int oldColumnHeight = -1;
    288         for (int i = 0; i < amplitudes.size(); i++) {
    289             if (i == 0) {
    290                 oldColumnHeight = amplitudes.get(i).maxHeight;
    291                 continue;
    292             }
    293 
    294             currentColumnHeight = amplitudes.get(i).maxHeight;
    295             if (oldColumnHeight > currentColumnHeight) {
    296                 // We want at least a good number of columns that declines nicely.
    297                 // After MIN_NUMBER_OF_DECREASING_COLUMNS, we don't really care that much
    298                 if (i < MIN_NUMBER_OF_DECREASING_COLUMNS
    299                         && currentColumnHeight < (oldColumnHeight * MAX_ALLOWED_COLUMN_DECREASE)) {
    300                     final String fmt =
    301                             "%1$s: Amplitude column heights declined too much, "
    302                                     + "old=%2$d, new=%3$d, column=%4$d";
    303                     return handleError(fmt, FN_TAG, oldColumnHeight, currentColumnHeight, i);
    304                 }
    305                 oldColumnHeight = currentColumnHeight;
    306             } else if (oldColumnHeight == currentColumnHeight) {
    307                 if (i < MIN_NUMBER_OF_DECREASING_COLUMNS) {
    308                     final String fmt =
    309                             "%1$s: Amplitude column heights are same, "
    310                                     + "old=%2$d, new=%3$d, column=%4$d";
    311                     return handleError(fmt, FN_TAG, oldColumnHeight, currentColumnHeight, i);
    312                 }
    313             } else {
    314                 final String fmt =
    315                         "%1$s: Amplitude column heights don't decline, "
    316                                 + "old=%2$d, new=%3$d, column=%4$d";
    317                 return handleError(fmt, FN_TAG, oldColumnHeight, currentColumnHeight, i);
    318             }
    319         }
    320 
    321         return new Pair<Result, String>(Result.PASS, "");
    322     }
    323 
    324     /**
    325      * Function to analyze the waveforms duration (how wide it stretches along x-axis) and to make
    326      * sure the waveform degrades nicely, i.e. the amplitude columns becomes smaller and smaller
    327      * over time.
    328      *
    329      * @param vertical - integer array with waveforms amplitude accumulated values
    330      * @param amplitude - calculated height of amplitude in percent of screen height
    331      * @param amplitudeTop - index in "vertical" array where waveform starts
    332      * @param amplitudeBottom - index in "vertical" array where waveform ends
    333      * @param amplitudeMin - if amplitude is below this value, return FAIL and failure reason
    334      * @param amplitudeMax - if amplitude exceed this value, return FAIL and failure reason
    335      * @param amplitudeCenterDiffThreshold - threshold to check that waveform is centered
    336      * @return - returns result status and failure reason, if any
    337      */
    338     private static Pair<Result, String> analyzeAmplitude(
    339             int[] vertical,
    340             float amplitude,
    341             int amplitudeTop,
    342             int amplitudeBottom,
    343             final float amplitudeMin,
    344             final float amplitudeMax,
    345             final int amplitudeCenterDiffThreshold) {
    346         final String FN_TAG = "AudioLoopbackImageAnalyzer.analyzeAmplitude";
    347 
    348         if (amplitude < amplitudeMin || amplitude > amplitudeMax) {
    349             final String fmt = "%1$s: Amplitude outside range, value=%2$f, range=(%3$f,%4$f)";
    350             final String err = String.format(fmt, FN_TAG, amplitude, amplitudeMin, amplitudeMax);
    351             CLog.w(err);
    352             return new Pair<Result, String>(Result.FAIL, err);
    353         }
    354 
    355         // Are the amplitude top/bottom centered around the centerline?
    356         final int amplitudeCenter = getAmplitudeCenter(vertical, amplitudeTop, amplitudeBottom);
    357         final int topDiff = amplitudeCenter - amplitudeTop;
    358         final int bottomDiff = amplitudeBottom - amplitudeCenter;
    359         final int diff = Math.abs(topDiff - bottomDiff);
    360 
    361         if (diff < amplitudeCenterDiffThreshold) {
    362             return new Pair<Result, String>(Result.PASS, "");
    363         }
    364 
    365         final String fmt =
    366                 "%1$s: Amplitude not centered topDiff=%2$d, bottomDiff=%3$d, "
    367                         + "center=%4$d, diff=%5$d";
    368         final String err = String.format(fmt, FN_TAG, topDiff, bottomDiff, amplitudeCenter, diff);
    369         CLog.w(err);
    370         return new Pair<Result, String>(Result.FAIL, err);
    371     }
    372 
    373     private static int getAmplitudeCenter(int[] vertical, int amplitudeTop, int amplitudeBottom) {
    374         int max = -1;
    375         int center = -1;
    376         for (int i = amplitudeTop; i < amplitudeBottom; i++) {
    377             if (vertical[i] > max) {
    378                 max = vertical[i];
    379                 center = i;
    380             }
    381         }
    382 
    383         return center;
    384     }
    385 
    386     private static void projectPixelsToXAxis(
    387             BufferedImage img,
    388             final int[] targetColors,
    389             int[] horizontal,
    390             final int width,
    391             final int height) {
    392         // "Flatten image" by projecting target colors horizontally,
    393         // counting number of found pixels in each column
    394         for (int y = 0; y < height; y++) {
    395             for (int x = 0; x < width; x++) {
    396                 final int color = img.getRGB(x, y);
    397                 for (final int targetColor : targetColors) {
    398                     if (color == targetColor) {
    399                         horizontal[x]++;
    400                         break;
    401                     }
    402                 }
    403             }
    404         }
    405     }
    406 
    407     private static void projectPixelsToYAxis(
    408             BufferedImage img,
    409             final int[] targetColors,
    410             int[] vertical,
    411             int height,
    412             Pair<Integer, Integer> horizontalMinMax) {
    413 
    414         final int min = horizontalMinMax.first.intValue();
    415         final int max = horizontalMinMax.second.intValue();
    416 
    417         // "Flatten image" by projecting target colors (between min/max) vertically,
    418         // counting number of found pixels in each row
    419 
    420         // Pass over y-axis, restricted to horizontalMin, horizontalMax
    421         for (int y = 0; y < height; y++) {
    422             for (int x = min; x <= max; x++) {
    423                 final int color = img.getRGB(x, y);
    424                 for (final int targetColor : targetColors) {
    425                     if (color == targetColor) {
    426                         vertical[y]++;
    427                         break;
    428                     }
    429                 }
    430             }
    431         }
    432     }
    433 
    434     private static Pair<Integer, Integer> getBounds(int[] array, int lowerBound, int upperBound) {
    435         // Determine min, max
    436         if (lowerBound == -1) {
    437             lowerBound = 0;
    438         }
    439 
    440         if (upperBound == -1) {
    441             upperBound = array.length - 1;
    442         }
    443 
    444         int min = -1;
    445         for (int i = lowerBound; i <= upperBound; i++) {
    446             if (array[i] > 0) {
    447                 min = i;
    448                 break;
    449             }
    450         }
    451 
    452         int max = -1;
    453         for (int i = upperBound; i >= lowerBound; i--) {
    454             if (array[i] > 0) {
    455                 max = i;
    456                 break;
    457             }
    458         }
    459 
    460         return new Pair<Integer, Integer>(Integer.valueOf(min), Integer.valueOf(max));
    461     }
    462 
    463     private static void filter(int[] array, final int threshold) {
    464         // Filter horizontal array; set all values < threshold to 0
    465         for (int i = 0; i < array.length; i++) {
    466             final int v = array[i];
    467             if (v != 0 && v <= threshold) {
    468                 array[i] = 0;
    469             }
    470         }
    471     }
    472 
    473     private static boolean boundsWithinRange(Pair<Integer, Integer> bounds, int low, int high) {
    474         return low <= bounds.first.intValue()
    475                 && bounds.first.intValue() < high
    476                 && low <= bounds.second.intValue()
    477                 && bounds.second.intValue() < high;
    478     }
    479 
    480     private static Pair<Result, String> handleError(String fmt, String tag, int arg1) {
    481         final String err = String.format(fmt, tag, arg1);
    482         CLog.w(err);
    483         return new Pair<Result, String>(Result.FAIL, err);
    484     }
    485 
    486     private static Pair<Result, String> handleError(
    487             String fmt, String tag, int arg1, int arg2, int arg3) {
    488         final String err = String.format(fmt, tag, arg1, arg2, arg3);
    489         CLog.w(err);
    490         return new Pair<Result, String>(Result.FAIL, err);
    491     }
    492 
    493     private static Pair<Result, String> handleError(
    494             String fmt, String tag, float arg1, float arg2, float arg3) {
    495         final String err = String.format(fmt, tag, arg1, arg2, arg3);
    496         CLog.w(err);
    497         return new Pair<Result, String>(Result.FAIL, err);
    498     }
    499 }
    500