001    /*
002    Copyright (c) 1996-2011, Damon Hart-Davis
003    All rights reserved.
004    
005    Redistribution and use in source and binary forms, with or without
006    modification, are permitted provided that the following conditions are
007    met:
008    
009      * Redistributions of source code must retain the above copyright
010        notice, this list of conditions and the following disclaimer.
011    
012      * Redistributions in binary form must reproduce the above copyright
013        notice, this list of conditions and the following disclaimer in the
014        documentation and/or other materials provided with the
015        distribution.
016    
017    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028    */
029    
030    package org.hd.d.pg2k.ai.scorer.parameterised;
031    
032    import java.io.IOException;
033    import java.util.ArrayList;
034    import java.util.Arrays;
035    import java.util.Collections;
036    import java.util.List;
037    import java.util.Map;
038    import java.util.concurrent.Callable;
039    
040    import org.hd.d.pg2k.ai.scorer.AbstractImgSampleScorer;
041    import org.hd.d.pg2k.ai.scorer.ScoreAndConf;
042    import org.hd.d.pg2k.ai.scorer.ScorerIF;
043    import org.hd.d.pg2k.ai.scorer.ScorerParam;
044    import org.hd.d.pg2k.ai.scorer.ScorerParamInteger;
045    import org.hd.d.pg2k.svrCore.ROIntArray;
046    import org.hd.d.pg2k.svrCore.Tuple.Pair;
047    
048    /**Simple score/measure of image "exposure" over random sample points; never null.
049     * This ignores outlier values of luminance (Y/brightness),
050     * and the wider the spread of the remainder normalised to the theoretical range the better
051     * and the more confident our prediction.
052     * This does not generate negative (bad) results,
053     * just good results for those images that seem to have good exposure by this simple metric.
054     * <p>
055     * The set of sample points is a grid spaced in a regular (though not a simple grid)
056     * pattern across the image.  The layout of the sample points depends on the image
057     * size and aspect ratio, and is generated from a pseudo-random generator,
058     * to help avoid poor sampling of images with regular patterns in them.
059     * <p>
060     * For example, if having discarded the few top and bottom Y values the remainder
061     * cover 90% of the available Y range, this returns a score of 90% of MAX.
062     * <p>
063     * The confidence depends on the number of selected sample points that are available;
064     * if all those chosen are available and fully opaque (no alpha) then the confidence is 1.
065     * If no chosen sample points are available and fully opaque then the confidence is 0.
066     * Partially-opaque points may be ignored or contribute pro-rata by opacity/alpha.
067     * <p>
068     * A single-pixel transparent image should result in a result of (0, 0)
069     * ie no range of luminance (Y), and no fully-opaque sample points.
070     * <p>
071     * Very small images will have all their pixels sampled,
072     * and so can in principle still achieve a MAX confidence.
073     * <p>
074     * This scoring method should run in constant time regardless of image size
075     * since it samples at most a fixed ceiling number of image points.
076     * <p>
077     * This may be most useful for continuous-tone true-colour/greyscale (eg JPEG) images.
078     *
079     * @author dhd
080     */
081    public final class SimpleExposure extends AbstractImgSampleScorer
082        {
083        /**Create simple non-parameterised instance. */
084        public SimpleExposure()
085            {
086            // All parameters get default values.
087            sampleSizeParam = sampleSizeParamBounds;
088            brightnessParam = brightnessParamBounds;
089            outlierParam = outlierParamBounds;
090            }
091    
092        /**Create parameterised version. */
093        public SimpleExposure(final String nameAndParameters)
094            {
095            super(nameAndParameters);
096    
097            final Pair<String, Map<String, String>> nap = parseNameAndParameters(nameAndParameters);
098            final Map<String, String> paramValueMap = nap.second; // Capture the parameters.
099    
100            // Assign parameters from the captured map where possible, using default values otherwise.
101            sampleSizeParam = (ScorerParamInteger) sampleSizeParamBounds.parse(paramValueMap.get(sampleSizeParamBounds.name));
102            brightnessParam = (ScorerParamInteger) brightnessParamBounds.parse(paramValueMap.get(brightnessParamBounds.name));
103            outlierParam = (ScorerParamInteger) outlierParamBounds.parse(paramValueMap.get(outlierParamBounds.name));
104            }
105    
106        /**Create parameterised version. */
107        public SimpleExposure(final String baseName, final List<ScorerParam> parameters)
108            {
109            super(baseName, parameters);
110    
111            final Map<String, ScorerParam> paramValueMap = paramListAsMap(parameters);
112    
113            // Assign parameters from the captured map where possible, using default values otherwise.
114            sampleSizeParam = (ScorerParamInteger) sampleSizeParamBounds.extract(paramValueMap.get(sampleSizeParamBounds.name));
115            brightnessParam = (ScorerParamInteger) brightnessParamBounds.extract(paramValueMap.get(brightnessParamBounds.name));
116            outlierParam = (ScorerParamInteger) outlierParamBounds.extract(paramValueMap.get(outlierParamBounds.name));
117            }
118    
119        /**Simple non-static factory for the parameterised case. */
120        public ScorerIF createVariant(final String nameAndParameters) throws IllegalArgumentException
121            { return(new SimpleExposure(nameAndParameters)); }
122    
123        /* (non-Javadoc)
124         * @see org.hd.d.pg2k.ai.scorer.ScorerIF#createVariant(java.lang.String, java.util.List)
125         */
126        public ScorerIF createVariant(final String baseName, final List<ScorerParam> parameters) throws IllegalArgumentException
127            { return(new SimpleExposure(baseName, parameters)); }
128    
129        /**Score the image for "exposure"; never null nor negative in any component.
130         * @throws IllegalArgumentException for a null image factory
131         * @throws IllegalStateException for some problem retrieving the image
132         * @return assessment of the image, else (0,0) for a null image
133         *
134         * @throws IllegalArgumentException for a null image factory
135         * @throws IllegalStateException for some problem retrieving the image
136         * @return assessment of the image, else (0,0) for a null image
137         */
138        @Override public ScoreAndConf computeScoreAndConfidenceOnStillImagePixelSamples(final Object key, final Callable<ROIntArray> stillImagePixelSamplesFactory)
139            throws IOException
140            {
141            final ROIntArray stillImagePixelSamples = callStillImagePixelSamplesFactory(stillImagePixelSamplesFactory);
142            if(stillImagePixelSamples == null) { return(ScoreAndConf.NO_OPINION); }
143    
144            // Compute the target number of sample points (strictly positive).
145            final int targetSamplePoints = (1 << sampleSizeParam.value);
146            assert(targetSamplePoints > 0);
147    
148            // Set of brightness values sampled from the image.
149            // Assumed to be sortable, ie all values finite.
150            final int availableSamples = stillImagePixelSamples.length();
151            // If an image has fewer pixels than our putative sample size then cap our target to match.
152            final int adjustedTargetSamplePoints = Math.min(availableSamples, targetSamplePoints);
153            final ArrayList<Byte> pointsSampled = new ArrayList<Byte>(adjustedTargetSamplePoints);
154            // Attempt to collect the target number of samples,
155            // but scan at most twice the required sample points to exact suitably opaque ones
156            // to avoid getting a wildly skewed set of samples from mainly-transparent images.
157            for(int i = Math.min(availableSamples, 2*targetSamplePoints); (--i >= 0) && (pointsSampled.size() < adjustedTargetSamplePoints); )
158                {
159                final int argb = stillImagePixelSamples.get(i);
160    
161                // Skip mainly-transparent points...
162                if(!rejectMainlyTransparentPoints.accept(argb)) { continue; }
163    
164                // Convert from sRGB to brightness/luminance/Y value in range [0,127].
165    
166                // Extract r g b components in the range [0,255].
167                final int r = (argb >>> 16) & 0xff;
168                final int g = (argb >>>  8) & 0xff;
169                final int b =  argb         & 0xff;
170    
171                // Compute B of HSB model.
172                if(USE_B_NOT_Y)
173                    {
174                    final int cmax = Math.max(r, Math.max(g, b));
175                    final int brByte = cmax/2;
176                    assert((brByte >= 0) && (brByte <= MAX_BRIGHTNESS));
177                    pointsSampled.add(Byte.valueOf((byte) brByte)); // Does not create new instances.
178                    continue;
179                    }
180    
181                // Compute Y of YUV model (as used in PAL TV encodings).
182                // Approx .299r + .587g + .114b, but mapping from [0-255] inputs to [0,127] result.
183                final int Y = (37 * r + 74 * g + 14 * b + 128) >> 8;
184                assert((Y >= 0) && (Y <= MAX_BRIGHTNESS));
185                pointsSampled.add(Byte.valueOf((byte) Y)); // Does not create new instances.
186                }
187    
188            // Actual samples extracted (after filtering).
189            final int nSamples = pointsSampled.size();
190    
191            // If we are to discard any outliers
192            // and have at least two points left to compute the range with
193            // then we must have collected at least 4 samples.
194            // If we don't have at least this many, return a "no confidence" result.
195            if(nSamples < 4) { return(ScoreAndConf.NO_OPINION); }
196            // Compute number of outliers to discard from either extreme.
197            final int nOutliers = Math.max(1, nSamples / (1 << outlierParam.value));
198            assert(nSamples - 2*nOutliers >= 2);
199    
200            // Sort the sampled points.
201            // Discard/ignore outliers from either end of the range, likely to be noise.
202            // Compute the relative interval between the remaining extremes to compute the score.
203            // The confidence is based on the sample size (relative to our target sample size) and the score.
204            Collections.sort(pointsSampled);
205            final int lowest = pointsSampled.get(nOutliers).intValue();
206            final int highest = pointsSampled.get(nSamples-1 - nOutliers).intValue();
207    //System.out.println("SimpleExposure: low/high = "+lowest+"/"+highest + "; lowest/highest = "+pointsSampled.get(0)+"/"+pointsSampled.get(nSamples-1));
208            // Note that brightness is in range [0,MAX_BRIGHTNESS] so does needs normalisation.
209            // We regard any image which covers a reasonable chunk of the luminance range to be fine.
210            final int score = Math.max(0, Math.min(ScoreAndConf.MAX, Math.round((ScoreAndConf.MAX * (highest-lowest) * 100) / (MAX_BRIGHTNESS * brightnessParam.value))));
211            final int confidence = Math.max(0, Math.min(ScoreAndConf.MAX, (int) Math.round(score * Math.sqrt(nSamples / (double) adjustedTargetSamplePoints))));
212    
213            return(new ScoreAndConf(score, confidence));
214            }
215    
216        /**Max brightness returned by samplePoint (minimum is 0); strictly positive. */
217        private static final byte MAX_BRIGHTNESS = 127;
218    
219        /**If true then use the B of the HSB model (max of R, G and B), else use the weighted Y value from YUV. */
220        private static final boolean USE_B_NOT_Y = false;
221    
222        /**Min, default and max shift (power-of-two) for the target sample size; never null.
223         * This has a bias to lower values since this results in less work done.
224         */
225        private static final ScorerParamInteger sampleSizeParamBounds =
226            ScorerParamInteger.createScorerParamInteger(2, 7, MAX_SAMPLE_SIZE_POWER, 1, true, "sampleSizePower");
227    
228        /**Target sample size for this instance; never null.
229         * Target number of sample points; strictly positive.
230         * If fewer sample points than this are available,
231         * then the result's confidence will drop
232         * in accordance with expected sample-variance noise reduction.
233         * <p>
234         * Reducing the sample size by a factor of x is assumed to increase the noise,
235         * and thus reduce the confidence, by a factor of sqrt(x).
236         * <p>
237         * Experience suggests that upwards of a thousand samples gives good consistency.
238         */
239        private final ScorerParamInteger sampleSizeParam;
240    
241        /**Min, default and max percentage of Y range that should be spanned for MAX score; never null. */
242        private static final ScorerParamInteger brightnessParamBounds =
243            ScorerParamInteger.createScorerParamInteger(3, 65, 100, 5, false, "brightnessRangePercent");
244    
245        /**Percentage brightness range that should be spanned for MAX score for this instance; never null. */
246        private final ScorerParamInteger brightnessParam;
247    
248        /**Min, default and max shift for the outlier fraction to remove; never null.
249         * Shift to get fraction/portion of the samples that are discarded from either end as potential noise; 3 or greater.
250         * A larger value discards fewer samples/values,
251         * but makes the measurement more sensitive to noise/grain and small highlights (etc).
252         * We always trim at least one outlier at each end.
253         * <p>
254         * A good shift value may line in the region of 3 to 7
255         * thus discarding ~13% to ~1% of the outliers from either extreme.
256         */
257        private static final ScorerParamInteger outlierParamBounds =
258            ScorerParamInteger.createScorerParamInteger(2, 6, 10, "outlierFractionPower");
259    
260        /**Outlier parameter value for this instance; never null. */
261        private final ScorerParamInteger outlierParam;
262    
263        /**Get parameter definitions and values (immutable) for this Scorer; never null. */
264        @Override
265        public List<ScorerParam> getParameterDefsAndValues()
266            {
267            return(Collections.unmodifiableList(Arrays.asList(new ScorerParam[] {
268                    sampleSizeParam, brightnessParam, outlierParam
269                })));
270            }
271        }