python-libphash 1.0.3__cp313-cp313-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- libphash/__init__.py +24 -0
- libphash/_build.py +97 -0
- libphash/_native.abi3.so +0 -0
- libphash/_native.pyi +39 -0
- libphash/context.py +94 -0
- libphash/exceptions.py +36 -0
- libphash/py.typed +0 -0
- libphash/types.py +58 -0
- libphash/utils.py +28 -0
- python_libphash-1.0.3.dist-info/METADATA +124 -0
- python_libphash-1.0.3.dist-info/RECORD +14 -0
- python_libphash-1.0.3.dist-info/WHEEL +5 -0
- python_libphash-1.0.3.dist-info/licenses/LICENSE +21 -0
- python_libphash-1.0.3.dist-info/top_level.txt +1 -0
libphash/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
PhashError,
|
|
5
|
+
AllocationError,
|
|
6
|
+
DecodeError,
|
|
7
|
+
InvalidArgumentError,
|
|
8
|
+
)
|
|
9
|
+
from .types import Digest, HashMethod
|
|
10
|
+
from .context import ImageContext
|
|
11
|
+
from .utils import hamming_distance, get_hash, compare_images
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"PhashError",
|
|
15
|
+
"AllocationError",
|
|
16
|
+
"DecodeError",
|
|
17
|
+
"InvalidArgumentError",
|
|
18
|
+
"Digest",
|
|
19
|
+
"HashMethod",
|
|
20
|
+
"ImageContext",
|
|
21
|
+
"hamming_distance",
|
|
22
|
+
"get_hash",
|
|
23
|
+
"compare_images",
|
|
24
|
+
]
|
libphash/_build.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import glob
|
|
3
|
+
from cffi import FFI
|
|
4
|
+
|
|
5
|
+
ffibuilder = FFI()
|
|
6
|
+
|
|
7
|
+
# 1. Define the C definitions exposed to Python
|
|
8
|
+
# We strip macros like PH_API for CFFI parsing
|
|
9
|
+
ffibuilder.cdef("""
|
|
10
|
+
// Constants
|
|
11
|
+
#define PH_DIGEST_MAX_BYTES 64
|
|
12
|
+
|
|
13
|
+
// Error Codes
|
|
14
|
+
typedef enum {
|
|
15
|
+
PH_SUCCESS = 0,
|
|
16
|
+
PH_ERR_ALLOCATION_FAILED = -1,
|
|
17
|
+
PH_ERR_DECODE_FAILED = -2,
|
|
18
|
+
PH_ERR_INVALID_ARGUMENT = -3,
|
|
19
|
+
PH_ERR_NOT_IMPLEMENTED = -4,
|
|
20
|
+
PH_ERR_EMPTY_IMAGE = -5,
|
|
21
|
+
...
|
|
22
|
+
} ph_error_t;
|
|
23
|
+
|
|
24
|
+
// Types
|
|
25
|
+
typedef struct ph_context ph_context_t;
|
|
26
|
+
|
|
27
|
+
typedef struct {
|
|
28
|
+
uint8_t data[PH_DIGEST_MAX_BYTES];
|
|
29
|
+
uint8_t size;
|
|
30
|
+
uint8_t reserved[7];
|
|
31
|
+
} ph_digest_t;
|
|
32
|
+
|
|
33
|
+
// Lifecycle
|
|
34
|
+
const char *ph_version(void);
|
|
35
|
+
ph_error_t ph_create(ph_context_t **out_ctx);
|
|
36
|
+
void ph_free(ph_context_t *ctx);
|
|
37
|
+
void ph_context_set_gamma(ph_context_t *ctx, float gamma);
|
|
38
|
+
|
|
39
|
+
// Loading
|
|
40
|
+
ph_error_t ph_load_from_file(ph_context_t *ctx, const char *filepath);
|
|
41
|
+
ph_error_t ph_load_from_memory(ph_context_t *ctx, const uint8_t *buffer, size_t length);
|
|
42
|
+
|
|
43
|
+
// uint64 Hashes
|
|
44
|
+
ph_error_t ph_compute_ahash(ph_context_t *ctx, uint64_t *out_hash);
|
|
45
|
+
ph_error_t ph_compute_dhash(ph_context_t *ctx, uint64_t *out_hash);
|
|
46
|
+
ph_error_t ph_compute_phash(ph_context_t *ctx, uint64_t *out_hash);
|
|
47
|
+
ph_error_t ph_compute_whash(ph_context_t *ctx, uint64_t *out_hash);
|
|
48
|
+
ph_error_t ph_compute_mhash(ph_context_t *ctx, uint64_t *out_hash);
|
|
49
|
+
|
|
50
|
+
// Digest Hashes
|
|
51
|
+
ph_error_t ph_compute_bmh(ph_context_t *ctx, ph_digest_t *out_digest);
|
|
52
|
+
ph_error_t ph_compute_color_hash(ph_context_t *ctx, ph_digest_t *out_digest);
|
|
53
|
+
ph_error_t ph_compute_radial_hash(ph_context_t *ctx, ph_digest_t *out_digest);
|
|
54
|
+
|
|
55
|
+
// Comparison
|
|
56
|
+
int ph_hamming_distance(uint64_t hash1, uint64_t hash2);
|
|
57
|
+
int ph_hamming_distance_digest(const ph_digest_t *a, const ph_digest_t *b);
|
|
58
|
+
double ph_l2_distance(const ph_digest_t *a, const ph_digest_t *b);
|
|
59
|
+
""")
|
|
60
|
+
|
|
61
|
+
# 2. Configure the Source Compilation
|
|
62
|
+
# We need to find all .c files in the native directory
|
|
63
|
+
curr_dir = os.path.dirname(os.path.abspath(__file__))
|
|
64
|
+
project_root = os.path.abspath(os.path.join(curr_dir, "../../"))
|
|
65
|
+
native_dir = os.path.abspath(os.path.join(project_root, "native", "libphash"))
|
|
66
|
+
|
|
67
|
+
sources = []
|
|
68
|
+
sources.extend(glob.glob(os.path.join(native_dir, "src", "*.c")))
|
|
69
|
+
sources.extend(glob.glob(os.path.join(native_dir, "src", "hashes", "*.c")))
|
|
70
|
+
|
|
71
|
+
include_dirs = ["native/libphash/include"]
|
|
72
|
+
source_files = [
|
|
73
|
+
"native/libphash/src/core.c",
|
|
74
|
+
"native/libphash/src/image.c",
|
|
75
|
+
"native/libphash/src/hashes/ahash.c",
|
|
76
|
+
"native/libphash/src/hashes/bmh.c",
|
|
77
|
+
"native/libphash/src/hashes/color_hash.c",
|
|
78
|
+
"native/libphash/src/hashes/common.c",
|
|
79
|
+
"native/libphash/src/hashes/dhash.c",
|
|
80
|
+
"native/libphash/src/hashes/mhash.c",
|
|
81
|
+
"native/libphash/src/hashes/phash.c",
|
|
82
|
+
"native/libphash/src/hashes/radial.c",
|
|
83
|
+
"native/libphash/src/hashes/whash.c",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
ffibuilder.set_source(
|
|
87
|
+
"libphash._native",
|
|
88
|
+
'#include "libphash.h"',
|
|
89
|
+
sources=source_files,
|
|
90
|
+
include_dirs=include_dirs,
|
|
91
|
+
libraries=["m"] if os.name == "posix" else [],
|
|
92
|
+
extra_compile_args=["-O3", "-Wall", "-fPIC"]
|
|
93
|
+
if os.name == "posix"
|
|
94
|
+
else ["/O2", "/W3"],
|
|
95
|
+
)
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
ffibuilder.compile(verbose=True)
|
libphash/_native.abi3.so
ADDED
|
Binary file
|
libphash/_native.pyi
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# src/libphash/_native.pyi
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
class FFI:
|
|
5
|
+
def new(self, type_name: str, value: Any = None) -> Any: ...
|
|
6
|
+
def buffer(self, ptr: Any, size: int) -> Any: ...
|
|
7
|
+
def memmove(self, dest: Any, src: Any, n: int) -> None: ...
|
|
8
|
+
|
|
9
|
+
class Lib:
|
|
10
|
+
# Lifecycle
|
|
11
|
+
def ph_create(self, ctx: Any) -> int: ...
|
|
12
|
+
def ph_free(self, ctx: Any) -> None: ...
|
|
13
|
+
|
|
14
|
+
# Loading
|
|
15
|
+
def ph_load_from_file(self, ctx: Any, path: bytes) -> int: ...
|
|
16
|
+
def ph_load_from_memory(self, ctx: Any, data: bytes, size: int) -> int: ...
|
|
17
|
+
|
|
18
|
+
# Settings
|
|
19
|
+
def ph_context_set_gamma(self, ctx: Any, gamma: float) -> None: ...
|
|
20
|
+
|
|
21
|
+
# Hashes (uint64)
|
|
22
|
+
def ph_compute_ahash(self, ctx: Any, out: Any) -> int: ...
|
|
23
|
+
def ph_compute_dhash(self, ctx: Any, out: Any) -> int: ...
|
|
24
|
+
def ph_compute_phash(self, ctx: Any, out: Any) -> int: ...
|
|
25
|
+
def ph_compute_whash(self, ctx: Any, out: Any) -> int: ...
|
|
26
|
+
def ph_compute_mhash(self, ctx: Any, out: Any) -> int: ...
|
|
27
|
+
|
|
28
|
+
# Hashes (Digests)
|
|
29
|
+
def ph_compute_bmh(self, ctx: Any, out: Any) -> int: ...
|
|
30
|
+
def ph_compute_color_hash(self, ctx: Any, out: Any) -> int: ...
|
|
31
|
+
def ph_compute_radial_hash(self, ctx: Any, out: Any) -> int: ...
|
|
32
|
+
|
|
33
|
+
# Distances
|
|
34
|
+
def ph_hamming_distance(self, h1: int, h2: int) -> int: ...
|
|
35
|
+
def ph_hamming_distance_digest(self, d1: Any, d2: Any) -> int: ...
|
|
36
|
+
def ph_l2_distance(self, d1: Any, d2: Any) -> float: ...
|
|
37
|
+
|
|
38
|
+
lib: Lib
|
|
39
|
+
ffi: FFI
|
libphash/context.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, final, Callable
|
|
4
|
+
from ._native import ffi, lib
|
|
5
|
+
from .exceptions import check_error
|
|
6
|
+
from .types import Digest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@final
|
|
10
|
+
class ImageContext:
|
|
11
|
+
_ptr: Any
|
|
12
|
+
_ctx_ptr_ptr: Any
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self, path: str | Path | None = None, bytes_data: bytes | None = None
|
|
16
|
+
) -> None:
|
|
17
|
+
self._ctx_ptr_ptr = ffi.new("ph_context_t **")
|
|
18
|
+
check_error(lib.ph_create(self._ctx_ptr_ptr))
|
|
19
|
+
self._ptr = self._ctx_ptr_ptr[0]
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
if path is not None:
|
|
23
|
+
self.load_from_file(path)
|
|
24
|
+
elif bytes_data is not None:
|
|
25
|
+
self.load_from_memory(bytes_data)
|
|
26
|
+
except Exception:
|
|
27
|
+
self.close()
|
|
28
|
+
raise
|
|
29
|
+
|
|
30
|
+
def close(self) -> None:
|
|
31
|
+
if hasattr(self, "_ptr") and self._ptr is not None:
|
|
32
|
+
lib.ph_free(self._ptr)
|
|
33
|
+
self._ptr = None
|
|
34
|
+
|
|
35
|
+
def __enter__(self) -> ImageContext:
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
def __exit__(self, exc_type: type[BaseException] | None, *args: Any) -> None:
|
|
39
|
+
self.close()
|
|
40
|
+
|
|
41
|
+
def load_from_file(self, path: str | Path) -> None:
|
|
42
|
+
path_obj = Path(path).resolve()
|
|
43
|
+
if not path_obj.exists():
|
|
44
|
+
raise FileNotFoundError(f"File not found: {path_obj}")
|
|
45
|
+
check_error(lib.ph_load_from_file(self._ptr, str(path_obj).encode()))
|
|
46
|
+
|
|
47
|
+
def load_from_memory(self, data: bytes) -> None:
|
|
48
|
+
check_error(lib.ph_load_from_memory(self._ptr, data, len(data)))
|
|
49
|
+
|
|
50
|
+
def set_gamma(self, gamma: float) -> None:
|
|
51
|
+
lib.ph_context_set_gamma(self._ptr, float(gamma))
|
|
52
|
+
|
|
53
|
+
# Внутренние хелперы теперь типизированы через Callable
|
|
54
|
+
def _uint64_prop(self, func: Callable[[Any, Any], int]) -> int:
|
|
55
|
+
out = ffi.new("uint64_t *")
|
|
56
|
+
check_error(func(self._ptr, out))
|
|
57
|
+
return int(out[0])
|
|
58
|
+
|
|
59
|
+
def _digest_prop(self, func: Callable[[Any, Any], int]) -> Digest:
|
|
60
|
+
out = ffi.new("ph_digest_t *")
|
|
61
|
+
check_error(func(self._ptr, out))
|
|
62
|
+
return Digest.from_c_struct(out[0])
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def ahash(self) -> int:
|
|
66
|
+
return self._uint64_prop(lib.ph_compute_ahash)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def dhash(self) -> int:
|
|
70
|
+
return self._uint64_prop(lib.ph_compute_dhash)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def phash(self) -> int:
|
|
74
|
+
return self._uint64_prop(lib.ph_compute_phash)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def whash(self) -> int:
|
|
78
|
+
return self._uint64_prop(lib.ph_compute_whash)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def mhash(self) -> int:
|
|
82
|
+
return self._uint64_prop(lib.ph_compute_mhash)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def bmh(self) -> Digest:
|
|
86
|
+
return self._digest_prop(lib.ph_compute_bmh)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def color_hash(self) -> Digest:
|
|
90
|
+
return self._digest_prop(lib.ph_compute_color_hash)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def radial_hash(self) -> Digest:
|
|
94
|
+
return self._digest_prop(lib.ph_compute_radial_hash)
|
libphash/exceptions.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PhashError(Exception):
|
|
5
|
+
"""Base exception for libphash library."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AllocationError(PhashError):
|
|
9
|
+
"""Raised when memory allocation fails in the C layer."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DecodeError(PhashError):
|
|
13
|
+
"""Raised when image decoding fails (stb_image error)."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InvalidArgumentError(PhashError):
|
|
17
|
+
"""Raised when an invalid argument is passed to the C function."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check_error(err_code: int) -> None:
|
|
21
|
+
"""Map C return codes to Python exceptions."""
|
|
22
|
+
if err_code == 0:
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
errors: dict[int, tuple[type[PhashError], str]] = {
|
|
26
|
+
-1: (AllocationError, "Memory allocation failed in libphash"),
|
|
27
|
+
-2: (DecodeError, "Failed to decode image (stb_image error)"),
|
|
28
|
+
-3: (InvalidArgumentError, "Invalid argument provided to libphash"),
|
|
29
|
+
-4: (PhashError, "Feature not implemented"),
|
|
30
|
+
-5: (PhashError, "Image is empty or invalid"),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
exc_class, msg = errors.get(
|
|
34
|
+
err_code, (PhashError, f"Unknown error code: {err_code}")
|
|
35
|
+
)
|
|
36
|
+
raise exc_class(msg)
|
libphash/py.typed
ADDED
|
File without changes
|
libphash/types.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any, final
|
|
4
|
+
from ._native import ffi, lib
|
|
5
|
+
|
|
6
|
+
PH_DIGEST_MAX_BYTES: int = 64
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HashMethod(Enum):
|
|
10
|
+
AHASH = "ahash"
|
|
11
|
+
DHASH = "dhash"
|
|
12
|
+
PHASH = "phash"
|
|
13
|
+
WHASH = "whash"
|
|
14
|
+
MHASH = "mhash"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@final
|
|
18
|
+
class Digest:
|
|
19
|
+
_data: bytes
|
|
20
|
+
_size: int
|
|
21
|
+
|
|
22
|
+
def __init__(self, data: bytes, size: int) -> None:
|
|
23
|
+
if len(data) > PH_DIGEST_MAX_BYTES:
|
|
24
|
+
raise ValueError(f"Data exceeds max size ({PH_DIGEST_MAX_BYTES})")
|
|
25
|
+
self._data = data
|
|
26
|
+
self._size = size
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def data(self) -> bytes:
|
|
30
|
+
return self._data
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def size(self) -> int:
|
|
34
|
+
return self._size
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_c_struct(cls, c_digest: Any) -> Digest:
|
|
38
|
+
# Pyright теперь знает, что у c_digest могут быть поля,
|
|
39
|
+
# так как мы описали это в .pyi как Any (динамический указатель)
|
|
40
|
+
size = int(c_digest.size)
|
|
41
|
+
raw_buffer = ffi.buffer(c_digest.data, size)
|
|
42
|
+
return cls(bytes(raw_buffer), size)
|
|
43
|
+
|
|
44
|
+
def to_c_struct(self) -> Any:
|
|
45
|
+
c_ptr = ffi.new("ph_digest_t *")
|
|
46
|
+
c_ptr.size = self._size
|
|
47
|
+
ffi.memmove(c_ptr.data, self._data, len(self._data))
|
|
48
|
+
return c_ptr
|
|
49
|
+
|
|
50
|
+
def distance_hamming(self, other: Digest) -> int:
|
|
51
|
+
if self._size != other._size:
|
|
52
|
+
raise ValueError("Digests must have the same size")
|
|
53
|
+
return lib.ph_hamming_distance_digest(self.to_c_struct(), other.to_c_struct())
|
|
54
|
+
|
|
55
|
+
def distance_l2(self, other: Digest) -> float:
|
|
56
|
+
if self._size != other._size:
|
|
57
|
+
raise ValueError("Digests must have the same size")
|
|
58
|
+
return lib.ph_l2_distance(self.to_c_struct(), other.to_c_struct())
|
libphash/utils.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import cast
|
|
5
|
+
from ._native import lib # type: ignore
|
|
6
|
+
from .context import ImageContext
|
|
7
|
+
from .types import HashMethod
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def hamming_distance(h1: int, h2: int) -> int:
|
|
11
|
+
"""Calculate Hamming distance between two uint64 hashes."""
|
|
12
|
+
return int(lib.ph_hamming_distance(int(h1), int(h2))) # type: ignore
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_hash(path: str | Path, method: HashMethod = HashMethod.PHASH) -> int:
|
|
16
|
+
"""Convenience function to get a hash for a file."""
|
|
17
|
+
with ImageContext(path=path) as ctx:
|
|
18
|
+
return cast(int, getattr(ctx, method.value))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def compare_images(
|
|
22
|
+
path1: str | Path, path2: str | Path, method: HashMethod = HashMethod.PHASH
|
|
23
|
+
) -> int:
|
|
24
|
+
"""Convenience function to compare two images."""
|
|
25
|
+
with ImageContext(path=path1) as ctx1, ImageContext(path=path2) as ctx2:
|
|
26
|
+
h1 = cast(int, getattr(ctx1, method.value))
|
|
27
|
+
h2 = cast(int, getattr(ctx2, method.value))
|
|
28
|
+
return hamming_distance(h1, h2)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-libphash
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: High-performance perceptual hashing library (CFFI bindings)
|
|
5
|
+
Author-email: gudoshnikovn <gudoshnikov-na@yandex.ru>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/gudoshnikovn/python-libphash
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: C
|
|
10
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: cffi>=1.15.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest; extra == "dev"
|
|
17
|
+
Requires-Dist: mypy; extra == "dev"
|
|
18
|
+
Requires-Dist: ruff; extra == "dev"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# python-libphash
|
|
22
|
+
|
|
23
|
+
High-performance Python bindings for [libphash](https://github.com/gudoshnikovn/libphash), a C library for perceptual image hashing.
|
|
24
|
+
|
|
25
|
+
[](https://opensource.org/licenses/MIT)
|
|
26
|
+
[](https://www.python.org/downloads/)
|
|
27
|
+
|
|
28
|
+
## Overview
|
|
29
|
+
|
|
30
|
+
`libphash` provides multiple algorithms to generate "perceptual hashes" of images. Unlike cryptographic hashes (like MD5 or SHA256), perceptual hashes change only slightly if the image is resized, compressed, or has minor color adjustments. This makes them ideal for finding duplicate or similar images.
|
|
31
|
+
|
|
32
|
+
### Supported Algorithms
|
|
33
|
+
|
|
34
|
+
* **64-bit Hashes (uint64):**
|
|
35
|
+
* `ahash`: Average Hash
|
|
36
|
+
* `dhash`: Difference Hash
|
|
37
|
+
* `phash`: Perceptual Hash (DCT based)
|
|
38
|
+
* `whash`: Wavelet Hash
|
|
39
|
+
* `mhash`: Median Hash
|
|
40
|
+
* **Digest Hashes (Multi-byte):**
|
|
41
|
+
* `bmh`: Block Mean Hash
|
|
42
|
+
* `color_hash`: Color Moment Hash
|
|
43
|
+
* `radial_hash`: Radial Variance Hash
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
### Prerequisites
|
|
48
|
+
* A C compiler (GCC/Clang or MSVC)
|
|
49
|
+
* Python 3.8 or higher
|
|
50
|
+
|
|
51
|
+
### Install from source
|
|
52
|
+
```bash
|
|
53
|
+
git clone --recursive https://github.com/yourusername/python-libphash.git
|
|
54
|
+
cd python-libphash
|
|
55
|
+
pip install .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
### Basic Usage
|
|
61
|
+
```python
|
|
62
|
+
from libphash import ImageContext, HashMethod, hamming_distance
|
|
63
|
+
|
|
64
|
+
# Use the context manager for automatic memory management
|
|
65
|
+
with ImageContext("photo.jpg") as ctx:
|
|
66
|
+
# Get standard 64-bit hashes
|
|
67
|
+
phash_val = ctx.phash
|
|
68
|
+
dhash_val = ctx.dhash
|
|
69
|
+
|
|
70
|
+
print(f"pHash: {phash_val:016x}")
|
|
71
|
+
print(f"dHash: {dhash_val:016x}")
|
|
72
|
+
|
|
73
|
+
# Compare two images
|
|
74
|
+
from libphash import compare_images
|
|
75
|
+
distance = compare_images("image1.jpg", "image2.jpg", method=HashMethod.PHASH)
|
|
76
|
+
print(f"Hamming Distance: {distance}")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Working with Digests (Advanced Hashes)
|
|
80
|
+
Algorithms like Radial Hash or Color Hash return a `Digest` object instead of a single integer.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
with ImageContext("photo.jpg") as ctx:
|
|
84
|
+
digest = ctx.radial_hash
|
|
85
|
+
print(f"Digest size: {digest.size} bytes")
|
|
86
|
+
print(f"Raw data: {digest.data.hex()}")
|
|
87
|
+
|
|
88
|
+
# Comparing digests
|
|
89
|
+
with ImageContext("photo_v2.jpg") as ctx2:
|
|
90
|
+
digest2 = ctx2.radial_hash
|
|
91
|
+
|
|
92
|
+
# Hamming distance for bit-wise comparison
|
|
93
|
+
h_dist = digest.distance_hamming(digest2)
|
|
94
|
+
|
|
95
|
+
# L2 (Euclidean) distance for similarity
|
|
96
|
+
l2_dist = digest.distance_l2(digest2)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## API Reference
|
|
100
|
+
|
|
101
|
+
### `ImageContext`
|
|
102
|
+
The main class for loading images and computing hashes.
|
|
103
|
+
* `__init__(path=None, bytes_data=None)`: Load an image from a file path or memory.
|
|
104
|
+
* `set_gamma(gamma: float)`: Set gamma correction (useful for Radial Hash).
|
|
105
|
+
* **Properties**: `ahash`, `dhash`, `phash`, `whash`, `mhash` (returns `int`).
|
|
106
|
+
* **Properties**: `bmh`, `color_hash`, `radial_hash` (returns `Digest`).
|
|
107
|
+
|
|
108
|
+
### `Digest`
|
|
109
|
+
* `data`: The raw `bytes` of the hash.
|
|
110
|
+
* `size`: Length of the hash in bytes.
|
|
111
|
+
* `distance_hamming(other)`: Calculates bit-wise distance.
|
|
112
|
+
* `distance_l2(other)`: Calculates Euclidean distance.
|
|
113
|
+
|
|
114
|
+
### Utilities
|
|
115
|
+
* `hamming_distance(h1: int, h2: int)`: Returns the number of differing bits between two 64-bit integers.
|
|
116
|
+
* `get_hash(path, method)`: Quick way to get a hash without manual context management.
|
|
117
|
+
* `compare_images(path1, path2, method)`: Returns the Hamming distance between two image files.
|
|
118
|
+
|
|
119
|
+
## Performance
|
|
120
|
+
Since the core logic is implemented in C and uses `stb_image` for decoding, `libphash` is significantly faster than pure-Python alternatives. It also uses CFFI's "out-of-line" mode for minimal overhead.
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
124
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
libphash/__init__.py,sha256=tqi8ZA7pA1gksrE5pkTYBbIdsB6nFAwNwrYNM_7dpR0,495
|
|
2
|
+
libphash/_build.py,sha256=3JRNepyK7gyYlkD_U0RX505Z-Spn2dJ_wtja0Z7wrM4,3279
|
|
3
|
+
libphash/types.py,sha256=r1gnlvBwCSFgUY5UWe4ZIcm0Zbi4omE9nF8LwIfcV2A,1772
|
|
4
|
+
libphash/_native.abi3.so,sha256=Q_uCYFeYebG33n67He4K7jPA03I_9ymgCBxvkt08kJI,227664
|
|
5
|
+
libphash/context.py,sha256=eCKZNuuadStVmpnI8UcyF_SbKzNaWFTmjv9ryA8zG0c,2879
|
|
6
|
+
libphash/utils.py,sha256=BGYIXyyWluBHlUmPVcStDxP0hTXIFHe1HNd4eoWaQsE,995
|
|
7
|
+
libphash/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
libphash/exceptions.py,sha256=HOo1nWh9jucqgH1c1rkxhZJoYO8EvmO6g3aENrbGn8U,1071
|
|
9
|
+
libphash/_native.pyi,sha256=BUSKo-ZUJyWu-3ftjmVe3nfII-6bOMvte_TCE0kn8lE,1406
|
|
10
|
+
python_libphash-1.0.3.dist-info/RECORD,,
|
|
11
|
+
python_libphash-1.0.3.dist-info/WHEEL,sha256=KreXLeNnYSLDPpk7qnNyKd0DQEhtY-je-mdlEpkBMmo,109
|
|
12
|
+
python_libphash-1.0.3.dist-info/top_level.txt,sha256=_Iys95-wvePR99MGJ1ikgjscpcotnUJ2m3JzmLvCG7w,9
|
|
13
|
+
python_libphash-1.0.3.dist-info/METADATA,sha256=JLoS7kRLWhDRukE0gU8xmJ3VvXfkvmKpAgK7WUubb1c,4377
|
|
14
|
+
python_libphash-1.0.3.dist-info/licenses/LICENSE,sha256=rRPeBnIMO5vVEJAMWDi-pAR4_AD3id6WwDG02zUnYzQ,1069
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gudoshnikovn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
libphash
|