155 lines
5.0 KiB
Java
155 lines
5.0 KiB
Java
package ru.soune.nocopy.service;
|
|||
|
|
|
||
|
|
import lombok.RequiredArgsConstructor;
|
||
|
|
import lombok.extern.slf4j.Slf4j;
|
||
|
|
import org.springframework.stereotype.Service;
|
||
|
|
import ru.soune.nocopy.entity.file.FileEntity;
|
||
|
|
import ru.soune.nocopy.entity.file.ImageHashEntity;
|
||
|
|
import ru.soune.nocopy.repository.ImageHashRepository;
|
||
|
|
|
||
|
|
import javax.imageio.ImageIO;
|
||
|
|
import java.awt.*;
|
||
|
|
import java.awt.image.BufferedImage;
|
||
|
|
import java.io.IOException;
|
||
|
|
import java.io.InputStream;
|
||
|
|
import java.nio.file.Files;
|
||
|
|
import java.nio.file.Path;
|
||
|
|
import java.time.LocalDateTime;
|
||
|
|
import java.util.Arrays;
|
||
|
|
|
||
|
|
@Service
|
||
|
|
@RequiredArgsConstructor
|
||
|
|
@Slf4j
|
||
|
|
public class ImageHashService {
|
||
|
|
|
||
|
|
private final ImageHashRepository repository;
|
||
|
|
|
||
|
|
public void create(FileEntity file, Path imagePath) {
|
||
|
|
try {
|
||
|
|
long hash64 = computePhash64(imagePath);
|
||
|
|
|
||
|
|
int hi = high32(hash64);
|
||
|
|
int lo = low32(hash64);
|
||
|
|
|
||
|
|
ImageHashEntity entity = ImageHashEntity.builder()
|
||
|
|
.file(file)
|
||
|
|
.hash64Hi(hi)
|
||
|
|
.hash64Lo(lo)
|
||
|
|
.hashAlgorithm("PHASH64")
|
||
|
|
.createdAt(LocalDateTime.now())
|
||
|
|
.build();
|
||
|
|
|
||
|
|
repository.save(entity);
|
||
|
|
} catch (IOException e) {
|
||
|
|
throw new RuntimeException("Failed to compute image hash", e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private long computePhash64(Path path) throws IOException {
|
||
|
|
try (InputStream is = Files.newInputStream(path)) {
|
||
|
|
BufferedImage image = ImageIO.read(is);
|
||
|
|
if (image == null) throw new IOException("Cannot read image file: " + path);
|
||
|
|
|
||
|
|
BufferedImage gray = toGrayScale(image);
|
||
|
|
BufferedImage resized = resizeImage(gray, 32, 32);
|
||
|
|
|
||
|
|
double[][] dct = applyDCT(resized);
|
||
|
|
double[][] topLeft = extractTopLeft8x8(dct);
|
||
|
|
double median = calculateMedian(topLeft, true);
|
||
|
|
|
||
|
|
return create64BitHashLong(topLeft, median);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private BufferedImage toGrayScale(BufferedImage image) {
|
||
|
|
BufferedImage gray = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
|
||
|
|
Graphics2D g = gray.createGraphics();
|
||
|
|
g.drawImage(image, 0, 0, null);
|
||
|
|
g.dispose();
|
||
|
|
return gray;
|
||
|
|
}
|
||
|
|
|
||
|
|
private BufferedImage resizeImage(BufferedImage image, int width, int height) {
|
||
|
|
BufferedImage resized = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
|
||
|
|
Graphics2D g = resized.createGraphics();
|
||
|
|
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||
|
|
g.drawImage(image, 0, 0, width, height, null);
|
||
|
|
g.dispose();
|
||
|
|
return resized;
|
||
|
|
}
|
||
|
|
|
||
|
|
private double[][] applyDCT(BufferedImage image) {
|
||
|
|
int w = image.getWidth();
|
||
|
|
int h = image.getHeight();
|
||
|
|
double[][] pixels = new double[h][w];
|
||
|
|
|
||
|
|
for (int y = 0; y < h; y++) {
|
||
|
|
for (int x = 0; x < w; x++) {
|
||
|
|
int gray = image.getRGB(x, y) & 0xFF;
|
||
|
|
pixels[y][x] = gray;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
double[][] dct = new double[h][w];
|
||
|
|
for (int u = 0; u < h; u++) {
|
||
|
|
double au = u == 0 ? 1.0 / Math.sqrt(h) : Math.sqrt(2.0 / h);
|
||
|
|
for (int v = 0; v < w; v++) {
|
||
|
|
double av = v == 0 ? 1.0 / Math.sqrt(w) : Math.sqrt(2.0 / w);
|
||
|
|
double sum = 0.0;
|
||
|
|
for (int x = 0; x < h; x++) {
|
||
|
|
for (int y = 0; y < w; y++) {
|
||
|
|
sum += pixels[x][y] *
|
||
|
|
Math.cos((2 * x + 1) * u * Math.PI / (2.0 * h)) *
|
||
|
|
Math.cos((2 * y + 1) * v * Math.PI / (2.0 * w));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
dct[u][v] = au * av * sum;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return dct;
|
||
|
|
}
|
||
|
|
|
||
|
|
private double[][] extractTopLeft8x8(double[][] dct) {
|
||
|
|
double[][] block = new double[8][8];
|
||
|
|
for (int i = 0; i < 8; i++) System.arraycopy(dct[i], 0, block[i], 0, 8);
|
||
|
|
return block;
|
||
|
|
}
|
||
|
|
|
||
|
|
private double calculateMedian(double[][] block, boolean excludeDC) {
|
||
|
|
double[] vals = new double[63];
|
||
|
|
int idx = 0;
|
||
|
|
for (int i = 0; i < 8; i++) {
|
||
|
|
for (int j = 0; j < 8; j++) {
|
||
|
|
if (i == 0 && j == 0 && excludeDC) continue;
|
||
|
|
vals[idx++] = block[i][j];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Arrays.sort(vals);
|
||
|
|
return (vals.length % 2 == 0) ?
|
||
|
|
(vals[vals.length / 2 - 1] + vals[vals.length / 2]) / 2.0 :
|
||
|
|
vals[vals.length / 2];
|
||
|
|
}
|
||
|
|
|
||
|
|
private long create64BitHashLong(double[][] block, double median) {
|
||
|
|
long hash = 0L;
|
||
|
|
|
||
|
|
for (int i = 0; i < 8; i++) {
|
||
|
|
for (int j = 0; j < 8; j++) {
|
||
|
|
if (i == 0 && j == 0) continue;
|
||
|
|
hash <<= 1;
|
||
|
|
if (block[i][j] > median) hash |= 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return hash;
|
||
|
|
}
|
||
|
|
|
||
|
|
private int high32(long hash64) {
|
||
|
|
return (int) (hash64 >>> 32);
|
||
|
|
}
|
||
|
|
|
||
|
|
private int low32(long hash64) {
|
||
|
|
return (int) hash64;
|
||
|
|
}
|
||
|
|
}
|