diff --git a/example/main.dart b/example/main.dart index 51d44c4..8aa7371 100644 --- a/example/main.dart +++ b/example/main.dart @@ -4,30 +4,30 @@ import 'package:image_compare/image_compare.dart'; void main(List 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'), ]; @@ -38,37 +38,37 @@ void main(List 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 @@ -78,15 +78,15 @@ void main(List 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}%')); } diff --git a/lib/src/algorithms.dart b/lib/src/algorithms.dart index cb205c7..718a1ac 100644 --- a/lib/src/algorithms.dart +++ b/lib/src/algorithms.dart @@ -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> _pixelListPair = Pair([], []); /// Default constructor gets implicitly called on subclass instantiation Algorithm(); @@ -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}); @@ -157,7 +157,7 @@ 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 @@ -165,7 +165,7 @@ class PixelMatching extends DirectAlgorithm { /// /// 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}); @@ -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); } @@ -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. @@ -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}); @@ -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)); @@ -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 bytes1 = src1.getBytes(); + List 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); @@ -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 pixelList) { var bitString = ''; - var matrix = List.filled(32, 0); - var row = List.filled(32, 0); - var rows = List.filled(32, 0); - var col = List.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 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.filled(32, 0); - var _size = matrix.length; + List calculateDCT(List matrix) { + var transformed = List.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); @@ -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> pixelListToMatrix(List pixelList) { + var matrix = List.generate(_size, (_) => List.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 @@ -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; @@ -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}); @@ -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}); diff --git a/lib/src/functions.dart b/lib/src/functions.dart index b9ff66d..d70fec1 100644 --- a/lib/src/functions.dart +++ b/lib/src/functions.dart @@ -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. @@ -88,9 +89,17 @@ Future _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); } @@ -111,7 +120,8 @@ Future _getImageFromDynamic(var src) async { Image _getValidImage(List 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."); } diff --git a/pubspec.lock b/pubspec.lock index 0e94cc2..cc2ea78 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,78 +5,121 @@ packages: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.4.10" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" image: dependency: "direct main" description: name: image - url: "https://pub.dartlang.org" + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + url: "https://pub.dev" + source: hosted + version: "4.1.7" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "0.7.1" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.12.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb" + url: "https://pub.dev" source: hosted version: "1.8.0" - pedantic: - dependency: "direct dev" - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "3.7.4" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + url: "https://pub.dev" source: hosted version: "1.3.0" universal_io: dependency: "direct main" description: name: universal_io - url: "https://pub.dartlang.org" + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.2.2" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" source: hosted - version: "5.1.2" + version: "6.5.0" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=3.2.6 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 5bf0fc5..f9ad8ed 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,17 +1,17 @@ name: image_compare description: Dart package for image comparison. Compare images for difference using a variety of algorithms. -repository: https://github.com/nitinramadoss/image_compare -version: 1.1.2 +repository: https://github.com/southglory/image_compare +version: 1.1.3 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=3.2.6 <4.0.0" dependencies: - image: ^3.0.0 - universal_io: ^2.0.0 + image: '>=4.0.17 <5.0.0' + universal_io: '>=2.2.2 <3.0.0' dev_dependencies: - pedantic: ^1.9.0 + flutter_lints: '>=2.0.0 <3.0.0' assets: - assets/