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;
031
032 import java.awt.Point;
033 import java.awt.image.BufferedImage;
034 import java.awt.image.RenderedImage;
035 import java.io.IOException;
036 import java.util.Arrays;
037 import java.util.HashSet;
038 import java.util.List;
039 import java.util.Random;
040 import java.util.Set;
041 import java.util.concurrent.Callable;
042
043 import org.hd.d.pg2k.svrCore.ImageUtils;
044 import org.hd.d.pg2k.svrCore.MemoryTools;
045 import org.hd.d.pg2k.svrCore.MemoryTools.SoftReferenceMap;
046 import org.hd.d.pg2k.svrCore.ROIntArray;
047
048
049 /**Base interface to compute the score and confidence for a 2D still image using pixel ARGB sampling.
050 * The run-time and memory (etc) consumption of these sampling image Scores should be bounded
051 * by the upper limit on sample-set size, and thus potentially lower than a generic image Scorer.
052 * <p>
053 * All methods in this interface are guaranteed to be "safe"
054 * in so far as they will complete in "reasonable" time
055 * with reasonable heap memory (and other resource, eg stack)
056 * and without doing anything that wouldn't be allowed in
057 * a minimal Applet/JWS sandbox.
058 * <p>
059 * All classes implementing this interface should be completely thread-safe
060 * in their implementation of the computeScoreAndConfidence() method,
061 * and preferably purely functional (no visible side-effects),
062 * with as many concurrent threads as required safely doing separate computations
063 * in any one instance.
064 * <p>
065 * Classes implementing this interface should, where possible,
066 * do their calculations using integer arithmetic,
067 * since FPUs to support float/double calculations may be a scarce resource
068 * on newer highly-threaded CPUs such as Sun's Niagara.
069 * <p>
070 * TODO: At least tie the cache to the AEP/pipeline, not static.
071 */
072 public abstract class AbstractImgSampleScorer extends AbstractImgScorer implements ScorerIF
073 {
074 /**Create simple non-parameterised instance. */
075 public AbstractImgSampleScorer() { }
076
077 /**Create parameterised version. */
078 public AbstractImgSampleScorer(final String nameAndParameters)
079 { super(nameAndParameters); }
080
081 /**Create parameterised version. */
082 public AbstractImgSampleScorer(final String baseName, final List<ScorerParam> parameters)
083 { super(baseName, parameters); }
084
085
086 /**Private static 'soft' cache from unique exhibit identifier to pixel sample.
087 * This exists to avoid having to unpack, decode and sample thumbnail images repeatedly.
088 * <p>
089 * The samples are held via SoftReferences to allow them to be ditched
090 * in case of memory shortage, and when individual images stop being used.
091 * <p>
092 * The cache key is the (full) exhibit name plus other static attributes
093 * on the basis that thumbnails hardly ever change once created.
094 * This also means that we will stop hitting any thumbnail caches
095 * in the data pipeline once we have fetched a thumbnail once.
096 * <p>
097 * This cache is thread-safe and highly concurrent.
098 * <p>
099 * This cache is shared between all derived image Scorer classes.
100 * <p>
101 * TODO: Deal with the case when a thumbnail IS changed/improved.
102 */
103 private static final SoftReferenceMap<Object, ROIntArray> _sampleCache = SoftReferenceMap.<Object, ROIntArray>create(16, true, "_sampleCache");
104
105 /**Call the factory to retrieve a still image, handling wrapped IOExceptions and timeouts specially.
106 * @throws IOException if the factory threw IOException
107 * @throws IllegalStateException if the factory throws something unrecognised
108 */
109 protected static ROIntArray callStillImagePixelSamplesFactory(final Callable<ROIntArray> stillImagePixelSamplesFactory)
110 throws IOException
111 {
112 if(stillImagePixelSamplesFactory == null) { throw new IllegalArgumentException(); }
113 try { return(stillImagePixelSamplesFactory.call()); }
114 catch(final IOException e) { throw e; }
115 catch(final Exception e) { throw new IllegalStateException("unable to retrieve image pixel samples", e); }
116 }
117
118 /**Convert the RenderedImage to pixel samples for our derived classes.
119 * This may cache the underlying samples.
120 * <p>
121 * This is final to prevent a derived sampling Scorer from getting at this directly.
122 *
123 * @throws IllegalArgumentException for a null image
124 */
125 @Override public final ScoreAndConf computeScoreAndConfidenceOnStillImage(final Object key, final Callable<RenderedImage> stillImageFactory)
126 throws IOException
127 {
128 if(stillImageFactory == null) { throw new IllegalArgumentException(); }
129
130 // Create the means to fetch samples from the still image.
131 final Callable<ROIntArray> sampleFactory = new Callable<ROIntArray>() {
132 /**Return the image pixel samples, or null if not possible (eg because the exhibit is not an image). */
133 public ROIntArray call() throws Exception
134 {
135 // Return from cache if present.
136 final ROIntArray cached = (key == null) ? null : _sampleCache.get(key);
137 //if(IsDebug.isDebug && (null != cached)) { System.out.println("pixel sample cache HIT for "+key); }
138 if(null != cached) { return(cached); }
139 //if(IsDebug.isDebug) { System.out.println("pixel sample cache MISS for "+key); }
140 // Else compute vanilla maximum-size sample set, cache it, and return.
141 final RenderedImage stillImage = stillImageFactory.call();
142 if(stillImage == null) { return(null); }
143 final ROIntArray wrappedSample = new ROIntArray(getUnfilteredSamplePoints(stillImage, 1 << MAX_SAMPLE_SIZE_POWER));
144 // Cache this value if we have a key and memory is not horribly stressed.
145 if((key != null) && !MemoryTools.isMemoryStressed())
146 {
147 _sampleCache.put(key, wrappedSample);
148 assert(_sampleCache.get(key) != null) : "Our value (or possibly a newer one) must now be present";
149 }
150 return(wrappedSample);
151 }
152 };
153
154 // Invoke Scorer algorithm on the samples.
155 return(computeScoreAndConfidenceOnStillImagePixelSamples(key, sampleFactory));
156 }
157
158
159 /**Compute score [-1,+1] and confidence[0,+1] for given image; never null.
160 * This is made public mainly so as to facilitate testing.
161 * <p>
162 * Note that the underlying factory may legitimately throw IOException, especially InterruptedIOException,
163 * in case of transient problems that indicate that a retry should probably be attempted later.
164 *
165 * @throws IllegalArgumentException for a null image factory
166 * @throws IllegalStateException for some problem retrieving the image
167 * @return assessment of the image, else (0,0) for a null image
168 *
169 * @param key unique key identifying image for cacheing intermediate results for example;
170 * if null then this will not be cached
171 * @param stillImage non-null ARGB pixel value samples from a still 2D image
172 */
173 public abstract ScoreAndConf computeScoreAndConfidenceOnStillImagePixelSamples(Object key, Callable<ROIntArray> stillImagePixelSamplesFactory) throws IOException;
174
175 /**Power of two of (hard) limit on maximum sample size for these image samplers; strictly positive.
176 * Sample sizes up to this may be more efficiently handled/cached by getSamplePoints();
177 * above this threshold results may simply be capped in size.
178 */
179 public static final int MAX_SAMPLE_SIZE_POWER = 12;
180
181 /**Collect a set of unfiltered ARGB samples from the image.
182 * This may return less than the number of samples requested,
183 * eg because the image does not have that many pixels
184 * or because we are rejecting 'transparent' points.
185 * <p>
186 * All samples returned are from unique pixels in the source image.
187 * <p>
188 * If the number of points requested greater than or equal to the number of image pixels
189 * then all pixels are 'sampled' and returned,
190 * else a pseudo-random (but consistent) sample set is returned.
191 * <p>
192 * The order of sample pixels is explicitly undefined
193 * since each sample should be treated independently of its position in the sample set.
194 * <p>
195 * This is made public mainly to assist with testing.
196 *
197 * @param stillImage still image (eg thumbnail) to sample pixels from; never null nor zero-sized
198 * @param maxSamplePoints maximum number of samples to collect; strictly positive
199 */
200 public static int[] getUnfilteredSamplePoints(final RenderedImage stillImage,
201 final int maxSamplePoints)
202 {
203 if(stillImage == null) { throw new IllegalArgumentException(); }
204 if(maxSamplePoints <= 0) { throw new IllegalArgumentException(); }
205
206 final int h = stillImage.getHeight();
207 final int w = stillImage.getWidth();
208
209 // Don't try to score strange (eg zero-sized) images.
210 if((h < 1) || (w < 1)) { throw new IllegalArgumentException(); }
211
212 final long availablePixels = h * w;
213 assert(availablePixels > 0);
214
215 final boolean smallImage = (availablePixels <= maxSamplePoints);
216 final int maxSampleablePoints = smallImage ? (int) availablePixels : maxSamplePoints;
217
218 // final List<Integer> results = new ArrayList<Integer>(maxSampleablePoints);
219 final int results[] = new int[maxSampleablePoints];
220 int resultCount = 0;
221
222 final int minX = stillImage.getMinX();
223 final int minY = stillImage.getMinY();
224
225 // For sampling we'll need a buffered image;
226 // so we copy the input image if need be.
227 final BufferedImage bi = ImageUtils.convertRenderedImageToBufferedImage(stillImage);
228
229 // For small images sample all pixels.
230 if(smallImage)
231 {
232 for(int x = w; --x >= 0; )
233 {
234 for(int y = h; --y >= 0; )
235 {
236 // Get sample value.
237 final int argb = bi.getRGB(x + minX, y + minY);
238
239 // Reject/filter pixels if requested.
240 // if((filter != null) && !filter.accept(argb)) { continue; }
241
242 // Capture sample.
243 results[resultCount++] = argb;
244 }
245 }
246 }
247 // For reasonable-size (not tiny) images,
248 // we sample pixels in a pseudo-random pattern based on image size/shape.
249 else
250 {
251 // Make the sample pattern (random seed) dependent on image size/shape
252 // to help circumvent some systematic sample pattern problems.
253 // We do not make the sample pattern/seed dependent on sample size
254 // so that as we take more samples all previous values are included.
255 final Random r = new Random(w ^ (((long)h) << 16));
256
257 // To allow in the main for non-square partially-transparent images
258 // but also for images close in pixels to our threshold size
259 // (so that we capture as many of the samplable points as reasonable possible)
260 // we are prepared to attempt the sampling operation
261 // more than the target number of samples
262 // (though we stop as soon as our target sample count is reached).
263 // We also reject would-be duplicate sampling of the same point.
264 assert((new Point(0, 0)).equals(new Point(0, 0)));
265 final Set<Point> alreadySampled = new HashSet<Point>(1 + 2*maxSampleablePoints);
266 for(int i = 2*maxSampleablePoints; (--i >= 0) && (alreadySampled.size() < maxSampleablePoints); )
267 {
268 // Choose a (new) random point and take a sample.
269 final int x = r.nextInt(w);
270 final int y = r.nextInt(h);
271 final Point p = new Point(x, y);
272 if(!alreadySampled.add(p)) { continue; } // Skip if point not new...
273 //System.out.println("[Sample point: "+p+".]");
274
275 // Get sample value.
276 final int argb = bi.getRGB(x + minX, y + minY);
277
278 // Reject/filter pixels if requested.
279 // if((filter != null) && !filter.accept(argb)) { continue; }
280
281 // Capture sample.
282 results[resultCount++] = argb;
283 }
284 }
285
286 assert(resultCount <= maxSamplePoints);
287
288 // If all requested samples were generated then return as-is...
289 if(resultCount == results.length) { return(results); }
290
291 // Else trim to size and return.
292 return(Arrays.copyOf(results, resultCount));
293 }
294 }