From 5ebbcf28454cdf26ff95f8e0c881adff353113dc Mon Sep 17 00:00:00 2001 From: simon987 Date: Thu, 26 Dec 2019 16:58:32 -0500 Subject: [PATCH] Add multi_hash (wip/untested) --- README.md | 31 +++++- bench/README.md | 4 + bench/benchmark.py | 13 +++ bench/results/multi_large.png | Bin 0 -> 17671 bytes bench/results/multi_small.png | Bin 0 -> 16164 bytes bench/run.py | 2 + benchmark.cpp | 18 ++++ fastimagehash.cpp | 172 +++++++++++++++++++++++++++++++++- fastimagehash.h | 15 +++ imhash.c | 12 +++ 10 files changed, 259 insertions(+), 8 deletions(-) create mode 100644 bench/results/multi_large.png create mode 100644 bench/results/multi_small.png diff --git a/README.md b/README.md index 52450ae..67ddb39 100644 --- a/README.md +++ b/README.md @@ -13,21 +13,42 @@ replacement for C/C++.

-*[\*benchmarks](bench/)* - ### Example usage -```C++ +```C #include "fastimagehash.h" int main() { - // TODO + unsigned char result[HASH_SIZE]; + + phash_file("image.jpeg", result, HASH_SIZE, HIGHFREQ_FACTOR); } ``` +For slight additional performance gains, `libfastimagehash` can +compute all hashes at once instead of decoding the same +image at each step. +

+ +

