Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Could you check if this PerceptualHash.compare algorithm in my version is correct? #14

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions example/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,30 @@ import 'package:image_compare/image_compare.dart';

void main(List<String> arguments) async {
var url1 =
'https://www.tompetty.com/sites/g/files/g2000007521/f/sample_01.jpg';
'https://cdn.pixabay.com/photo/2018/04/22/19/16/marguerite-daisy-3342050_1280.jpg';
var url2 =
'https://fujifilm-x.com/wp-content/uploads/2019/08/x-t30_sample-images03.jpg';

var file1 = File('../images/drawings/kolam1.png');
var file2 = File('../images/drawings/scribble1.png');
var file1 = File('images/drawings/kolam1.png');
var file2 = File('images/drawings/scribble1.png');

var bytes1 = File('../images/animals/koala.jpg').readAsBytesSync();
var bytes2 = File('../images/animals/komodo.jpg').readAsBytesSync();
var bytes1 = File('images/animals/koala.jpg').readAsBytesSync();
var bytes2 = File('images/animals/komodo.jpg').readAsBytesSync();

var image1 = decodeImage(bytes1);
var image2 = decodeImage(bytes2);

var assetImages = [
File('../images/animals/bunny.jpg'),
File('../images/objects/red_apple.png'),
File('../images/animals/tiger.jpg')
File('images/animals/bunny.jpg'),
File('images/objects/red_apple.png'),
File('images/animals/tiger.jpg')
];

var networkImages = [
Uri.parse(
'https://fujifilm-x.com/wp-content/uploads/2019/08/x-t30_sample-images03.jpg'),
Uri.parse(
'https://hs.sbcounty.gov/cn/Photo%20Gallery/Sample%20Picture%20-%20Koala.jpg'),
'https://cdn.pixabay.com/photo/2015/07/21/15/19/koala-854021_1280.jpg'),
Uri.parse(
'https://c.files.bbci.co.uk/12A9B/production/_111434467_gettyimages-1143489763.jpg'),
];
Expand All @@ -38,37 +38,37 @@ void main(List<String> arguments) async {
src2: Uri.parse(url2),
algorithm: ChiSquareDistanceHistogram());

print('Difference: ${networkResult * 100}%');
print('Difference of network images with Chi Square: ${networkResult * 100}%');

// Calculate IMED between two asset images
var assetResult = await compareImages(
src1: image1, src2: image2, algorithm: IMED(blurRatio: 0.001));

print('Difference: ${assetResult * 100}%');
print('Difference of asset images with IMED: ${assetResult * 100}%');

// Calculate intersection histogram difference between two bytes of images
var byteResult = await compareImages(
src1: bytes1, src2: bytes2, algorithm: IntersectionHistogram());

print('Difference: ${byteResult * 100}%');
print('Difference of byte images with Intersection Histogram: ${byteResult * 100}%');

// Calculate euclidean color distance between two images
var imageResult = await compareImages(
src1: file1, src2: file2, algorithm: EuclideanColorDistance(ignoreAlpha: true));

print('Difference: ${imageResult * 100}%');
print('Difference of image files with Euclidean Color Distance: ${imageResult * 100}%');

// Calculate pixel matching between one network and one asset image
var networkAssetResult =
await compareImages(src1: Uri.parse(url2), src2: image2, algorithm: PixelMatching(tolerance: 0.1));

print('Difference: ${networkAssetResult * 100}%');
print('Difference of network and asset images with Pixel Matching: ${networkAssetResult * 100}%');

// Calculate median hash between a byte array and image
var byteImageResult =
await compareImages(src1: image1, src2: bytes2, algorithm: MedianHash());

print('Difference: ${byteImageResult * 100}%');
print('Difference of byte image and image with Median Hash: ${byteImageResult * 100}%');

// Calculate average hash difference between a network image
// and a list of network images
Expand All @@ -78,15 +78,15 @@ void main(List<String> arguments) async {
algorithm: AverageHash(),
);

networkResults.forEach((e) => print('Difference: ${e * 100}%'));
networkResults.forEach((e) => print('Difference of network images with Average Hash: ${e * 100}%'));

