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        }