+ +*[\*See all benchmarks](bench/)* + + ### Build from source -// TODO +```bash +# Download dependencies +apt install libopencv-dev libfftw3-dev cmake + +# Checkout source +git clone --recursive https://github.com/simon987/fastimagehash + +# Build +cmake . +make +``` + **Built with** * [opencv](https://github.com/opencv) for image decoding & resizing diff --git a/bench/README.md b/bench/README.md index 52e78ad..7d4842c 100644 --- a/bench/README.md +++ b/bench/README.md @@ -29,3 +29,7 @@ fastimagehash v0.1 **ahash** ![ahash_s](results/ahash_small.png) ![ahash_l](results/ahash_large.png) + +**multi_hash** +![multi_s](results/multi_small.png) +![multi_l](results/multi_large.png) diff --git a/bench/benchmark.py b/bench/benchmark.py index d1ceb76..b937dce 100644 --- a/bench/benchmark.py +++ b/bench/benchmark.py @@ -36,3 +36,16 @@ print_result("ahash", timeit.timeit( stmt="average_hash(Image.open('%s'), hash_size=%d)" % (IMAGE, SIZE), number=COUNT )) + +print_result("multi", timeit.timeit( + setup="from imagehash import average_hash,phash,whash,dhash \n" + "from PIL import Image", + stmt="im = Image.open('%s');" + "size = %d;" + "average_hash(im.copy(), hash_size=size);" + "phash(im.copy(), hash_size=size);" + "whash(im.copy(), hash_size=size, remove_max_haar_ll=False);" + "dhash(im.copy(), hash_size=size);" + % (IMAGE, SIZE), + number=COUNT +)) diff --git a/bench/results/multi_large.png b/bench/results/multi_large.png new file mode 100644 index 0000000000000000000000000000000000000000..3d86b5577ff19c0a63d24d697084abcf4f1402fe GIT binary patch literal 17671 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 Q`yE13R8FK^$ROyy0flhervLx| literal 0 HcmV?d00001 diff --git a/bench/run.py b/bench/run.py index 6abda3b..1fb061b 100644 --- a/bench/run.py +++ b/bench/run.py @@ -34,4 +34,6 @@ for f in files: method = "ahash" if "whash" in m: method = "whash" + if "multi" in m: + method = "multi" print("%s_%s,%s" % (f, method, t)) diff --git a/benchmark.cpp b/benchmark.cpp index 6b95add..b5f7def 100644 --- a/benchmark.cpp +++ b/benchmark.cpp @@ -67,10 +67,28 @@ static void BM_ahash(benchmark::State &state) { free(buf); } +static void BM_multi(benchmark::State &state) { + + size_t size; + void *buf = load_test_file(&size); + + multi_hash_t *m = multi_hash_create(state.range()); + + for (auto _ : state) { + multi_hash_file(filepath, m, state.range(), 4, 0); + } + + multi_hash_destroy(m); + + free(buf); +} + + BENCHMARK(BM_phash)->ArgName("size")->Arg(8); BENCHMARK(BM_whash)->ArgName("size")->Arg(8); BENCHMARK(BM_dhash)->ArgName("size")->Arg(8); BENCHMARK(BM_ahash)->ArgName("size")->Arg(8); +BENCHMARK(BM_multi)->ArgName("size")->Arg(8); int main(int argc, char **argv) { diff --git a/fastimagehash.cpp b/fastimagehash.cpp index 08e1344..3cb2750 100644 --- a/fastimagehash.cpp +++ b/fastimagehash.cpp @@ -119,7 +119,7 @@ int ahash_mem(void *buf, uchar *out, size_t buf_len, int hash_size) { uchar *pixel = im.ptr(0); int endPixel = im.cols * im.rows; - for (int i = 0; i <= endPixel; i++) { + for (int i = 0; i < endPixel; i++) { set_bit_at(out, i, pixel[i] > avg); } return 0; @@ -213,7 +213,7 @@ int whash_mem(void *buf, uchar *out, size_t buf_len, int hash_size, int img_scal uchar *pixel = im.ptr(0); const int endPixel = im.cols * im.rows; - for (int i = 0; i <= endPixel; i++) { + for (int i = 0; i < endPixel; i++) { data[i] = (double) pixel[i] / 255; } @@ -265,7 +265,7 @@ int phash_mem(void *buf, uchar *out, size_t buf_len, int hash_size, int highfreq uchar *pixel = im.ptr(0); int endPixel = im.cols * im.rows; - for (int i = 0; i <= endPixel; i++) { + for (int i = 0; i < endPixel; i++) { pixels[i] = (double) pixel[i] / 255; } @@ -302,3 +302,169 @@ int phash_mem(void *buf, uchar *out, size_t buf_len, int hash_size, int highfreq return 0; } +multi_hash_t *multi_hash_create(int hash_size) { + auto multi_hash = (multi_hash_t *) malloc(sizeof(multi_hash_t)); + auto data = (uchar *) malloc((hash_size + 1) * 4); + + multi_hash->ahash = data; + multi_hash->phash = data + (hash_size + 1); + multi_hash->dhash = data + (hash_size + 1) * 2; + multi_hash->whash = data + (hash_size + 1) * 3; + + return multi_hash; +} + +void multi_hash_destroy(multi_hash_t *h) { + free(h->ahash); + free(h); +} + +int multi_hash_file(const char *filepath, multi_hash_t *out, int hash_size, + int ph_highfreq_factor, int wh_img_scale) { + + size_t size; + void *buf = load_file_in_mem(filepath, &size); + + if (buf == nullptr) { + return FASTIMAGEHASH_ERR; + } + + int ret = multi_hash_mem(buf, out, size, hash_size, ph_highfreq_factor, wh_img_scale); + free(buf); + return ret; +} + +int multi_hash_mem(void *buf, multi_hash_t *out, size_t buf_len, + int hash_size, int ph_highfreq_factor, int wh_img_scale) { + + Mat im; + try { + im = imdecode(Mat(1, buf_len, CV_8UC1, buf), IMREAD_GRAYSCALE); + } catch (Exception &e) { + return FASTIMAGEHASH_ERR; + } + + Mat ahash_im; + Mat dhash_im; + Mat phash_im; + Mat whash_im; + + int ph_img_scale = hash_size * ph_highfreq_factor; + + if ((hash_size & (hash_size - 1)) != 0) { + throw std::invalid_argument("hash_size must be a power of two"); + } + + if (wh_img_scale != 0) { + if ((wh_img_scale & (wh_img_scale - 1)) != 0) { + throw std::invalid_argument("wh_img_scale must be a power of two"); + } + } else { + int image_natural_scale = (int) pow(2, (int) log2(MIN(im.rows, im.cols))); + wh_img_scale = MAX(image_natural_scale, hash_size); + } + + int ll_max_level = (int) log2(wh_img_scale); + int level = (int) log2(hash_size); + + if (ll_max_level < level) { + throw std::invalid_argument("hash_size in a wrong range"); + } + + int dwt_level = ll_max_level - level; + + try { + im = imdecode(Mat(1, buf_len, CV_8UC1, buf), IMREAD_GRAYSCALE); + + resize(im, ahash_im, Size(hash_size, hash_size), 0, 0, INTER_AREA); + resize(im, dhash_im, Size(hash_size + 1, hash_size), 0, 0, INTER_AREA); + resize(im, whash_im, Size(wh_img_scale, wh_img_scale), 0, 0, INTER_AREA); + resize(im, phash_im, Size(ph_img_scale, ph_img_scale), 0, 0, INTER_AREA); + + } catch (Exception &e) { + return FASTIMAGEHASH_ERR; + } + + double *pixels = new double[MAX(ph_img_scale, wh_img_scale) * MAX(ph_img_scale, wh_img_scale)]; + + // ahash + double avg = mean(ahash_im).val[0]; + + uchar *pixel = ahash_im.ptr(0); + int endPixel = ahash_im.cols * ahash_im.rows; + for (int i = 0; i < endPixel; i++) { + set_bit_at(out->ahash, i, pixel[i] > avg); + } + + //dhash + int offset = 0; + for (int i = 0; i < dhash_im.rows; ++i) { + pixel = dhash_im.ptr(i); + + for (int j = 1; j < dhash_im.cols; ++j) { + set_bit_at(out->dhash, offset++, pixel[j] > pixel[j - 1]); + } + } + + //phash + pixel = phash_im.ptr(0); + endPixel = phash_im.cols * phash_im.rows; + for (int i = 0; i < endPixel; i++) { + pixels[i] = (double) pixel[i] / 255; + } + + double dct_out[ph_img_scale * ph_img_scale]; + fftw_plan plan = fftw_plan_r2r_2d( + ph_img_scale, ph_img_scale, + pixels, dct_out, + FFTW_REDFT10, FFTW_REDFT10, // DCT-II + FFTW_ESTIMATE + ); + fftw_execute(plan); + fftw_destroy_plan(plan); + + double dct_lowfreq[hash_size * hash_size]; + double sorted[hash_size * hash_size]; + + int ptr_low = 0; + int ptr = 0; + for (int i = 0; i < hash_size; ++i) { + for (int j = 0; j < hash_size; ++j) { + dct_lowfreq[ptr_low] = dct_out[ptr]; + sorted[ptr_low] = dct_out[ptr]; + ptr_low += 1; + ptr += 1; + } + ptr += (ph_img_scale - hash_size); + } + + double med = median(sorted, hash_size * hash_size); + + for (int i = 0; i < hash_size * hash_size; ++i) { + set_bit_at(out->phash, i, dct_lowfreq[i] > med); + } + + //whash + pixel = whash_im.ptr(0); + endPixel = whash_im.cols * whash_im.rows; + for (int i = 0; i < endPixel; i++) { + pixels[i] = (double) pixel[i] / 255; + } + + //TODO: haar option + wave_object w = wave_init("haar"); + wt2_object wt = wt2_init(w, "dwt", wh_img_scale, wh_img_scale, dwt_level); + + double *coeffs = dwt2(wt, pixels); + + memcpy(sorted, coeffs, sizeof(double) * (hash_size * hash_size)); + + med = median(sorted, hash_size * hash_size); + + for (int i = 0; i < hash_size * hash_size; ++i) { + set_bit_at(out->whash, i, coeffs[i] > med); + } + + delete[] pixels; + return 0; +} diff --git a/fastimagehash.h b/fastimagehash.h index b520921..0e767d6 100644 --- a/fastimagehash.h +++ b/fastimagehash.h @@ -8,10 +8,25 @@ typedef unsigned char uchar; +typedef struct multi_hash { + uchar *ahash; + uchar *phash; + uchar *dhash; + uchar *whash; +} multi_hash_t; + #ifdef __cplusplus extern "C" { #endif +multi_hash_t *multi_hash_create(int hash_size); + +void multi_hash_destroy(multi_hash_t *h); + +int multi_hash_file(const char *filepath, multi_hash_t *out, int hash_size, int ph_highfreq_factor, int wh_img_scale); + +int multi_hash_mem(void *buf, multi_hash_t *out, size_t buf_len, int hash_size, int ph_highfreq_factor, int wh_img_scale); + void hash_to_hex_string_reversed(const uchar *h, char *out, int hash_size); void hash_to_hex_string(const uchar *h, char *out, int hash_size); diff --git a/imhash.c b/imhash.c index aebc0e2..f748442 100644 --- a/imhash.c +++ b/imhash.c @@ -54,6 +54,18 @@ int main(int argc, char *argv[]) { printf("%s\tw:%s\n", argv[i], hashstr); } } + + multi_hash_t *m = multi_hash_create(8); + multi_hash_file(argv[i], m, 8, 4, 0); + + hash_to_hex_string_reversed(m->phash, hashstr, 8); + printf("%s\tmp:%s\n", argv[i], hashstr); + hash_to_hex_string_reversed(m->ahash, hashstr, 8); + printf("%s\tma:%s\n", argv[i], hashstr); + hash_to_hex_string_reversed(m->dhash, hashstr, 8); + printf("%s\tmd:%s\n", argv[i], hashstr); + hash_to_hex_string_reversed(m->whash, hashstr, 8); + printf("%s\tmw:%s\n", argv[i], hashstr); } } } \ No newline at end of file