// Calculate perceptual hash difference between an asset image
// and a list of asset iamges
var assetResults = await listCompare(
target: File('../images/animals/deer.jpg'),
target: File('images/animals/deer.jpg'),
list: assetImages,
algorithm: PerceptualHash(),
);

assetResults.forEach((e) => print('Difference: ${e * 100}%'));
assetResults.forEach((e) => print('Difference of asset images with Perceptual Hash: ${e * 100}%'));
}
127 changes: 58 additions & 69 deletions lib/src/algorithms.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:image/image.dart';
/// Abstract class for all algorithms
abstract class Algorithm {
/// [Pair] of [Pixel] lists for [src1] and [src2]
var _pixelListPair;
Pair<List<Pixel>, List<Pixel>> _pixelListPair = Pair([], []);

/// Default constructor gets implicitly called on subclass instantiation
Algorithm();
Expand Down Expand Up @@ -97,7 +97,7 @@ abstract class DirectAlgorithm extends Algorithm {
/// * Returns percentage difference (0.0 - no difference, 1.0 - 100% difference)
class EuclideanColorDistance extends DirectAlgorithm {
/// Ignores alpha channel when computing difference
var ignoreAlpha;
bool ignoreAlpha;

EuclideanColorDistance({bool this.ignoreAlpha = false});

Expand Down Expand Up @@ -157,15 +157,15 @@ class EuclideanColorDistance extends DirectAlgorithm {
/// * Returns percentage diffence (0.0 - no difference, 1.0 - 100% difference)
class PixelMatching extends DirectAlgorithm {
/// Ignores alpha channel when computing difference
var ignoreAlpha;
bool ignoreAlpha;

/// Percentage tolerance value between 0.0 and 1.0
/// of the range of RGB values, 256, used when directly
/// comparing pixels for equivalence.
///
/// A value of 0.05 means that one RGB value can be + or -
/// (0.05 * 256) of another RGB value.
var tolerance;
double tolerance;

PixelMatching({bool this.ignoreAlpha = false, this.tolerance = 0.05});

Expand Down Expand Up @@ -203,7 +203,7 @@ class PixelMatching extends DirectAlgorithm {
return 1 - (count / numPixels);
}

bool _withinRange(var delta, var value, var target) {
bool _withinRange(double delta, int value, int target) {
return (target - delta <= value && value <= target + delta);
}

Expand All @@ -229,7 +229,7 @@ class PixelMatching extends DirectAlgorithm {
/// * Returns percentage difference (0.0 - no difference, 1.0 - 100% difference)
class IMED extends DirectAlgorithm {
/// Width parameter of the guassian function
var sigma;
double sigma;

/// Percentage of the smaller image's width representing a
/// component of the window (box) width used for the gaussian blur.
Expand All @@ -240,7 +240,7 @@ class IMED extends DirectAlgorithm {
///
/// Note: Large [blurRatio] values can lead to a long computation time
/// for comparisons.
var blurRatio;
double blurRatio;

IMED({double this.sigma = 1, double this.blurRatio = 0.005});

Expand Down Expand Up @@ -310,12 +310,12 @@ class IMED extends DirectAlgorithm {

/// Helper function to return grayscale value of a pixel
int _grayValue(Pixel p) {
return getLuminanceRgb(p._red, p._green, p._blue);
return getLuminanceRgb(p._red, p._green, p._blue).round();
}

/// Helper function to return distance between two pixels at
/// indices [i] and [j]
double _distance(var i, var j, var width) {
double _distance(int i, int j, int width) {
var distance = 0.0;
var pointA = Pair((i % width), (i / width));
var pointB = Pair((j % width), (j / width));
Expand Down Expand Up @@ -374,10 +374,27 @@ class PerceptualHash extends HashAlgorithm {
///Resize and grayscale images
@override
double compare(Image src1, Image src2) {
src1 = copyResize(src1, height: 32, width: 32);
src2 = copyResize(src2, height: 32, width: 32);

super.compare(src1, src2);
src1 = copyResize(grayscale(src1), width: _size, height: _size);
src2 = copyResize(grayscale(src2), width: _size, height: _size);

// Pixel representation of [src1] and [src2]
_pixelListPair = Pair([], []);

// Bytes for [src1] and [src2]
List<int> bytes1 = src1.getBytes();
List<int> bytes2 = src2.getBytes();
final bytesPerPixel = 4;

for (int i = 0; i <= bytes1.length - bytesPerPixel; i += bytesPerPixel) {
_pixelListPair._first
.add(Pixel(bytes1[i], bytes1[i + 1], bytes1[i + 2], bytes1[i + 3]));
}

for (int i = 0; i <= bytes2.length - bytesPerPixel; i += bytesPerPixel) {
_pixelListPair._second
.add(Pixel(bytes2[i], bytes2[i + 1], bytes2[i + 2], bytes2[i + 3]));
}

var hash1 = calcPhash(_pixelListPair._first);
var hash2 = calcPhash(_pixelListPair._second);
Expand All @@ -386,70 +403,52 @@ class PerceptualHash extends HashAlgorithm {
}

/// Helper function which computes a binary hash of a [List] of [Pixel]
String calcPhash(List pixelList) {
String calcPhash(List<Pixel> pixelList) {
var bitString = '';
var matrix = List<dynamic>.filled(32, 0);
var row = List<dynamic>.filled(32, 0);
var rows = List<dynamic>.filled(32, 0);
var col = List<dynamic>.filled(32, 0);

var data = unit8ListToMatrix(pixelList); //returns a matrix used for DCT
var matrix = List.generate(_size, (_) => List.filled(_size, 0.0));
var data = pixelListToMatrix(pixelList);
var rows = List.generate(_size, (_) => List.filled(_size, 0.0));

// Apply DCT on each row
for (var y = 0; y < _size; y++) {
for (var x = 0; x < _size; x++) {
var color = data[x][y];

row[x] = getLuminanceRgb(color._red, color._green, color._blue);
}

var row = List.generate(_size, (x) => getLuminanceRgb(data[x][y]._red, data[x][y]._green, data[x][y]._blue).toDouble());
rows[y] = calculateDCT(row);
}

// Apply DCT on each column of the result from previous step
for (var x = 0; x < _size; x++) {
var col = List.generate(_size, (y) => rows[y][x]);
var colTransformed = calculateDCT(col);
for (var y = 0; y < _size; y++) {
col[y] = rows[y][x];
matrix[y][x] = colTransformed[y];
}

matrix[x] = calculateDCT(col);
}

// Extract the top 8x8 pixels.
var pixels = [];

for (var y = 0; y < 8; y++) {
for (var x = 0; x < 8; x++) {
pixels.add(matrix[y][x]);
}
}
var pixels = matrix.expand((i) => i).take(64).toList();
var averageValue = average(pixels);

// Calculate hash.
var bits = [];
var compare = average(pixels);

for (var pixel in pixels) {
bits.add(pixel > compare ? 1 : 0);
bitString += (pixel > averageValue) ? '1' : '0';
}

bits.forEach((element) {
bitString += (1 * element).toString();
});

return BigInt.parse(bitString, radix: 2).toRadixString(16);
}

///Helper funciton to compute the average of an array after dct caclulations
num average(List pixels) {
num average(List<double> pixels) {
// Calculate the average value from top 8x8 pixels, except for the first one.
var n = pixels.length - 1;
return pixels.sublist(1, n).reduce((a, b) => a + b) / n;
}

///Helper function to perform 1D discrete cosine tranformation on a matrix
List calculateDCT(List matrix) {
var transformed = List<num>.filled(32, 0);
var _size = matrix.length;
List<double> calculateDCT(List<double> matrix) {
var transformed = List<double>.filled(_size, 0.0);

for (var i = 0; i < _size; i++) {
num sum = 0;
double sum = 0;

for (var j = 0; j < _size; j++) {
sum += matrix[j] * cos((i * pi * (j + 0.5)) / _size);
Expand All @@ -467,25 +466,15 @@ class PerceptualHash extends HashAlgorithm {
return transformed;
}

///Helper function to convert a Unit8List to a nD matrix
List unit8ListToMatrix(List pixelList) {
var copy = pixelList.sublist(0);
pixelList.clear();

for (var r = 0; r < _size; r++) {
var res = [];
for (var c = 0; c < _size; c++) {
var i = r * _size + c;

if (i < copy.length) {
res.add(copy[i]);
}
}

pixelList.add(res);
///Helper function to convert a list of pixels to a matrix
List<List<Pixel>> pixelListToMatrix(List<Pixel> pixelList) {
var matrix = List.generate(_size, (_) => List<Pixel>.filled(_size, Pixel(0, 0, 0, 0), growable: false), growable: false);
for (var i = 0; i < pixelList.length; i++) {
var x = i % _size;
var y = i ~/ _size;
matrix[y][x] = pixelList[i];
}

return pixelList;
return matrix;
}

@override
Expand Down Expand Up @@ -650,7 +639,7 @@ abstract class HistogramAlgorithm extends Algorithm {
/// Organizational class for storing [src1] and [src2] data.
/// Fields are RGBA histograms (256 element lists)
class RGBAHistogram {
final _binSize;
final int _binSize;
late List redHist;
late List greenHist;
late List blueHist;
Expand Down Expand Up @@ -678,7 +667,7 @@ class RGBAHistogram {
/// * Returns percentage difference (0.0 - no difference, 1.0 - 100% difference)
class ChiSquareDistanceHistogram extends HistogramAlgorithm {
/// Ignores alpha channel when computing difference
var ignoreAlpha;
bool ignoreAlpha;

ChiSquareDistanceHistogram({bool this.ignoreAlpha = false});

Expand Down Expand Up @@ -740,7 +729,7 @@ class ChiSquareDistanceHistogram extends HistogramAlgorithm {
/// * Returns percentage diffence (0.0 - no difference, 1.0 - 100% difference)
class IntersectionHistogram extends HistogramAlgorithm {
/// Ignores alpha channel when computing difference
var ignoreAlpha;
bool ignoreAlpha;

IntersectionHistogram({bool this.ignoreAlpha = false});

Expand Down
16 changes: 13 additions & 3 deletions lib/src/functions.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'algorithms.dart';
import 'package:image/image.dart';
import 'package:universal_io/io.dart';
import 'dart:typed_data';

/// Compare images from [src1] and [src2] with a specified [algorithm].
/// If [algorithm] is not specified, the default (PixelMatching()) is supplied.
Expand Down Expand Up @@ -88,9 +89,17 @@ Future<Image> _getImageFromDynamic(var src) async {

err += '$list<...>';
} else if (src is Image) {
err += '$src. $src.data.length != width * height';
// Construct an error message with detailed information about the mismatch.
err += 'Expected data length for an image of size ${src.width}x${src.height} is either ${src.width * src.height * 3} (for RGB) or ${src.width * src.height * 4} (for RGBA), but found ${src.data?.length ?? 0}.';

if (src.height * src.width != src.data.length) {
// Calculate the expected data lengths for both RGB and RGBA formats.
int expectedLengthRGB = src.width * src.height * 3; // 3 bytes per pixel for RGB
int expectedLengthRGBA = src.width * src.height * 4; // 4 bytes per pixel for RGBA

// Check if the actual data length matches either the RGB or RGBA format.
// Throw a FormatException if there is a mismatch.
int actualLength = src.data?.length ?? 0;
if (actualLength != expectedLengthRGB && actualLength != expectedLengthRGBA) {
throw FormatException(err);
}

Expand All @@ -111,7 +120,8 @@ Future<Image> _getImageFromDynamic(var src) async {
Image _getValidImage(List<int> bytes, String err) {
var image;
try {
image = decodeImage(bytes);
Uint8List uint8list = Uint8List.fromList(bytes);
image = decodeImage(uint8list);
} catch (Exception) {
throw FormatException("Insufficient data provided to identify image.");
}
Expand Down
Loading