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 }