From e7a3847514a8a1b7ea2f5f22691f5b310f03a8fd Mon Sep 17 00:00:00 2001 From: Damian Lesiuk Date: Thu, 26 Oct 2017 13:14:33 +0200 Subject: [PATCH 01/12] optimized imports --- .../com/pragone/jphash/image/SimpleGrayscaleImage.java | 7 ------- .../pragone/jphash/image/radial/RadialHashAlgorithm.java | 1 - .../pragone/jphash/pruningconetree/PruningConeTree2.java | 4 +++- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java b/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java index 7444274..966afc4 100644 --- a/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java +++ b/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java @@ -1,17 +1,10 @@ package com.pragone.jphash.image; -import sun.jvm.hotspot.debugger.Debugger; -import sun.misc.Cleaner; - import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.Map; public class SimpleGrayscaleImage { private static final int BYTE_SIZE = 8; diff --git a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java index 5b95264..66658af 100644 --- a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java +++ b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java @@ -1,7 +1,6 @@ package com.pragone.jphash.image.radial; import com.pragone.jphash.image.SimpleGrayscaleImage; -import com.pragone.jphash.image.UnsafeSimpleGrayscaleImage; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; diff --git a/src/main/java/com/pragone/jphash/pruningconetree/PruningConeTree2.java b/src/main/java/com/pragone/jphash/pruningconetree/PruningConeTree2.java index 0451060..5ed5c62 100644 --- a/src/main/java/com/pragone/jphash/pruningconetree/PruningConeTree2.java +++ b/src/main/java/com/pragone/jphash/pruningconetree/PruningConeTree2.java @@ -5,7 +5,9 @@ import com.pragone.jphash.index.SearchIndex; import com.pragone.jphash.index.Vector; -import java.util.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; From e818dafc524f3808bf88e3de664fdd50efe761fb Mon Sep 17 00:00:00 2001 From: Damian Lesiuk Date: Sun, 29 Oct 2017 14:00:35 +0100 Subject: [PATCH 02/12] creates image input stream --- .../com/pragone/jphash/image/radial/RadialHashAlgorithm.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java index 66658af..7194c22 100644 --- a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java +++ b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java @@ -38,12 +38,12 @@ public static RadialHash getHash(File file) throws IOException { try { return getHash(is); } finally { - is.close();; + is.close(); } } public static RadialHash getHash(InputStream inputStream) throws IOException { - BufferedImage img = ImageIO.read(inputStream); + BufferedImage img = ImageIO.read(ImageIO.createImageInputStream(inputStream)); return getHash(img); } From 516bcc11dcb69c936b3530530f72822ba87512dc Mon Sep 17 00:00:00 2001 From: Damian Lesiuk Date: Sun, 29 Oct 2017 14:36:13 +0100 Subject: [PATCH 03/12] less memory usage --- pom.xml | 7 + .../jphash/image/BigBufferedImage.java | 299 ++++++++++++++++++ .../image/radial/RadialHashAlgorithm.java | 14 +- src/main/java/com/pragone/jphash/jpHash.java | 2 +- 4 files changed, 311 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/pragone/jphash/image/BigBufferedImage.java diff --git a/pom.xml b/pom.xml index 41ca467..629aa76 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,13 @@ + + maven-compiler-plugin + + 1.7 + 1.7 + + org.apache.maven.plugins maven-surefire-plugin diff --git a/src/main/java/com/pragone/jphash/image/BigBufferedImage.java b/src/main/java/com/pragone/jphash/image/BigBufferedImage.java new file mode 100644 index 0000000..ca1d3a6 --- /dev/null +++ b/src/main/java/com/pragone/jphash/image/BigBufferedImage.java @@ -0,0 +1,299 @@ +package com.pragone.jphash.image; + +/* + * This class is part of MCFS (Mission Control - Flight Software) a development + * of Team Puli Space, official Google Lunar XPRIZE contestant. + * This class is released under Creative Commons CC0. + * @author Zsolt Pocze, Dimitry Polivaev + * Please like us on facebook, and/or join our Small Step Club. + * http://www.pulispace.com + * https://www.facebook.com/pulispace + * http://nyomdmegteis.hu/en/ + */ +import sun.nio.ch.DirectBuffer; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.image.*; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.util.*; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class BigBufferedImage extends BufferedImage { + + private static final String TMP_DIR = System.getProperty("java.io.tmpdir"); + public static final int MAX_PIXELS_IN_MEMORY = 1024 * 1024; + + public static BufferedImage create(int width, int height, int imageType) { + if (width * height > MAX_PIXELS_IN_MEMORY) { + try { + final File tempDir = new File(TMP_DIR); + return createBigBufferedImage(tempDir, width, height, imageType); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + return new BufferedImage(width, height, imageType); + } + } + + public static BufferedImage create(File inputFile, int imageType) throws IOException { + try (ImageInputStream stream = ImageIO.createImageInputStream(inputFile);) { + Iterator readers = ImageIO.getImageReaders(stream); + if (readers.hasNext()) { + try { + ImageReader reader = readers.next(); + reader.setInput(stream, true, true); + int width = reader.getWidth(reader.getMinIndex()); + int height = reader.getHeight(reader.getMinIndex()); + BufferedImage image = create(width, height, imageType); + int cores = Math.max(1, Runtime.getRuntime().availableProcessors() / 2); + int block = Math.min(MAX_PIXELS_IN_MEMORY / cores / width, (int) (Math.ceil(height / (double) cores))); + ExecutorService generalExecutor = Executors.newFixedThreadPool(cores); + List> partLoaders = new ArrayList<>(); + for (int y = 0; y < height; y += block) { + partLoaders.add(new ImagePartLoader( + y, width, Math.min(block, height - y), inputFile, image)); + } + generalExecutor.invokeAll(partLoaders); + generalExecutor.shutdown(); + return image; + } catch (InterruptedException ex) { + Logger.getLogger(BigBufferedImage.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + return null; + } + + private static BufferedImage createBigBufferedImage(File tempDir, int width, int height, int imageType) + throws FileNotFoundException, IOException { + FileDataBuffer buffer = new FileDataBuffer(tempDir, width * height, 4); + ColorModel colorModel = null; + BandedSampleModel sampleModel = null; + switch (imageType) { + case TYPE_INT_RGB: + colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + new int[]{8, 8, 8, 0}, + false, + false, + ComponentColorModel.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + sampleModel = new BandedSampleModel(DataBuffer.TYPE_BYTE, width, height, 3); + break; + case TYPE_INT_ARGB: + colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + new int[]{8, 8, 8, 8}, + true, + false, + ComponentColorModel.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + sampleModel = new BandedSampleModel(DataBuffer.TYPE_BYTE, width, height, 4); + break; + default: + throw new IllegalArgumentException("Unsupported image type: " + imageType); + } + SimpleRaster raster = new SimpleRaster(sampleModel, buffer, new Point(0, 0)); + BigBufferedImage image = new BigBufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), null); + return image; + } + + private static class ImagePartLoader implements Callable { + + private final int y; + private final BufferedImage image; + private final Rectangle region; + private final File file; + + public ImagePartLoader(int y, int width, int height, File file, BufferedImage image) { + this.y = y; + this.image = image; + this.file = file; + region = new Rectangle(0, y, width, height); + } + + @Override + public ImagePartLoader call() throws Exception { + Thread.currentThread().setPriority((Thread.MIN_PRIORITY + Thread.NORM_PRIORITY) / 2); + try (ImageInputStream stream = ImageIO.createImageInputStream(file);) { + Iterator readers = ImageIO.getImageReaders(stream); + if (readers.hasNext()) { + ImageReader reader = readers.next(); + reader.setInput(stream, true, true); + ImageReadParam param = reader.getDefaultReadParam(); + param.setSourceRegion(region); + BufferedImage part = reader.read(0, param); + Raster source = part.getRaster(); + WritableRaster target = image.getRaster(); + target.setRect(0, y, source); + } + } + return ImagePartLoader.this; + } + } + + private BigBufferedImage(ColorModel cm, SimpleRaster raster, boolean isRasterPremultiplied, Hashtable properties) { + super(cm, raster, isRasterPremultiplied, properties); + } + + public void dispose() { + ((SimpleRaster) getRaster()).dispose(); + } + + public static void dispose(RenderedImage image) { + if (image instanceof BigBufferedImage) { + ((BigBufferedImage) image).dispose(); + } + } + + private static class SimpleRaster extends WritableRaster { + + public SimpleRaster(SampleModel sampleModel, FileDataBuffer dataBuffer, Point origin) { + super(sampleModel, dataBuffer, origin); + } + + public void dispose() { + ((FileDataBuffer) getDataBuffer()).dispose(); + } + + } + + private static final class FileDataBufferDeleterHook extends Thread { + + static { + Runtime.getRuntime().addShutdownHook(new FileDataBufferDeleterHook()); + } + + private static final HashSet undisposedBuffers = new HashSet<>(); + + @Override + public void run() { + final FileDataBuffer[] buffers = undisposedBuffers.toArray(new FileDataBuffer[0]); + for (FileDataBuffer b : buffers) { + b.disposeNow(); + } + } + } + + private static class FileDataBuffer extends DataBuffer { + + private final String id = "buffer-" + System.currentTimeMillis() + "-" + ((int) (Math.random() * 1000)); + private File dir; + private String path; + private File[] files; + private RandomAccessFile[] accessFiles; + private MappedByteBuffer[] buffer; + + public FileDataBuffer(File dir, int size) throws FileNotFoundException, IOException { + super(TYPE_BYTE, size); + this.dir = dir; + init(); + } + + public FileDataBuffer(File dir, int size, int numBanks) throws FileNotFoundException, IOException { + super(TYPE_BYTE, size, numBanks); + this.dir = dir; + init(); + } + + private void init() throws FileNotFoundException, IOException { + FileDataBufferDeleterHook.undisposedBuffers.add(this); + if (dir == null) { + dir = new File("."); + } + if (!dir.exists()) { + throw new RuntimeException("FileDataBuffer constructor parameter dir does not exist: " + dir); + } + if (!dir.isDirectory()) { + throw new RuntimeException("FileDataBuffer constructor parameter dir is not a directory: " + dir); + } + path = dir.getPath() + "/" + id; + File subDir = new File(path); + subDir.mkdir(); + buffer = new MappedByteBuffer[banks]; + accessFiles = new RandomAccessFile[banks]; + files = new File[banks]; + for (int i = 0; i < banks; i++) { + File file = files[i] = new File(path + "/bank" + i + ".dat"); + final RandomAccessFile randomAccessFile = accessFiles[i] = new RandomAccessFile(file, "rw"); + buffer[i] = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, getSize()); + } + } + + @Override + public int getElem(int bank, int i) { + return buffer[bank].get(i) & 0xff; + } + + @Override + public void setElem(int bank, int i, int val) { + buffer[bank].put(i, (byte) val); + } + + @Override + protected void finalize() throws Throwable { + dispose(); + } + + private void disposeNow() { + final MappedByteBuffer[] disposedBuffer = this.buffer; + this.buffer = null; + disposeNow(disposedBuffer); + } + + public void dispose() { + final MappedByteBuffer[] disposedBuffer = this.buffer; + this.buffer = null; + new Thread() { + @Override + public void run() { + disposeNow(disposedBuffer); + } + }.start(); + } + + private void disposeNow(final MappedByteBuffer[] disposedBuffer) { + FileDataBufferDeleterHook.undisposedBuffers.remove(this); + if (disposedBuffer != null) { + for (MappedByteBuffer b : disposedBuffer) { + ((DirectBuffer) b).cleaner().clean(); + } + } + if (accessFiles != null) { + for (RandomAccessFile file : accessFiles) { + try { + file.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + accessFiles = null; + } + if (files != null) { + for (File file : files) { + file.delete(); + } + files = null; + } + if (path != null) { + new File(path).delete(); + path = null; + } + } + + } +} \ No newline at end of file diff --git a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java index 7194c22..7103756 100644 --- a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java +++ b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java @@ -1,5 +1,6 @@ package com.pragone.jphash.image.radial; +import com.pragone.jphash.image.BigBufferedImage; import com.pragone.jphash.image.SimpleGrayscaleImage; import javax.imageio.ImageIO; @@ -29,19 +30,12 @@ public class RadialHashAlgorithm { } } - public static RadialHash getHash(String file) throws IOException { - return getHash(new File(file)); - } - public static RadialHash getHash(File file) throws IOException { - FileInputStream is = new FileInputStream(file); - try { - return getHash(is); - } finally { - is.close(); - } + BufferedImage img = BigBufferedImage.create(file, BigBufferedImage.TYPE_INT_RGB); + return getHash(img); } + public static RadialHash getHash(InputStream inputStream) throws IOException { BufferedImage img = ImageIO.read(ImageIO.createImageInputStream(inputStream)); return getHash(img); diff --git a/src/main/java/com/pragone/jphash/jpHash.java b/src/main/java/com/pragone/jphash/jpHash.java index a391165..91fa926 100644 --- a/src/main/java/com/pragone/jphash/jpHash.java +++ b/src/main/java/com/pragone/jphash/jpHash.java @@ -15,7 +15,7 @@ */ public class jpHash { public static RadialHash getImageRadialHash(String path) throws IOException { - return RadialHashAlgorithm.getHash(path); + return RadialHashAlgorithm.getHash(new File(path)); } public static RadialHash getImageRadialHash(File file) throws IOException { return RadialHashAlgorithm.getHash(file); From f17ae15cc47e24432384c2519544f7df14c6a491 Mon Sep 17 00:00:00 2001 From: Damian Lesiuk Date: Sun, 29 Oct 2017 14:40:46 +0100 Subject: [PATCH 04/12] Revert "less memory usage" This reverts commit 516bcc11dcb69c936b3530530f72822ba87512dc. --- pom.xml | 7 - .../jphash/image/BigBufferedImage.java | 299 ------------------ .../image/radial/RadialHashAlgorithm.java | 14 +- src/main/java/com/pragone/jphash/jpHash.java | 2 +- 4 files changed, 11 insertions(+), 311 deletions(-) delete mode 100644 src/main/java/com/pragone/jphash/image/BigBufferedImage.java diff --git a/pom.xml b/pom.xml index 629aa76..41ca467 100644 --- a/pom.xml +++ b/pom.xml @@ -19,13 +19,6 @@ - - maven-compiler-plugin - - 1.7 - 1.7 - - org.apache.maven.plugins maven-surefire-plugin diff --git a/src/main/java/com/pragone/jphash/image/BigBufferedImage.java b/src/main/java/com/pragone/jphash/image/BigBufferedImage.java deleted file mode 100644 index ca1d3a6..0000000 --- a/src/main/java/com/pragone/jphash/image/BigBufferedImage.java +++ /dev/null @@ -1,299 +0,0 @@ -package com.pragone.jphash.image; - -/* - * This class is part of MCFS (Mission Control - Flight Software) a development - * of Team Puli Space, official Google Lunar XPRIZE contestant. - * This class is released under Creative Commons CC0. - * @author Zsolt Pocze, Dimitry Polivaev - * Please like us on facebook, and/or join our Small Step Club. - * http://www.pulispace.com - * https://www.facebook.com/pulispace - * http://nyomdmegteis.hu/en/ - */ -import sun.nio.ch.DirectBuffer; - -import javax.imageio.ImageIO; -import javax.imageio.ImageReadParam; -import javax.imageio.ImageReader; -import javax.imageio.stream.ImageInputStream; -import java.awt.*; -import java.awt.color.ColorSpace; -import java.awt.image.*; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.util.*; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class BigBufferedImage extends BufferedImage { - - private static final String TMP_DIR = System.getProperty("java.io.tmpdir"); - public static final int MAX_PIXELS_IN_MEMORY = 1024 * 1024; - - public static BufferedImage create(int width, int height, int imageType) { - if (width * height > MAX_PIXELS_IN_MEMORY) { - try { - final File tempDir = new File(TMP_DIR); - return createBigBufferedImage(tempDir, width, height, imageType); - } catch (IOException e) { - throw new RuntimeException(e); - } - } else { - return new BufferedImage(width, height, imageType); - } - } - - public static BufferedImage create(File inputFile, int imageType) throws IOException { - try (ImageInputStream stream = ImageIO.createImageInputStream(inputFile);) { - Iterator readers = ImageIO.getImageReaders(stream); - if (readers.hasNext()) { - try { - ImageReader reader = readers.next(); - reader.setInput(stream, true, true); - int width = reader.getWidth(reader.getMinIndex()); - int height = reader.getHeight(reader.getMinIndex()); - BufferedImage image = create(width, height, imageType); - int cores = Math.max(1, Runtime.getRuntime().availableProcessors() / 2); - int block = Math.min(MAX_PIXELS_IN_MEMORY / cores / width, (int) (Math.ceil(height / (double) cores))); - ExecutorService generalExecutor = Executors.newFixedThreadPool(cores); - List> partLoaders = new ArrayList<>(); - for (int y = 0; y < height; y += block) { - partLoaders.add(new ImagePartLoader( - y, width, Math.min(block, height - y), inputFile, image)); - } - generalExecutor.invokeAll(partLoaders); - generalExecutor.shutdown(); - return image; - } catch (InterruptedException ex) { - Logger.getLogger(BigBufferedImage.class.getName()).log(Level.SEVERE, null, ex); - } - } - } - return null; - } - - private static BufferedImage createBigBufferedImage(File tempDir, int width, int height, int imageType) - throws FileNotFoundException, IOException { - FileDataBuffer buffer = new FileDataBuffer(tempDir, width * height, 4); - ColorModel colorModel = null; - BandedSampleModel sampleModel = null; - switch (imageType) { - case TYPE_INT_RGB: - colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), - new int[]{8, 8, 8, 0}, - false, - false, - ComponentColorModel.TRANSLUCENT, - DataBuffer.TYPE_BYTE); - sampleModel = new BandedSampleModel(DataBuffer.TYPE_BYTE, width, height, 3); - break; - case TYPE_INT_ARGB: - colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), - new int[]{8, 8, 8, 8}, - true, - false, - ComponentColorModel.TRANSLUCENT, - DataBuffer.TYPE_BYTE); - sampleModel = new BandedSampleModel(DataBuffer.TYPE_BYTE, width, height, 4); - break; - default: - throw new IllegalArgumentException("Unsupported image type: " + imageType); - } - SimpleRaster raster = new SimpleRaster(sampleModel, buffer, new Point(0, 0)); - BigBufferedImage image = new BigBufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), null); - return image; - } - - private static class ImagePartLoader implements Callable { - - private final int y; - private final BufferedImage image; - private final Rectangle region; - private final File file; - - public ImagePartLoader(int y, int width, int height, File file, BufferedImage image) { - this.y = y; - this.image = image; - this.file = file; - region = new Rectangle(0, y, width, height); - } - - @Override - public ImagePartLoader call() throws Exception { - Thread.currentThread().setPriority((Thread.MIN_PRIORITY + Thread.NORM_PRIORITY) / 2); - try (ImageInputStream stream = ImageIO.createImageInputStream(file);) { - Iterator readers = ImageIO.getImageReaders(stream); - if (readers.hasNext()) { - ImageReader reader = readers.next(); - reader.setInput(stream, true, true); - ImageReadParam param = reader.getDefaultReadParam(); - param.setSourceRegion(region); - BufferedImage part = reader.read(0, param); - Raster source = part.getRaster(); - WritableRaster target = image.getRaster(); - target.setRect(0, y, source); - } - } - return ImagePartLoader.this; - } - } - - private BigBufferedImage(ColorModel cm, SimpleRaster raster, boolean isRasterPremultiplied, Hashtable properties) { - super(cm, raster, isRasterPremultiplied, properties); - } - - public void dispose() { - ((SimpleRaster) getRaster()).dispose(); - } - - public static void dispose(RenderedImage image) { - if (image instanceof BigBufferedImage) { - ((BigBufferedImage) image).dispose(); - } - } - - private static class SimpleRaster extends WritableRaster { - - public SimpleRaster(SampleModel sampleModel, FileDataBuffer dataBuffer, Point origin) { - super(sampleModel, dataBuffer, origin); - } - - public void dispose() { - ((FileDataBuffer) getDataBuffer()).dispose(); - } - - } - - private static final class FileDataBufferDeleterHook extends Thread { - - static { - Runtime.getRuntime().addShutdownHook(new FileDataBufferDeleterHook()); - } - - private static final HashSet undisposedBuffers = new HashSet<>(); - - @Override - public void run() { - final FileDataBuffer[] buffers = undisposedBuffers.toArray(new FileDataBuffer[0]); - for (FileDataBuffer b : buffers) { - b.disposeNow(); - } - } - } - - private static class FileDataBuffer extends DataBuffer { - - private final String id = "buffer-" + System.currentTimeMillis() + "-" + ((int) (Math.random() * 1000)); - private File dir; - private String path; - private File[] files; - private RandomAccessFile[] accessFiles; - private MappedByteBuffer[] buffer; - - public FileDataBuffer(File dir, int size) throws FileNotFoundException, IOException { - super(TYPE_BYTE, size); - this.dir = dir; - init(); - } - - public FileDataBuffer(File dir, int size, int numBanks) throws FileNotFoundException, IOException { - super(TYPE_BYTE, size, numBanks); - this.dir = dir; - init(); - } - - private void init() throws FileNotFoundException, IOException { - FileDataBufferDeleterHook.undisposedBuffers.add(this); - if (dir == null) { - dir = new File("."); - } - if (!dir.exists()) { - throw new RuntimeException("FileDataBuffer constructor parameter dir does not exist: " + dir); - } - if (!dir.isDirectory()) { - throw new RuntimeException("FileDataBuffer constructor parameter dir is not a directory: " + dir); - } - path = dir.getPath() + "/" + id; - File subDir = new File(path); - subDir.mkdir(); - buffer = new MappedByteBuffer[banks]; - accessFiles = new RandomAccessFile[banks]; - files = new File[banks]; - for (int i = 0; i < banks; i++) { - File file = files[i] = new File(path + "/bank" + i + ".dat"); - final RandomAccessFile randomAccessFile = accessFiles[i] = new RandomAccessFile(file, "rw"); - buffer[i] = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, getSize()); - } - } - - @Override - public int getElem(int bank, int i) { - return buffer[bank].get(i) & 0xff; - } - - @Override - public void setElem(int bank, int i, int val) { - buffer[bank].put(i, (byte) val); - } - - @Override - protected void finalize() throws Throwable { - dispose(); - } - - private void disposeNow() { - final MappedByteBuffer[] disposedBuffer = this.buffer; - this.buffer = null; - disposeNow(disposedBuffer); - } - - public void dispose() { - final MappedByteBuffer[] disposedBuffer = this.buffer; - this.buffer = null; - new Thread() { - @Override - public void run() { - disposeNow(disposedBuffer); - } - }.start(); - } - - private void disposeNow(final MappedByteBuffer[] disposedBuffer) { - FileDataBufferDeleterHook.undisposedBuffers.remove(this); - if (disposedBuffer != null) { - for (MappedByteBuffer b : disposedBuffer) { - ((DirectBuffer) b).cleaner().clean(); - } - } - if (accessFiles != null) { - for (RandomAccessFile file : accessFiles) { - try { - file.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - accessFiles = null; - } - if (files != null) { - for (File file : files) { - file.delete(); - } - files = null; - } - if (path != null) { - new File(path).delete(); - path = null; - } - } - - } -} \ No newline at end of file diff --git a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java index 7103756..7194c22 100644 --- a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java +++ b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java @@ -1,6 +1,5 @@ package com.pragone.jphash.image.radial; -import com.pragone.jphash.image.BigBufferedImage; import com.pragone.jphash.image.SimpleGrayscaleImage; import javax.imageio.ImageIO; @@ -30,11 +29,18 @@ public class RadialHashAlgorithm { } } - public static RadialHash getHash(File file) throws IOException { - BufferedImage img = BigBufferedImage.create(file, BigBufferedImage.TYPE_INT_RGB); - return getHash(img); + public static RadialHash getHash(String file) throws IOException { + return getHash(new File(file)); } + public static RadialHash getHash(File file) throws IOException { + FileInputStream is = new FileInputStream(file); + try { + return getHash(is); + } finally { + is.close(); + } + } public static RadialHash getHash(InputStream inputStream) throws IOException { BufferedImage img = ImageIO.read(ImageIO.createImageInputStream(inputStream)); diff --git a/src/main/java/com/pragone/jphash/jpHash.java b/src/main/java/com/pragone/jphash/jpHash.java index 91fa926..a391165 100644 --- a/src/main/java/com/pragone/jphash/jpHash.java +++ b/src/main/java/com/pragone/jphash/jpHash.java @@ -15,7 +15,7 @@ */ public class jpHash { public static RadialHash getImageRadialHash(String path) throws IOException { - return RadialHashAlgorithm.getHash(new File(path)); + return RadialHashAlgorithm.getHash(path); } public static RadialHash getImageRadialHash(File file) throws IOException { return RadialHashAlgorithm.getHash(file); From 4ff111c487dbe9ffc13ad7a6e2d333c9a1c077d9 Mon Sep 17 00:00:00 2001 From: Damian Lesiuk Date: Tue, 7 Nov 2017 15:44:30 +0100 Subject: [PATCH 05/12] performance fixes --- .../image/radial/RadialHashAlgorithm.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java index 7194c22..8c39f01 100644 --- a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java +++ b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java @@ -177,13 +177,11 @@ private static Projections calculate180Projections(SimpleGrayscaleImage img) { } public static double getSimilarity(RadialHash hash1, RadialHash hash2) { - int N = hash1.getCoefficients().length; byte[] x_coeffs = hash1.getCoefficients(); byte[] y_coeffs = hash2.getCoefficients(); - double r[] = new double[N]; double sumx = 0.0; double sumy = 0.0; for (int i=0;i < N;i++){ @@ -198,15 +196,21 @@ public static double getSimilarity(RadialHash hash1, RadialHash hash2) { double denx = 0.0; double deny = 0.0; for (int i=0;i max) - max = r[d]; + + double rD = num/Math.sqrt(denx * deny); + if (rD > max) + max = rD; } - return max; //To change body of created methods use File | Settings | File Templates. + return max; } private static class Projections { From 410e32dec78e6c1cd7317e69eb6705251cc1a097 Mon Sep 17 00:00:00 2001 From: Damian Lesiuk Date: Tue, 7 Nov 2017 16:05:31 +0100 Subject: [PATCH 06/12] minor performance adjustment --- .../pragone/jphash/image/radial/RadialHashAlgorithm.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java index 8c39f01..b979441 100644 --- a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java +++ b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java @@ -177,11 +177,11 @@ private static Projections calculate180Projections(SimpleGrayscaleImage img) { } public static double getSimilarity(RadialHash hash1, RadialHash hash2) { - int N = hash1.getCoefficients().length; - byte[] x_coeffs = hash1.getCoefficients(); byte[] y_coeffs = hash2.getCoefficients(); + int N = x_coeffs.length; + double sumx = 0.0; double sumy = 0.0; for (int i=0;i < N;i++){ @@ -199,7 +199,7 @@ public static double getSimilarity(RadialHash hash1, RadialHash hash2) { double denXCurrent = x_coeffs[i] - meanx; double denYCurrent = y_coeffs[(N + i - d) % N] - meany; - num += denXCurrent*denYCurrent; + num += denXCurrent * denYCurrent; denx += denXCurrent * denXCurrent; From 219dd7b7f9a3e05188a66c17e1b70fc5d65e4338 Mon Sep 17 00:00:00 2001 From: Damian Lesiuk Date: Tue, 7 Nov 2017 16:40:11 +0100 Subject: [PATCH 07/12] checked using jmh, it vectorizes better, on my machine it's 125k op/s vs 105k op/s --- .../jphash/image/radial/RadialHashAlgorithm.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java index b979441..cc296d0 100644 --- a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java +++ b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java @@ -190,7 +190,9 @@ public static double getSimilarity(RadialHash hash1, RadialHash hash2) { } double meanx = sumx/N; double meany = sumy/N; + double[] r = new double[N]; double max = 0; + for (int d=0;d max) - max = rD; + r[d] = num/Math.sqrt(denx * deny); + if (r[d] > max) + max = r[d]; } return max; } From 178cedde0126ab23a2d822ba07ec6e30daa1ad66 Mon Sep 17 00:00:00 2001 From: Damian Lesiuk Date: Sat, 11 Nov 2017 18:50:35 +0100 Subject: [PATCH 08/12] increased accuracy --- pom.xml | 10 + .../image/DisposingSimpleGrayscaleImage.java | 346 ------------------ .../jphash/image/SimpleGrayscaleImage.java | 231 +++--------- .../image/UnsafeSimpleGrayscaleImage.java | 307 ---------------- 4 files changed, 60 insertions(+), 834 deletions(-) delete mode 100644 src/main/java/com/pragone/jphash/image/DisposingSimpleGrayscaleImage.java delete mode 100644 src/main/java/com/pragone/jphash/image/UnsafeSimpleGrayscaleImage.java diff --git a/pom.xml b/pom.xml index 41ca467..78f09c3 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,16 @@ + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + 1.8 + 1.8 + + + org.apache.maven.plugins maven-surefire-plugin diff --git a/src/main/java/com/pragone/jphash/image/DisposingSimpleGrayscaleImage.java b/src/main/java/com/pragone/jphash/image/DisposingSimpleGrayscaleImage.java deleted file mode 100644 index 12f95aa..0000000 --- a/src/main/java/com/pragone/jphash/image/DisposingSimpleGrayscaleImage.java +++ /dev/null @@ -1,346 +0,0 @@ -package com.pragone.jphash.image; - -import sun.misc.Cleaner; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.Map; - -public class DisposingSimpleGrayscaleImage { - private static final int BYTE_SIZE = 8; - private static Map, Method> cleanerMethods = new HashMap, Method>(); - private int width; - private int height; - private ByteBuffer data; - private int numPixels; - - static { - ByteBuffer temp = ByteBuffer.allocateDirect(16); - if (temp != null) { - Method cleanerMethod = null; - try { - cleanerMethod = temp.getClass().getMethod("cleaner"); - cleanerMethod.setAccessible(true); - cleanerMethods.put(temp.getClass(), cleanerMethod); - - try { - Cleaner cleaner = (Cleaner) cleanerMethod.invoke(temp); - cleaner.clean(); - } catch (Exception e) { - System.err.println("Couldnt clean up temporary Direct Byte Buffer. Possible memory leaks"); - } - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } - } - } - - private static Method getCleanerMethod(Class byteBufferClass) { - Method temp = cleanerMethods.get(byteBufferClass); - if (temp == null) { - synchronized (DisposingSimpleGrayscaleImage.class) { - if (!cleanerMethods.containsKey(byteBufferClass)) { - try { - Method cleanerMethod = temp.getClass().getMethod("cleaner"); - cleanerMethod.setAccessible(true); - cleanerMethods.put(temp.getClass(), cleanerMethod); - return cleanerMethod; - } catch (NoSuchMethodException e) { - System.err.println("Couldnt clean up temporary Direct Byte Buffer. Possible memory leaks"); - } - } - } - temp = cleanerMethods.get(byteBufferClass); - } - return temp; - } -// private static final int[] NORMALIZATION_APROX; -// private static final int NORMALIZATION_DENOMINATOR_POWER = 11; // 2048 -// -// static { -// NORMALIZATION_APROX = new int[256]; -// double denominator = 1 << NORMALIZATION_DENOMINATOR_POWER; -// for (int i = 1; i < 256; i++) { -// double toAprox = ((double) 256)/i; -// NORMALIZATION_APROX[i] = (int) Math.round(toAprox * denominator); -// } -// } - - public DisposingSimpleGrayscaleImage(int width, int height) { - this.width = width; - this.height = height; - this.numPixels = width*height; - this.data = ByteBuffer.allocateDirect(width * height); - -// this.pointer = Unsafe.getUnsafe().allocateMemory(width*height); -// Unsafe.getUnsafe().setMemory(this.pointer, width * height, (byte) 0); - } - - public DisposingSimpleGrayscaleImage(BufferedImage image) { - this(image.getWidth(), image.getHeight()); - loadImage(image); - resizeToNextSize(); - blur(); - } - - public void blur() { - if (width <= 6 || height <= 6) { - return; - } - // First a horizontal pass - int[] buffer = new int[width]; - int[] buffer_1 = new int[width]; - int[] buffer_2 = new int[width]; - this.data.rewind(); - for (int y = 0; y < height; y++) { - // Copy this horizontal line - for (int i = 0; i < width; i++) { - buffer[i] = this.data.get(width*y + i) & 0xFF; - buffer_1[i] = (byte) (buffer[i] >> 1); // buffer * 0.5 - buffer_2[i] = (byte) (buffer[i] >> 2); // buffer * 0.25 - } - // idx: 0 - long t = ((buffer[0] + buffer_1[1] + buffer_2[2])*585)>>10; // 1024/585 = 1.75042 - this.data.put(width*y,(byte) (t > 255 ? 255 : t)); - t = ((buffer_1[0] + buffer[1] + buffer_1[2] + buffer_2[3])*455)>>10; // 1024/455 = 2.250549 - this.data.put(width*y+1,(byte) (t > 255 ? 255 : t)); - for (int x = 2; x < width-2; x++) { - t = (int) (((long) (buffer_2[x-2] + buffer_1[x-1] + buffer[x] + buffer_1[x+1] + buffer_2[x+2]))*409)>>10; // 1024/409 = 2.503 ~ 1 + 2*0.5 + 2*0.25 - this.data.put(width*y+x, (byte) (t > 255 ? 255 : t)); - } - int x = width-2; - t = ((buffer_2[x-2] + buffer_1[x-1] + buffer[x] + buffer_1[x+1])*455)>>10; // 1024/455 = 2.250549 - this.data.put(width*y+x, (byte) (t > 255 ? 255 : t)); - x++; - t = ((buffer_2[x-2] + buffer_1[x-1] + buffer[x])*585)>>10; // 1024/585 = 1.75042 - this.data.put(width*y+x, (byte) (t > 255 ? 255 : t)); - } - - // Now a vertical pass - buffer = new int[height]; - buffer_1 = new int[height]; - buffer_2 = new int[height]; - for (int x = 0; x < width; x++) { - // Copy this vertical line - for (int i = 0; i < height; i++) { - buffer[i] = this.data.get(width*i + x) & 0xFF; - buffer_1[i] = (byte) (buffer[i] >> 1); // buffer * 0.5 - buffer_2[i] = (byte) (buffer[i] >> 2); // buffer * 0.25 - } - // y = 0 - this.data.put(x,(byte) (((buffer[0] + buffer_1[1] + buffer_2[2])*585)>>10)); // 1024/585 = 1.75042 - // y = 1 - this.data.put(width+x,(byte) (((buffer_1[0] + buffer[1] + buffer_1[2] + buffer_2[3])*455)>>10)); // 1024/455 = 2.250549 - for (int y = 2; y < height-2; y++) { - long t = (((long) (buffer_2[y-2] + buffer_1[y-1] + buffer[y] + buffer_1[y+1] + buffer_2[y+2]))*409)>>10; - this.data.put(width*y+x, (byte) (t > 255 ? 255 : t)); // 1024/409 = 2.503 ~ 1 + 2*0.5 + 2*0.25 - } - int y = height-2; - this.data.put(width*y+x, (byte) (((buffer_2[y-2] + buffer_1[y-1] + buffer[y] + buffer_1[y+1])*455)>>10)); // 1024/455 = 2.250549 - y++; - this.data.put(width*y+x, (byte) (((buffer_2[y-2] + buffer_1[y-1] + buffer[y])*585)>>10)); // 1024/585 = 1.75042 - } - } - - private void resizeToNextSize() { - int min = (width < height) ? width : height; - min = getClosestSmallerPowerOf2(min); - this.resize(min,min); - } - - private int getClosestSmallerPowerOf2(int value) { - int i = 0; - int v = 1; - while (v < value && i < 24) { - i++; - v=v<<1; - } - return v >> 1; - } - - public void resize(int dest_width, int dest_height) { - ByteBuffer newData = ByteBuffer.allocateDirect(dest_width * dest_height); - - double tx = ((double) width) / dest_width; - double ty = ((double) height) / dest_height; - - int Cc; - int C[] = new int[5]; - int d0, d2, d3, a0, a1, a2, a3; - - for (int i = 0; i < dest_height; ++i) { - for (int j = 0; j < dest_width; ++j) { - int x = (int) (tx * j); - int y = (int) (ty * i); - double dx = tx * j - x; - double dy = ty * i - y; - - for (int jj = 0; jj <= 3; ++jj) { - d0 = safeGet((y - 1 + jj) * width + (x - 1)) - - safeGet((y - 1 + jj) * width + (x)); - d2 = safeGet((y - 1 + jj) * width + (x + 1)) - - safeGet((y - 1 + jj) * width + (x)); - d3 = safeGet((y - 1 + jj) * width + (x + 2)) - - safeGet((y - 1 + jj) * width + (x)); - a0 = safeGet((y - 1 + jj) * width + (x)); - a1 = (int) (-1.0 / 3 * d0 + d2 - 1.0 / 6 * d3); - a2 = (int) (1.0 / 2 * d0 + 1.0 / 2 * d2); - a3 = (int) (-1.0 / 6 * d0 - 1.0 / 2 * d2 + 1.0 / 6 * d3); - C[jj] = (int) (a0 + a1 * dx + a2 * dx * dx + a3 * dx * dx * dx); - - d0 = C[0] - C[1]; - d2 = C[2] - C[1]; - d3 = C[3] - C[1]; - a0 = C[1]; - a1 = (int) (-1.0 / 3 * d0 + d2 -1.0 / 6 * d3); - a2 = (int) (1.0 / 2 * d0 + 1.0 / 2 * d2); - a3 = (int) (-1.0 / 6 * d0 - 1.0 / 2 * d2 + 1.0 / 6 * d3); - Cc = (int) (a0 + a1 * dy + a2 * dy * dy + a3* dy * dy * dy); - newData.put(i * dest_width + j, (byte) (Cc & 0xFF)); - } - } - } - dispose(); - this.data = newData; - this.width = dest_width; - this.height = dest_height; - this.numPixels = width*height; - } - - - private int safeGet(int index) { - if (index < 0) { - return 0; - } - if (index >= numPixels) { - return 0; - } - return this.data.get(index) & 0xFF; - } - - public void loadImage(BufferedImage image) { - int numPixels = width*height; - int numComponents = image.getColorModel().getNumComponents(); - int maxPixel = 0; - if (image.getColorModel().getComponentSize(0) == BYTE_SIZE) { - // Components are byte sized - int bufferSize = numPixels*numComponents; - byte[] tempBuffer ; - try{ - tempBuffer = new byte[bufferSize]; - }catch (OutOfMemoryError e) { - System.out.println("Died trying to allocate a buffer of size: " + bufferSize + "!!"); - while (true) { - try { - Thread.sleep(1000); - } catch (InterruptedException e1) { - e1.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. - } - } - } - image.getRaster().getDataElements(0,0,width,height,tempBuffer); - - if (numComponents == 1) { - // Already byte gray - this.data.put(tempBuffer, 0, numPixels); - } else if (image.getType() == BufferedImage.TYPE_3BYTE_BGR) { - int j = 0; - for (int i = 0; i< bufferSize; i+= numComponents) { - // V1 -// long yTemp = ((tempBuffer[i]& 0xFF)*BLUE_Y_COEFF + -// (tempBuffer[i+1]& 0xFF)*GREEN_Y_COEFF + -// (tempBuffer[i+2]& 0xFF)*RED_Y_COEFF); -// int y = (int) (((yTemp >> 10) & 0xFF) + ((yTemp >> 9) & 0x1)); -// where: -// private static final long BLUE_Y_COEFF = (long) Math.floor(0.114d * 1024); -// private static final long GREEN_Y_COEFF = (long) Math.floor(0.587d * 1024); -// private static final long RED_Y_COEFF = (long) Math.floor(0.299d * 1024); - - // V2: CImg version - int y = (((tempBuffer[i+2]& 0xFF) * 66 + (tempBuffer[i+1]& 0xFF) * 129 + - (tempBuffer[i]& 0xFF) *25) >> 8) + 16; - - // if (y < 0) y = 0; - // else - - if (y>255) y = 255; - - if (y > maxPixel) { - maxPixel = y; - } - this.data.put(j++,(byte) y); - } - } else { - throw new IllegalArgumentException("Can't work with this type of byte image: " + image.getType()); - } - } else { - throw new IllegalArgumentException("Can't work with non-byte image buffers"); - } - if (maxPixel > 0) { - // Let's normalize amount of light - // V1: with double math - for (int i =0; i < numPixels; i++) { - long temp = (this.data.get(i) << 8) / maxPixel; - this.data.put(i, (byte) (temp & 0xFF)); - } - // V2: with int only math -// for (int i =0; i < numPixels; i++) { -// long temp = (this.data.get(i) * NORMALIZATION_APROX[this.maxPixel]) >> NORMALIZATION_DENOMINATOR_POWER; -// this.data.put(i, (byte) (temp & 0xFF)); -// } - } - - } - - public void save(String path) { - BufferedImage temp = new BufferedImage(width,height,BufferedImage.TYPE_BYTE_GRAY); - byte[] buffer = new byte[width*height]; - this.data.rewind(); - this.data.get(buffer); - temp.getRaster().setDataElements(0,0,width,height,buffer); - try { - ImageIO.write(temp, "jpg", new File(path)); - } catch (IOException e) { - } - } - - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - public int get(int x, int y) { - return this.data.get(width*y + x) & 0xFF; - } - - public void dispose() { - if (this.data != null && this.data.isDirect()) { - - Method cleanerMethod = getCleanerMethod(this.data.getClass()); - try { - Cleaner cleaner = (Cleaner) cleanerMethod.invoke(this.data); - cleaner.clean(); - } catch (InvocationTargetException e) { - e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. - } catch (IllegalAccessException e) { - e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. - } - - } - } - -// @Override -// protected void finalize() throws Throwable { -// Unsafe.getUnsafe().freeMemory(this.pointer); -// super.finalize(); -// } -} \ No newline at end of file diff --git a/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java b/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java index 966afc4..80dec66 100644 --- a/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java +++ b/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java @@ -1,29 +1,23 @@ package com.pragone.jphash.image; -import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.color.ColorSpace; import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; +import java.awt.image.ColorConvertOp; import java.nio.ByteBuffer; public class SimpleGrayscaleImage { - private static final int BYTE_SIZE = 8; private int width; private int height; private ByteBuffer data; private int numPixels; - public SimpleGrayscaleImage(int width, int height) { - this.width = width; - this.height = height; - this.numPixels = width*height; - this.data = ByteBuffer.allocateDirect(width * height); - } - public SimpleGrayscaleImage(BufferedImage image) { - this(image.getWidth(), image.getHeight()); + this.width = image.getWidth(); + this.height = image.getHeight(); + this.numPixels = width * height; + loadImage(image); - resizeToNextSize(); blur(); } @@ -39,25 +33,25 @@ public void blur() { for (int y = 0; y < height; y++) { // Copy this horizontal line for (int i = 0; i < width; i++) { - buffer[i] = this.data.get(width*y + i) & 0xFF; + buffer[i] = this.data.get(width * y + i) & 0xFF; buffer_1[i] = (byte) (buffer[i] >> 1); // buffer * 0.5 buffer_2[i] = (byte) (buffer[i] >> 2); // buffer * 0.25 } // idx: 0 - long t = ((buffer[0] + buffer_1[1] + buffer_2[2])*585)>>10; // 1024/585 = 1.75042 - this.data.put(width*y,(byte) (t > 255 ? 255 : t)); - t = ((buffer_1[0] + buffer[1] + buffer_1[2] + buffer_2[3])*455)>>10; // 1024/455 = 2.250549 - this.data.put(width*y+1,(byte) (t > 255 ? 255 : t)); - for (int x = 2; x < width-2; x++) { - t = (int) (((long) (buffer_2[x-2] + buffer_1[x-1] + buffer[x] + buffer_1[x+1] + buffer_2[x+2]))*409)>>10; // 1024/409 = 2.503 ~ 1 + 2*0.5 + 2*0.25 - this.data.put(width*y+x, (byte) (t > 255 ? 255 : t)); + long t = ((buffer[0] + buffer_1[1] + buffer_2[2]) * 585) >> 10; // 1024/585 = 1.75042 + this.data.put(width * y, (byte) (t > 255 ? 255 : t)); + t = ((buffer_1[0] + buffer[1] + buffer_1[2] + buffer_2[3]) * 455) >> 10; // 1024/455 = 2.250549 + this.data.put(width * y + 1, (byte) (t > 255 ? 255 : t)); + for (int x = 2; x < width - 2; x++) { + t = (int) (((long) (buffer_2[x - 2] + buffer_1[x - 1] + buffer[x] + buffer_1[x + 1] + buffer_2[x + 2])) * 409) >> 10; // 1024/409 = 2.503 ~ 1 + 2*0.5 + 2*0.25 + this.data.put(width * y + x, (byte) (t > 255 ? 255 : t)); } - int x = width-2; - t = ((buffer_2[x-2] + buffer_1[x-1] + buffer[x] + buffer_1[x+1])*455)>>10; // 1024/455 = 2.250549 - this.data.put(width*y+x, (byte) (t > 255 ? 255 : t)); + int x = width - 2; + t = ((buffer_2[x - 2] + buffer_1[x - 1] + buffer[x] + buffer_1[x + 1]) * 455) >> 10; // 1024/455 = 2.250549 + this.data.put(width * y + x, (byte) (t > 255 ? 255 : t)); x++; - t = ((buffer_2[x-2] + buffer_1[x-1] + buffer[x])*585)>>10; // 1024/585 = 1.75042 - this.data.put(width*y+x, (byte) (t > 255 ? 255 : t)); + t = ((buffer_2[x - 2] + buffer_1[x - 1] + buffer[x]) * 585) >> 10; // 1024/585 = 1.75042 + this.data.put(width * y + x, (byte) (t > 255 ? 255 : t)); } // Now a vertical pass @@ -67,177 +61,52 @@ public void blur() { for (int x = 0; x < width; x++) { // Copy this vertical line for (int i = 0; i < height; i++) { - buffer[i] = this.data.get(width*i + x) & 0xFF; + buffer[i] = this.data.get(width * i + x) & 0xFF; buffer_1[i] = (byte) (buffer[i] >> 1); // buffer * 0.5 buffer_2[i] = (byte) (buffer[i] >> 2); // buffer * 0.25 } // y = 0 - this.data.put(x,(byte) (((buffer[0] + buffer_1[1] + buffer_2[2])*585)>>10)); // 1024/585 = 1.75042 + this.data.put(x, (byte) (((buffer[0] + buffer_1[1] + buffer_2[2]) * 585) >> 10)); // 1024/585 = 1.75042 // y = 1 - this.data.put(width+x,(byte) (((buffer_1[0] + buffer[1] + buffer_1[2] + buffer_2[3])*455)>>10)); // 1024/455 = 2.250549 - for (int y = 2; y < height-2; y++) { - long t = (((long) (buffer_2[y-2] + buffer_1[y-1] + buffer[y] + buffer_1[y+1] + buffer_2[y+2]))*409)>>10; - this.data.put(width*y+x, (byte) (t > 255 ? 255 : t)); // 1024/409 = 2.503 ~ 1 + 2*0.5 + 2*0.25 + this.data.put(width + x, (byte) (((buffer_1[0] + buffer[1] + buffer_1[2] + buffer_2[3]) * 455) >> 10)); // 1024/455 = 2.250549 + for (int y = 2; y < height - 2; y++) { + long t = (((long) (buffer_2[y - 2] + buffer_1[y - 1] + buffer[y] + buffer_1[y + 1] + buffer_2[y + 2])) * 409) >> 10; + this.data.put(width * y + x, (byte) (t > 255 ? 255 : t)); // 1024/409 = 2.503 ~ 1 + 2*0.5 + 2*0.25 } - int y = height-2; - this.data.put(width*y+x, (byte) (((buffer_2[y-2] + buffer_1[y-1] + buffer[y] + buffer_1[y+1])*455)>>10)); // 1024/455 = 2.250549 + int y = height - 2; + this.data.put(width * y + x, (byte) (((buffer_2[y - 2] + buffer_1[y - 1] + buffer[y] + buffer_1[y + 1]) * 455) >> 10)); // 1024/455 = 2.250549 y++; - this.data.put(width*y+x, (byte) (((buffer_2[y-2] + buffer_1[y-1] + buffer[y])*585)>>10)); // 1024/585 = 1.75042 - } - } - - private void resizeToNextSize() { - int min = (width < height) ? width : height; - min = getClosestSmallerPowerOf2(min); - this.resize(min,min); - } - - private int getClosestSmallerPowerOf2(int value) { - int i = 0; - int v = 1; - while (v < value && i < 24) { - i++; - v=v<<1; - } - return v >> 1; - } - - public void resize(int dest_width, int dest_height) { - ByteBuffer newData = ByteBuffer.allocateDirect(dest_width * dest_height); - - double tx = ((double) width) / dest_width; - double ty = ((double) height) / dest_height; - - int Cc; - int C[] = new int[5]; - int d0, d2, d3, a0, a1, a2, a3; - - for (int i = 0; i < dest_height; ++i) { - for (int j = 0; j < dest_width; ++j) { - int x = (int) (tx * j); - int y = (int) (ty * i); - double dx = tx * j - x; - double dy = ty * i - y; - - for (int jj = 0; jj <= 3; ++jj) { - d0 = safeGet((y - 1 + jj) * width + (x - 1)) - - safeGet((y - 1 + jj) * width + (x)); - d2 = safeGet((y - 1 + jj) * width + (x + 1)) - - safeGet((y - 1 + jj) * width + (x)); - d3 = safeGet((y - 1 + jj) * width + (x + 2)) - - safeGet((y - 1 + jj) * width + (x)); - a0 = safeGet((y - 1 + jj) * width + (x)); - a1 = (int) (-1.0 / 3 * d0 + d2 - 1.0 / 6 * d3); - a2 = (int) (1.0 / 2 * d0 + 1.0 / 2 * d2); - a3 = (int) (-1.0 / 6 * d0 - 1.0 / 2 * d2 + 1.0 / 6 * d3); - C[jj] = (int) (a0 + a1 * dx + a2 * dx * dx + a3 * dx * dx * dx); - - d0 = C[0] - C[1]; - d2 = C[2] - C[1]; - d3 = C[3] - C[1]; - a0 = C[1]; - a1 = (int) (-1.0 / 3 * d0 + d2 -1.0 / 6 * d3); - a2 = (int) (1.0 / 2 * d0 + 1.0 / 2 * d2); - a3 = (int) (-1.0 / 6 * d0 - 1.0 / 2 * d2 + 1.0 / 6 * d3); - Cc = (int) (a0 + a1 * dy + a2 * dy * dy + a3* dy * dy * dy); - newData.put(i * dest_width + j, (byte) (Cc & 0xFF)); - } - } + this.data.put(width * y + x, (byte) (((buffer_2[y - 2] + buffer_1[y - 1] + buffer[y]) * 585) >> 10)); // 1024/585 = 1.75042 } - this.data = newData; - this.width = dest_width; - this.height = dest_height; - this.numPixels = width*height; } - - private int safeGet(int index) { - if (index < 0) { - return 0; - } - if (index >= numPixels) { - return 0; - } - return this.data.get(index) & 0xFF; - } + private ColorConvertOp colorConvert = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null); + private static final int RESIZE_WIDTH = 1024; + private static final int RESIZE_HEIGHT = 1024; public void loadImage(BufferedImage image) { - int numPixels = width*height; - int numComponents = image.getColorModel().getNumComponents(); - int maxPixel = 0; - if (image.getColorModel().getComponentSize(0) == BYTE_SIZE) { - // Components are byte sized - int bufferSize = numPixels*numComponents; - byte[] tempBuffer ; - try{ - tempBuffer = new byte[bufferSize]; - }catch (OutOfMemoryError e) { - System.out.println("Died trying to allocate a buffer of size: " + bufferSize + ". Please increas heap size!!"); - throw e; - } - image.getRaster().getDataElements(0,0,width,height,tempBuffer); - - if (numComponents == 1) { - // Already byte gray - this.data.put(tempBuffer, 0, numPixels); - } else if (image.getType() == BufferedImage.TYPE_3BYTE_BGR) { - int j = 0; - for (int i = 0; i< bufferSize; i+= numComponents) { - // V1 -// long yTemp = ((tempBuffer[i]& 0xFF)*BLUE_Y_COEFF + -// (tempBuffer[i+1]& 0xFF)*GREEN_Y_COEFF + -// (tempBuffer[i+2]& 0xFF)*RED_Y_COEFF); -// int y = (int) (((yTemp >> 10) & 0xFF) + ((yTemp >> 9) & 0x1)); -// where: -// private static final long BLUE_Y_COEFF = (long) Math.floor(0.114d * 1024); -// private static final long GREEN_Y_COEFF = (long) Math.floor(0.587d * 1024); -// private static final long RED_Y_COEFF = (long) Math.floor(0.299d * 1024); - - // V2: CImg version - int y = (((tempBuffer[i+2]& 0xFF) * 66 + (tempBuffer[i+1]& 0xFF) * 129 + - (tempBuffer[i]& 0xFF) *25) >> 8) + 16; + try { + BufferedImage resizedImage = new BufferedImage(RESIZE_WIDTH, RESIZE_HEIGHT, BufferedImage.TYPE_BYTE_GRAY); + Graphics2D g = resizedImage.createGraphics(); + g.drawImage(image, 0, 0, RESIZE_WIDTH, RESIZE_HEIGHT, null); + g.dispose(); - // if (y < 0) y = 0; - // else + colorConvert.filter(resizedImage, resizedImage); - if (y>255) y = 255; + numPixels = (RESIZE_WIDTH * RESIZE_HEIGHT); + width = RESIZE_WIDTH; + height = RESIZE_HEIGHT; - if (y > maxPixel) { - maxPixel = y; - } - this.data.put(j++,(byte) y); - } - } else { - throw new IllegalArgumentException("Can't work with this type of byte image: " + image.getType()); - } - } else { - throw new IllegalArgumentException("Can't work with non-byte image buffers"); - } - if (maxPixel > 0) { - // Let's normalize amount of light - // V1: with double math - for (int i =0; i < numPixels; i++) { - long temp = (this.data.get(i) << 8) / maxPixel; - this.data.put(i, (byte) (temp & 0xFF)); - } - // V2: with int only math -// for (int i =0; i < numPixels; i++) { -// long temp = (this.data.get(i) * NORMALIZATION_APROX[this.maxPixel]) >> NORMALIZATION_DENOMINATOR_POWER; -// this.data.put(i, (byte) (temp & 0xFF)); -// } - } + int totalPixels = numPixels * resizedImage.getColorModel().getNumComponents(); - } + byte[] tempBuffer = new byte[totalPixels]; + resizedImage.getRaster().getDataElements(0, 0, RESIZE_WIDTH, RESIZE_HEIGHT, tempBuffer); - public void save(String path) { - BufferedImage temp = new BufferedImage(width,height,BufferedImage.TYPE_BYTE_GRAY); - byte[] buffer = new byte[width*height]; - this.data.rewind(); - this.data.get(buffer); - temp.getRaster().setDataElements(0,0,width,height,buffer); - try { - ImageIO.write(temp, "jpg", new File(path)); - } catch (IOException e) { + data = ByteBuffer.allocateDirect(totalPixels); + data.put(tempBuffer); + } catch (OutOfMemoryError e) { + System.err.println("Died trying to allocate a buffer for image. Please increase heap size!!"); + throw e; } } @@ -250,7 +119,7 @@ public int getHeight() { } public int get(int x, int y) { - return this.data.get(width*y + x) & 0xFF; + return this.data.get(width * y + x) & 0xFF; } } \ No newline at end of file diff --git a/src/main/java/com/pragone/jphash/image/UnsafeSimpleGrayscaleImage.java b/src/main/java/com/pragone/jphash/image/UnsafeSimpleGrayscaleImage.java deleted file mode 100644 index e299244..0000000 --- a/src/main/java/com/pragone/jphash/image/UnsafeSimpleGrayscaleImage.java +++ /dev/null @@ -1,307 +0,0 @@ -package com.pragone.jphash.image; - -import sun.misc.Unsafe; - -import java.awt.image.BufferedImage; -import java.lang.reflect.Field; - -public class UnsafeSimpleGrayscaleImage { - private static final int BYTE_SIZE = 8; - private static final Unsafe unsafeInstance = getUnsafe(); - private long pointer; - private int width; - private int height; - private int numPixels; - - private static Unsafe getUnsafe() { - try { - Field f = Unsafe.class.getDeclaredField("theUnsafe"); - f.setAccessible(true); - return (Unsafe) f.get(null); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public UnsafeSimpleGrayscaleImage(int width, int height) { - this.width = width; - this.height = height; - this.numPixels = width*height; - this.pointer = unsafeInstance.allocateMemory(width*height); - unsafeInstance.setMemory(this.pointer, width * height, (byte) 0); - } - - public UnsafeSimpleGrayscaleImage(BufferedImage image) { - this(image.getWidth(), image.getHeight()); - loadImage(image); - resizeToNextSize(); - blur(); - } - - public void blur() { - if (width <= 6 || height <= 6) { - return; - } - // First a horizontal pass - int[] buffer = new int[width]; - int[] buffer_1 = new int[width]; - int[] buffer_2 = new int[width]; - for (int y = 0; y < height; y++) { - // Copy this horizontal line - for (int i = 0; i < width; i++) { - buffer[i] = getByte(width * y + i) & 0xFF; - buffer_1[i] = (byte) (buffer[i] >> 1); // buffer * 0.5 - buffer_2[i] = (byte) (buffer[i] >> 2); // buffer * 0.25 - } - // idx: 0 - long t = ((buffer[0] + buffer_1[1] + buffer_2[2])*585)>>10; // 1024/585 = 1.75042 - putByte(width * y, (byte) (t > 255 ? 255 : t)); - t = ((buffer_1[0] + buffer[1] + buffer_1[2] + buffer_2[3])*455)>>10; // 1024/455 = 2.250549 - putByte(width*y+1,(byte) (t > 255 ? 255 : t)); - for (int x = 2; x < width-2; x++) { - t = (int) (((long) (buffer_2[x-2] + buffer_1[x-1] + buffer[x] + buffer_1[x+1] + buffer_2[x+2]))*409)>>10; // 1024/409 = 2.503 ~ 1 + 2*0.5 + 2*0.25 - putByte(width*y+x, (byte) (t > 255 ? 255 : t)); - } - int x = width-2; - t = ((buffer_2[x-2] + buffer_1[x-1] + buffer[x] + buffer_1[x+1])*455)>>10; // 1024/455 = 2.250549 - putByte(width*y+x, (byte) (t > 255 ? 255 : t)); - x++; - t = ((buffer_2[x-2] + buffer_1[x-1] + buffer[x])*585)>>10; // 1024/585 = 1.75042 - putByte(width*y+x, (byte) (t > 255 ? 255 : t)); - } - - // Now a vertical pass - buffer = new int[height]; - buffer_1 = new int[height]; - buffer_2 = new int[height]; - for (int x = 0; x < width; x++) { - // Copy this vertical line - for (int i = 0; i < height; i++) { - buffer[i] = getByte(width*i + x) & 0xFF; - buffer_1[i] = (byte) (buffer[i] >> 1); // buffer * 0.5 - buffer_2[i] = (byte) (buffer[i] >> 2); // buffer * 0.25 - } - // y = 0 - putByte(x,(byte) (((buffer[0] + buffer_1[1] + buffer_2[2])*585)>>10)); // 1024/585 = 1.75042 - // y = 1 - putByte(width+x,(byte) (((buffer_1[0] + buffer[1] + buffer_1[2] + buffer_2[3])*455)>>10)); // 1024/455 = 2.250549 - for (int y = 2; y < height-2; y++) { - long t = (((long) (buffer_2[y-2] + buffer_1[y-1] + buffer[y] + buffer_1[y+1] + buffer_2[y+2]))*409)>>10; - putByte(width*y+x, (byte) (t > 255 ? 255 : t)); // 1024/409 = 2.503 ~ 1 + 2*0.5 + 2*0.25 - } - int y = height-2; - putByte(width*y+x, (byte) (((buffer_2[y-2] + buffer_1[y-1] + buffer[y] + buffer_1[y+1])*455)>>10)); // 1024/455 = 2.250549 - y++; - putByte(width*y+x, (byte) (((buffer_2[y-2] + buffer_1[y-1] + buffer[y])*585)>>10)); // 1024/585 = 1.75042 - } - } - - private byte getByte(long offset) { - return unsafeInstance.getByte(this.pointer + offset); - } - - private void putByte(long offset, byte value) { - unsafeInstance.putByte(this.pointer + offset, value); - } - - private void resizeToNextSize() { - int min = (width < height) ? width : height; - min = getClosestSmallerPowerOf2(min); - this.resize(min,min); - } - - private int getClosestSmallerPowerOf2(int value) { - int i = 0; - int v = 1; - while (v < value && i < 24) { - i++; - v=v<<1; - } - return v >> 1; - } - - public void resize(int dest_width, int dest_height) { - long newData = unsafeInstance.allocateMemory(dest_width * dest_height); - - double tx = ((double) width) / dest_width; - double ty = ((double) height) / dest_height; - - int Cc; - int C[] = new int[5]; - int d0, d2, d3, a0, a1, a2, a3; - - for (int i = 0; i < dest_height; ++i) { - for (int j = 0; j < dest_width; ++j) { - int x = (int) (tx * j); - int y = (int) (ty * i); - double dx = tx * j - x; - double dy = ty * i - y; - - for (int jj = 0; jj <= 3; ++jj) { - d0 = safeGet((y - 1 + jj) * width + (x - 1)) - - safeGet((y - 1 + jj) * width + (x)); - d2 = safeGet((y - 1 + jj) * width + (x + 1)) - - safeGet((y - 1 + jj) * width + (x)); - d3 = safeGet((y - 1 + jj) * width + (x + 2)) - - safeGet((y - 1 + jj) * width + (x)); - a0 = safeGet((y - 1 + jj) * width + (x)); - a1 = (int) (-1.0 / 3 * d0 + d2 - 1.0 / 6 * d3); - a2 = (int) (1.0 / 2 * d0 + 1.0 / 2 * d2); - a3 = (int) (-1.0 / 6 * d0 - 1.0 / 2 * d2 + 1.0 / 6 * d3); - C[jj] = (int) (a0 + a1 * dx + a2 * dx * dx + a3 * dx * dx * dx); - - d0 = C[0] - C[1]; - d2 = C[2] - C[1]; - d3 = C[3] - C[1]; - a0 = C[1]; - a1 = (int) (-1.0 / 3 * d0 + d2 -1.0 / 6 * d3); - a2 = (int) (1.0 / 2 * d0 + 1.0 / 2 * d2); - a3 = (int) (-1.0 / 6 * d0 - 1.0 / 2 * d2 + 1.0 / 6 * d3); - Cc = (int) (a0 + a1 * dy + a2 * dy * dy + a3* dy * dy * dy); - unsafeInstance.putByte(newData + i * dest_width + j, (byte) (Cc & 0xFF)); - } - } - } - long temp = this.pointer; - this.pointer = newData; - release(temp); - this.width = dest_width; - this.height = dest_height; - this.numPixels = width*height; - } - - - private int safeGet(int index) { - if (index < 0) { - return 0; - } - if (index >= numPixels) { - return 0; - } - return getByte(index) & 0xFF; - } - - public void loadImage(BufferedImage image) { - int numPixels = width*height; - int numComponents = image.getColorModel().getNumComponents(); - int maxPixel = 0; - if (image.getColorModel().getComponentSize(0) == BYTE_SIZE) { - // Components are byte sized - int bufferSize = numPixels*numComponents; - byte[] tempBuffer ; - try{ - tempBuffer = new byte[bufferSize]; - }catch (OutOfMemoryError e) { - System.out.println("Died trying to allocate a buffer of size: " + bufferSize + "!!"); - while (true) { - try { - Thread.sleep(1000); - } catch (InterruptedException e1) { - e1.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. - } - } - } - image.getRaster().getDataElements(0,0,width,height,tempBuffer); - - if (numComponents == 1) { - // Already byte gray - putBytes(tempBuffer, 0, numPixels,0); - } else if (image.getType() == BufferedImage.TYPE_3BYTE_BGR) { - int j = 0; - for (int i = 0; i< bufferSize; i+= numComponents) { - // V1 -// long yTemp = ((tempBuffer[i]& 0xFF)*BLUE_Y_COEFF + -// (tempBuffer[i+1]& 0xFF)*GREEN_Y_COEFF + -// (tempBuffer[i+2]& 0xFF)*RED_Y_COEFF); -// int y = (int) (((yTemp >> 10) & 0xFF) + ((yTemp >> 9) & 0x1)); -// where: -// private static final long BLUE_Y_COEFF = (long) Math.floor(0.114d * 1024); -// private static final long GREEN_Y_COEFF = (long) Math.floor(0.587d * 1024); -// private static final long RED_Y_COEFF = (long) Math.floor(0.299d * 1024); - - // V2: CImg version - int y = (((tempBuffer[i+2]& 0xFF) * 66 + (tempBuffer[i+1]& 0xFF) * 129 + - (tempBuffer[i]& 0xFF) *25) >> 8) + 16; - - // if (y < 0) y = 0; - // else - - if (y>255) y = 255; - - if (y > maxPixel) { - maxPixel = y; - } - putByte(j++,(byte) y); - } - } else { - throw new IllegalArgumentException("Can't work with this type of byte image: " + image.getType()); - } - } else { - throw new IllegalArgumentException("Can't work with non-byte image buffers"); - } - if (maxPixel > 0) { - // Let's normalize amount of light - // V1: with double math - for (int i =0; i < numPixels; i++) { - long temp = (getByte(i) << 8) / maxPixel; - putByte(i, (byte) (temp & 0xFF)); - } - // V2: with int only math -// for (int i =0; i < numPixels; i++) { -// long temp = (getByte(i) * NORMALIZATION_APROX[this.maxPixel]) >> NORMALIZATION_DENOMINATOR_POWER; -// putByte(i, (byte) (temp & 0xFF)); -// } - } - - } - - private void putBytes(byte[] buffer, long bufferOffset, long numBytes, long toMemoryOffset) { - for (long i = 0; i < numBytes; i++) { - putByte(toMemoryOffset + i, buffer[(int) (bufferOffset+i)]); - } - } - -// public void save(String path) { -// BufferedImage temp = new BufferedImage(width,height,BufferedImage.TYPE_BYTE_GRAY); -// byte[] buffer = new byte[width*height]; -// this.data.rewind(); -// getByte(buffer); -// temp.getRaster().setDataElements(0,0,width,height,buffer); -// try { -// ImageIO.write(temp, "jpg", new File(path)); -// } catch (IOException e) { -// } -// } - - private static long allocate(long size) { - long temp = unsafeInstance.allocateMemory(size); - unsafeInstance.setMemory(temp, size, (byte) 0); - return temp; - } - - private static void release(long pointer) { - unsafeInstance.freeMemory(pointer); - } - - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - public int get(int x, int y) { - return getByte(width*y + x) & 0xFF; - } - - public void dispose() { - release(this.pointer); - } - -// @Override -// protected void finalize() throws Throwable { -// Unsafe.getUnsafe().freeMemory(this.pointer); -// super.finalize(); -// } -} \ No newline at end of file From dd976ce0dc3ff1f69fb59eb5227de037e442fa8d Mon Sep 17 00:00:00 2001 From: Damian Lesiuk Date: Sat, 11 Nov 2017 19:00:29 +0100 Subject: [PATCH 09/12] test --- .../jphash/image/SimpleGrayscaleImage.java | 45 ++++++++---------- .../image/radial/RadialHashAlgorithmTest.java | 9 ++++ src/test/resources/not_earth.jpg | Bin 0 -> 26843 bytes 3 files changed, 29 insertions(+), 25 deletions(-) create mode 100644 src/test/resources/not_earth.jpg diff --git a/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java b/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java index 80dec66..8a4d00f 100644 --- a/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java +++ b/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java @@ -1,15 +1,12 @@ package com.pragone.jphash.image; import java.awt.*; -import java.awt.color.ColorSpace; import java.awt.image.BufferedImage; -import java.awt.image.ColorConvertOp; -import java.nio.ByteBuffer; public class SimpleGrayscaleImage { private int width; private int height; - private ByteBuffer data; + private byte[] data; private int numPixels; public SimpleGrayscaleImage(BufferedImage image) { @@ -21,7 +18,7 @@ public SimpleGrayscaleImage(BufferedImage image) { blur(); } - public void blur() { + private void blur() { if (width <= 6 || height <= 6) { return; } @@ -29,29 +26,28 @@ public void blur() { int[] buffer = new int[width]; int[] buffer_1 = new int[width]; int[] buffer_2 = new int[width]; - this.data.rewind(); for (int y = 0; y < height; y++) { // Copy this horizontal line for (int i = 0; i < width; i++) { - buffer[i] = this.data.get(width * y + i) & 0xFF; + buffer[i] = this.data[width * y + i]; buffer_1[i] = (byte) (buffer[i] >> 1); // buffer * 0.5 buffer_2[i] = (byte) (buffer[i] >> 2); // buffer * 0.25 } // idx: 0 long t = ((buffer[0] + buffer_1[1] + buffer_2[2]) * 585) >> 10; // 1024/585 = 1.75042 - this.data.put(width * y, (byte) (t > 255 ? 255 : t)); + this.data[width * y] = (byte) (t > 255 ? 255 : t); t = ((buffer_1[0] + buffer[1] + buffer_1[2] + buffer_2[3]) * 455) >> 10; // 1024/455 = 2.250549 - this.data.put(width * y + 1, (byte) (t > 255 ? 255 : t)); + this.data[width * y + 1] = (byte) (t > 255 ? 255 : t); for (int x = 2; x < width - 2; x++) { t = (int) (((long) (buffer_2[x - 2] + buffer_1[x - 1] + buffer[x] + buffer_1[x + 1] + buffer_2[x + 2])) * 409) >> 10; // 1024/409 = 2.503 ~ 1 + 2*0.5 + 2*0.25 - this.data.put(width * y + x, (byte) (t > 255 ? 255 : t)); + this.data[width * y + x] = (byte) (t > 255 ? 255 : t); } int x = width - 2; t = ((buffer_2[x - 2] + buffer_1[x - 1] + buffer[x] + buffer_1[x + 1]) * 455) >> 10; // 1024/455 = 2.250549 - this.data.put(width * y + x, (byte) (t > 255 ? 255 : t)); + this.data[width * y + x] = (byte) (t > 255 ? 255 : t); x++; t = ((buffer_2[x - 2] + buffer_1[x - 1] + buffer[x]) * 585) >> 10; // 1024/585 = 1.75042 - this.data.put(width * y + x, (byte) (t > 255 ? 255 : t)); + this.data[width * y + x] = (byte) (t > 255 ? 255 : t); } // Now a vertical pass @@ -61,37 +57,37 @@ public void blur() { for (int x = 0; x < width; x++) { // Copy this vertical line for (int i = 0; i < height; i++) { - buffer[i] = this.data.get(width * i + x) & 0xFF; + buffer[i] = this.data[width * i + x]; buffer_1[i] = (byte) (buffer[i] >> 1); // buffer * 0.5 buffer_2[i] = (byte) (buffer[i] >> 2); // buffer * 0.25 } // y = 0 - this.data.put(x, (byte) (((buffer[0] + buffer_1[1] + buffer_2[2]) * 585) >> 10)); // 1024/585 = 1.75042 + this.data[x] = (byte) (((buffer[0] + buffer_1[1] + buffer_2[2]) * 585) >> 10); // 1024/585 = 1.75042 // y = 1 - this.data.put(width + x, (byte) (((buffer_1[0] + buffer[1] + buffer_1[2] + buffer_2[3]) * 455) >> 10)); // 1024/455 = 2.250549 + this.data[width + x] = (byte) (((buffer_1[0] + buffer[1] + buffer_1[2] + buffer_2[3]) * 455) >> 10); // 1024/455 = 2.250549 for (int y = 2; y < height - 2; y++) { long t = (((long) (buffer_2[y - 2] + buffer_1[y - 1] + buffer[y] + buffer_1[y + 1] + buffer_2[y + 2])) * 409) >> 10; - this.data.put(width * y + x, (byte) (t > 255 ? 255 : t)); // 1024/409 = 2.503 ~ 1 + 2*0.5 + 2*0.25 + this.data[width * y + x] = (byte) (t > 255 ? 255 : t); // 1024/409 = 2.503 ~ 1 + 2*0.5 + 2*0.25 } int y = height - 2; - this.data.put(width * y + x, (byte) (((buffer_2[y - 2] + buffer_1[y - 1] + buffer[y] + buffer_1[y + 1]) * 455) >> 10)); // 1024/455 = 2.250549 + this.data[width * y + x] = (byte) (((buffer_2[y - 2] + buffer_1[y - 1] + buffer[y] + buffer_1[y + 1]) * 455) >> 10) ; // 1024/455 = 2.250549 y++; - this.data.put(width * y + x, (byte) (((buffer_2[y - 2] + buffer_1[y - 1] + buffer[y]) * 585) >> 10)); // 1024/585 = 1.75042 + this.data[width * y + x] = (byte) (((buffer_2[y - 2] + buffer_1[y - 1] + buffer[y]) * 585) >> 10); // 1024/585 = 1.75042 } } - private ColorConvertOp colorConvert = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null); +// private ColorConvertOp colorConvert = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null); private static final int RESIZE_WIDTH = 1024; private static final int RESIZE_HEIGHT = 1024; - public void loadImage(BufferedImage image) { + private void loadImage(BufferedImage image) { try { BufferedImage resizedImage = new BufferedImage(RESIZE_WIDTH, RESIZE_HEIGHT, BufferedImage.TYPE_BYTE_GRAY); Graphics2D g = resizedImage.createGraphics(); g.drawImage(image, 0, 0, RESIZE_WIDTH, RESIZE_HEIGHT, null); g.dispose(); - colorConvert.filter(resizedImage, resizedImage); +// colorConvert.filter(resizedImage, resizedImage); numPixels = (RESIZE_WIDTH * RESIZE_HEIGHT); width = RESIZE_WIDTH; @@ -102,8 +98,7 @@ public void loadImage(BufferedImage image) { byte[] tempBuffer = new byte[totalPixels]; resizedImage.getRaster().getDataElements(0, 0, RESIZE_WIDTH, RESIZE_HEIGHT, tempBuffer); - data = ByteBuffer.allocateDirect(totalPixels); - data.put(tempBuffer); + data = tempBuffer; } catch (OutOfMemoryError e) { System.err.println("Died trying to allocate a buffer for image. Please increase heap size!!"); throw e; @@ -118,8 +113,8 @@ public int getHeight() { return height; } - public int get(int x, int y) { - return this.data.get(width * y + x) & 0xFF; + public byte get(int x, int y) { + return this.data[width * y + x]; } } \ No newline at end of file diff --git a/src/test/java/com/pragone/jphash/image/radial/RadialHashAlgorithmTest.java b/src/test/java/com/pragone/jphash/image/radial/RadialHashAlgorithmTest.java index 2ee3e3c..c8cfd15 100644 --- a/src/test/java/com/pragone/jphash/image/radial/RadialHashAlgorithmTest.java +++ b/src/test/java/com/pragone/jphash/image/radial/RadialHashAlgorithmTest.java @@ -58,6 +58,15 @@ public void testEarthResizedDistance() throws IOException { ), 0.000001); } + @Test + public void testNotEarthDistance() throws IOException { + Assert.assertEquals(EARTH_RESIZED_DISTANCE, + RadialHashAlgorithm.getSimilarity( + getHashFor("earth1.jpg"), + getHashFor("not_earth.jpg") + ), 0.000001); + } + @Test public void testEarthCroppedDistance() throws IOException { Assert.assertEquals(EARTH_CROPPED_DISTANCE, diff --git a/src/test/resources/not_earth.jpg b/src/test/resources/not_earth.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d9a5e0f7997df7cea3d759e00cbae7839530460 GIT binary patch literal 26843 zcmaI7dmz*A`#-LiqH?NKPDMv5=bXy1Qc4IppOf=(ma}1%Q{_~I5JEW(6UH27A(Uf| za~O-|IOaIRZ2LV|y$$3cH~u8rntpT&Yas)XU;s_D}UxQ@7XhF_Wls&3A-RI{OIK& zsXbHIZ(YA->C|Ap-7hCMIPN=NT1pJX&2DEBi{|C5tPn*DFa(VR*P-F)6e#EE@IyED zpEmULXu5Z8|Ih#6;}|R9hnkRQe|{DYz6}s0N0Wrp-H%doA>`8}rqeM>N`GZ1IIm8% z5Wk#__fc9b3nR?WgPWtc{PAt^{vgQM81@#Al_816T4%Bc=Z_2VoTv}XJoC7{-xhoL2FFY7((y8fQcS7U*oHtzX1Q6lN$p0T*7)hD6H^*-C-}k#=Sc6u zI1kdCN<0SqgTFHi?Qa6N#tO{tP$*$N%3I&V8tDSk55omif-04$8~y2

H(5^6PXh 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 I?&ib)0ofsmDF6Tf literal 0 HcmV?d00001 From c46d967c65d08699f6c34893d040c76dd1130ec5 Mon Sep 17 00:00:00 2001 From: Damian Lesiuk Date: Sat, 11 Nov 2017 19:02:04 +0100 Subject: [PATCH 10/12] fixes --- .../jphash/image/SimpleGrayscaleImage.java | 41 +++++++++++-------- .../image/radial/RadialHashAlgorithmTest.java | 2 +- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java b/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java index 8a4d00f..43d924e 100644 --- a/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java +++ b/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java @@ -1,12 +1,15 @@ package com.pragone.jphash.image; import java.awt.*; +import java.awt.color.ColorSpace; import java.awt.image.BufferedImage; +import java.awt.image.ColorConvertOp; +import java.nio.ByteBuffer; public class SimpleGrayscaleImage { private int width; private int height; - private byte[] data; + private ByteBuffer data; private int numPixels; public SimpleGrayscaleImage(BufferedImage image) { @@ -18,7 +21,7 @@ public SimpleGrayscaleImage(BufferedImage image) { blur(); } - private void blur() { + public void blur() { if (width <= 6 || height <= 6) { return; } @@ -26,28 +29,29 @@ private void blur() { int[] buffer = new int[width]; int[] buffer_1 = new int[width]; int[] buffer_2 = new int[width]; + this.data.rewind(); for (int y = 0; y < height; y++) { // Copy this horizontal line for (int i = 0; i < width; i++) { - buffer[i] = this.data[width * y + i]; + buffer[i] = this.data.get(width * y + i) & 0xFF; buffer_1[i] = (byte) (buffer[i] >> 1); // buffer * 0.5 buffer_2[i] = (byte) (buffer[i] >> 2); // buffer * 0.25 } // idx: 0 long t = ((buffer[0] + buffer_1[1] + buffer_2[2]) * 585) >> 10; // 1024/585 = 1.75042 - this.data[width * y] = (byte) (t > 255 ? 255 : t); + this.data.put(width * y, (byte) (t > 255 ? 255 : t)); t = ((buffer_1[0] + buffer[1] + buffer_1[2] + buffer_2[3]) * 455) >> 10; // 1024/455 = 2.250549 - this.data[width * y + 1] = (byte) (t > 255 ? 255 : t); + this.data.put(width * y + 1, (byte) (t > 255 ? 255 : t)); for (int x = 2; x < width - 2; x++) { t = (int) (((long) (buffer_2[x - 2] + buffer_1[x - 1] + buffer[x] + buffer_1[x + 1] + buffer_2[x + 2])) * 409) >> 10; // 1024/409 = 2.503 ~ 1 + 2*0.5 + 2*0.25 - this.data[width * y + x] = (byte) (t > 255 ? 255 : t); + this.data.put(width * y + x, (byte) (t > 255 ? 255 : t)); } int x = width - 2; t = ((buffer_2[x - 2] + buffer_1[x - 1] + buffer[x] + buffer_1[x + 1]) * 455) >> 10; // 1024/455 = 2.250549 - this.data[width * y + x] = (byte) (t > 255 ? 255 : t); + this.data.put(width * y + x, (byte) (t > 255 ? 255 : t)); x++; t = ((buffer_2[x - 2] + buffer_1[x - 1] + buffer[x]) * 585) >> 10; // 1024/585 = 1.75042 - this.data[width * y + x] = (byte) (t > 255 ? 255 : t); + this.data.put(width * y + x, (byte) (t > 255 ? 255 : t)); } // Now a vertical pass @@ -57,22 +61,22 @@ private void blur() { for (int x = 0; x < width; x++) { // Copy this vertical line for (int i = 0; i < height; i++) { - buffer[i] = this.data[width * i + x]; + buffer[i] = this.data.get(width * i + x) & 0xFF; buffer_1[i] = (byte) (buffer[i] >> 1); // buffer * 0.5 buffer_2[i] = (byte) (buffer[i] >> 2); // buffer * 0.25 } // y = 0 - this.data[x] = (byte) (((buffer[0] + buffer_1[1] + buffer_2[2]) * 585) >> 10); // 1024/585 = 1.75042 + this.data.put(x, (byte) (((buffer[0] + buffer_1[1] + buffer_2[2]) * 585) >> 10)); // 1024/585 = 1.75042 // y = 1 - this.data[width + x] = (byte) (((buffer_1[0] + buffer[1] + buffer_1[2] + buffer_2[3]) * 455) >> 10); // 1024/455 = 2.250549 + this.data.put(width + x, (byte) (((buffer_1[0] + buffer[1] + buffer_1[2] + buffer_2[3]) * 455) >> 10)); // 1024/455 = 2.250549 for (int y = 2; y < height - 2; y++) { long t = (((long) (buffer_2[y - 2] + buffer_1[y - 1] + buffer[y] + buffer_1[y + 1] + buffer_2[y + 2])) * 409) >> 10; - this.data[width * y + x] = (byte) (t > 255 ? 255 : t); // 1024/409 = 2.503 ~ 1 + 2*0.5 + 2*0.25 + this.data.put(width * y + x, (byte) (t > 255 ? 255 : t)); // 1024/409 = 2.503 ~ 1 + 2*0.5 + 2*0.25 } int y = height - 2; - this.data[width * y + x] = (byte) (((buffer_2[y - 2] + buffer_1[y - 1] + buffer[y] + buffer_1[y + 1]) * 455) >> 10) ; // 1024/455 = 2.250549 + this.data.put(width * y + x, (byte) (((buffer_2[y - 2] + buffer_1[y - 1] + buffer[y] + buffer_1[y + 1]) * 455) >> 10)); // 1024/455 = 2.250549 y++; - this.data[width * y + x] = (byte) (((buffer_2[y - 2] + buffer_1[y - 1] + buffer[y]) * 585) >> 10); // 1024/585 = 1.75042 + this.data.put(width * y + x, (byte) (((buffer_2[y - 2] + buffer_1[y - 1] + buffer[y]) * 585) >> 10)); // 1024/585 = 1.75042 } } @@ -80,7 +84,7 @@ private void blur() { private static final int RESIZE_WIDTH = 1024; private static final int RESIZE_HEIGHT = 1024; - private void loadImage(BufferedImage image) { + public void loadImage(BufferedImage image) { try { BufferedImage resizedImage = new BufferedImage(RESIZE_WIDTH, RESIZE_HEIGHT, BufferedImage.TYPE_BYTE_GRAY); Graphics2D g = resizedImage.createGraphics(); @@ -98,7 +102,8 @@ private void loadImage(BufferedImage image) { byte[] tempBuffer = new byte[totalPixels]; resizedImage.getRaster().getDataElements(0, 0, RESIZE_WIDTH, RESIZE_HEIGHT, tempBuffer); - data = tempBuffer; + data = ByteBuffer.allocateDirect(totalPixels); + data.put(tempBuffer); } catch (OutOfMemoryError e) { System.err.println("Died trying to allocate a buffer for image. Please increase heap size!!"); throw e; @@ -113,8 +118,8 @@ public int getHeight() { return height; } - public byte get(int x, int y) { - return this.data[width * y + x]; + public int get(int x, int y) { + return this.data.get(width * y + x) & 0xFF; } } \ No newline at end of file diff --git a/src/test/java/com/pragone/jphash/image/radial/RadialHashAlgorithmTest.java b/src/test/java/com/pragone/jphash/image/radial/RadialHashAlgorithmTest.java index c8cfd15..0506a53 100644 --- a/src/test/java/com/pragone/jphash/image/radial/RadialHashAlgorithmTest.java +++ b/src/test/java/com/pragone/jphash/image/radial/RadialHashAlgorithmTest.java @@ -60,7 +60,7 @@ public void testEarthResizedDistance() throws IOException { @Test public void testNotEarthDistance() throws IOException { - Assert.assertEquals(EARTH_RESIZED_DISTANCE, + Assert.assertEquals(0.4790905650101384, RadialHashAlgorithm.getSimilarity( getHashFor("earth1.jpg"), getHashFor("not_earth.jpg") From f8aa0160e3c955cbaf1f09c674366bc165c091a4 Mon Sep 17 00:00:00 2001 From: Damian Lesiuk Date: Sun, 12 Nov 2017 10:26:17 +0100 Subject: [PATCH 11/12] performance optimisations --- .../com/pragone/jphash/image/SimpleGrayscaleImage.java | 7 +------ .../pragone/jphash/image/radial/RadialHashAlgorithm.java | 8 ++------ .../jphash/image/radial/RadialHashAlgorithmTest.java | 2 +- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java b/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java index 43d924e..ea88e26 100644 --- a/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java +++ b/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java @@ -1,9 +1,7 @@ package com.pragone.jphash.image; import java.awt.*; -import java.awt.color.ColorSpace; import java.awt.image.BufferedImage; -import java.awt.image.ColorConvertOp; import java.nio.ByteBuffer; public class SimpleGrayscaleImage { @@ -91,8 +89,6 @@ public void loadImage(BufferedImage image) { g.drawImage(image, 0, 0, RESIZE_WIDTH, RESIZE_HEIGHT, null); g.dispose(); -// colorConvert.filter(resizedImage, resizedImage); - numPixels = (RESIZE_WIDTH * RESIZE_HEIGHT); width = RESIZE_WIDTH; height = RESIZE_HEIGHT; @@ -102,8 +98,7 @@ public void loadImage(BufferedImage image) { byte[] tempBuffer = new byte[totalPixels]; resizedImage.getRaster().getDataElements(0, 0, RESIZE_WIDTH, RESIZE_HEIGHT, tempBuffer); - data = ByteBuffer.allocateDirect(totalPixels); - data.put(tempBuffer); + data = ByteBuffer.wrap(tempBuffer, 0, totalPixels); } catch (OutOfMemoryError e) { System.err.println("Died trying to allocate a buffer for image. Please increase heap size!!"); throw e; diff --git a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java index cc296d0..ae0bbaf 100644 --- a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java +++ b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java @@ -34,17 +34,13 @@ public static RadialHash getHash(String file) throws IOException { } public static RadialHash getHash(File file) throws IOException { - FileInputStream is = new FileInputStream(file); - try { + try (FileInputStream is = new FileInputStream(file)) { return getHash(is); - } finally { - is.close(); } } public static RadialHash getHash(InputStream inputStream) throws IOException { - BufferedImage img = ImageIO.read(ImageIO.createImageInputStream(inputStream)); - return getHash(img); + return getHash(ImageIO.read(inputStream)); } public static RadialHash getHash(BufferedImage img) throws IOException { diff --git a/src/test/java/com/pragone/jphash/image/radial/RadialHashAlgorithmTest.java b/src/test/java/com/pragone/jphash/image/radial/RadialHashAlgorithmTest.java index 0506a53..430dda0 100644 --- a/src/test/java/com/pragone/jphash/image/radial/RadialHashAlgorithmTest.java +++ b/src/test/java/com/pragone/jphash/image/radial/RadialHashAlgorithmTest.java @@ -60,7 +60,7 @@ public void testEarthResizedDistance() throws IOException { @Test public void testNotEarthDistance() throws IOException { - Assert.assertEquals(0.4790905650101384, + Assert.assertEquals(0.42, RadialHashAlgorithm.getSimilarity( getHashFor("earth1.jpg"), getHashFor("not_earth.jpg") From b1d7e2c2b51558e40f42c9b2f3413b9d3bbe67a7 Mon Sep 17 00:00:00 2001 From: Damian Lesiuk Date: Wed, 15 Nov 2017 10:04:56 +0100 Subject: [PATCH 12/12] fix --- .../jphash/image/SimpleGrayscaleImage.java | 3 +- .../image/radial/RadialHashAlgorithm.java | 77 ++++++++++--------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java b/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java index ea88e26..34866c3 100644 --- a/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java +++ b/src/main/java/com/pragone/jphash/image/SimpleGrayscaleImage.java @@ -78,7 +78,6 @@ public void blur() { } } -// private ColorConvertOp colorConvert = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null); private static final int RESIZE_WIDTH = 1024; private static final int RESIZE_HEIGHT = 1024; @@ -98,7 +97,7 @@ public void loadImage(BufferedImage image) { byte[] tempBuffer = new byte[totalPixels]; resizedImage.getRaster().getDataElements(0, 0, RESIZE_WIDTH, RESIZE_HEIGHT, tempBuffer); - data = ByteBuffer.wrap(tempBuffer, 0, totalPixels); + data = ByteBuffer.wrap(tempBuffer); } catch (OutOfMemoryError e) { System.err.println("Died trying to allocate a buffer for image. Please increase heap size!!"); throw e; diff --git a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java index ae0bbaf..4f290eb 100644 --- a/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java +++ b/src/main/java/com/pragone/jphash/image/radial/RadialHashAlgorithm.java @@ -24,7 +24,7 @@ public class RadialHashAlgorithm { THETA_180 = new double[180]; TAN_THETA_180 = new double[180]; for (int i = 0; i < 180; i++) { - THETA_180[i] = i* Math.PI/180; + THETA_180[i] = i * Math.PI / 180; TAN_THETA_180[i] = Math.tan(THETA_180[i]); } } @@ -48,8 +48,7 @@ public static RadialHash getHash(BufferedImage img) throws IOException { Projections projections = calculate180Projections(grayscaleImage); // grayscaleImage.dispose(); Features features = calculateFeatures(projections); - RadialHash temp = calculateHash(features); - return temp; + return calculateHash(features); } private static RadialHash calculateHash(Features features) { @@ -65,25 +64,25 @@ private static RadialHash calculateHash(Features features) { double D_temp[] = new double[nb_coeffs]; double max = 0.0; double min = 0.0; - for (int k = 0;k max) max = D_temp[k]; if (D_temp[k] < min) min = D_temp[k]; } - for (int i=0;i height)?width:height; + int D = (width > height) ? width : height; int x_off = (width >> 1) + (width & 0x1); // round(width/2) but only with integer operations int y_off = (height >> 1) + (height & 0x1); // round(height/2) but only with integer operations - Projections projections = new Projections(N,D); + Projections projections = new Projections(N, D); int[][] ptr_radon_map = projections.projections; int[] nb_per_line = projections.nb_pix_perline; - for (int k=0;k= 0 ? 0.5 : -0.5)); - if ((yd + y_off >= 0)&&(yd + y_off < height) && (x < width)) { + for (int x = 0; x < D; x++) { + double y = alpha * (x - x_off); + int yd = (int) Math.floor(y + (y >= 0 ? 0.5 : -0.5)); + if ((yd + y_off >= 0) && (yd + y_off < height) && (x < width)) { ptr_radon_map[k][x] = img.get(x, yd + y_off); nb_per_line[k] += 1; } - if ((yd + x_off >= 0) && (yd + x_off < width) && (k != N/4) && (x < height)) { - ptr_radon_map[N/2-k][x] = img.get(yd + x_off, x); - nb_per_line[N/2-k] += 1; + if ((yd + x_off >= 0) && (yd + x_off < width) && (k != N / 4) && (x < height)) { + ptr_radon_map[N / 2 - k][x] = img.get(yd + x_off, x); + nb_per_line[N / 2 - k] += 1; } } } - int j= 0; - for (int k=3*N/4;k= 0 ? 0.5 : -0.5)); - if ((yd + y_off >= 0)&&(yd + y_off < height) && (x < width)){ + for (int x = 0; x < D; x++) { + double y = alpha * (x - x_off); + int yd = (int) Math.floor(y + (y >= 0 ? 0.5 : -0.5)); + if ((yd + y_off >= 0) && (yd + y_off < height) && (x < width)) { ptr_radon_map[k][x] = img.get(x, yd + y_off); nb_per_line[k] += 1; } - if ((y_off - yd >= 0)&&(y_off - yd=0)&&(2*y_off-x= 0) && (y_off - yd < width) && (2 * y_off - x >= 0) && (2 * y_off - x < height) && (k != 3 * N / 4)) { + ptr_radon_map[k - j][x] = img.get(-yd + y_off, -(x - y_off) + y_off); + nb_per_line[k - j] += 1; } } @@ -180,29 +179,31 @@ public static double getSimilarity(RadialHash hash1, RadialHash hash2) { double sumx = 0.0; double sumy = 0.0; - for (int i=0;i < N;i++){ + for (int i = 0; i < N; i++) { sumx += x_coeffs[i] & 0xFF; sumy += y_coeffs[i] & 0xFF; } - double meanx = sumx/N; - double meany = sumy/N; + + double meanx = sumx / N; + double meany = sumy / N; double[] r = new double[N]; double max = 0; - for (int d=0;d max) max = r[d]; }