Perception 0.8.3__tar.gz → 0.8.4__tar.gz

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.
Files changed (59) hide show
  1. {perception-0.8.3 → perception-0.8.4}/PKG-INFO +4 -4
  2. {perception-0.8.3 → perception-0.8.4}/build.py +0 -1
  3. {perception-0.8.3 → perception-0.8.4}/perception/approximate_deduplication/__init__.py +22 -47
  4. perception-0.8.4/perception/approximate_deduplication/_graph_backend.py +138 -0
  5. {perception-0.8.3 → perception-0.8.4}/perception/approximate_deduplication/debug.py +2 -4
  6. {perception-0.8.3 → perception-0.8.4}/perception/benchmarking/image_transforms.py +2 -2
  7. {perception-0.8.3 → perception-0.8.4}/perception/hashers/__init__.py +0 -1
  8. {perception-0.8.3 → perception-0.8.4}/pyproject.toml +5 -3
  9. {perception-0.8.3 → perception-0.8.4}/setup.py +5 -3
  10. {perception-0.8.3 → perception-0.8.4}/LICENSE +0 -0
  11. {perception-0.8.3 → perception-0.8.4}/README.md +0 -0
  12. {perception-0.8.3 → perception-0.8.4}/perception/__init__.py +0 -0
  13. {perception-0.8.3 → perception-0.8.4}/perception/approximate_deduplication/index.py +0 -0
  14. {perception-0.8.3 → perception-0.8.4}/perception/approximate_deduplication/serve.py +0 -0
  15. {perception-0.8.3 → perception-0.8.4}/perception/benchmarking/__init__.py +0 -0
  16. {perception-0.8.3 → perception-0.8.4}/perception/benchmarking/common.py +0 -0
  17. {perception-0.8.3 → perception-0.8.4}/perception/benchmarking/extensions.pyx +0 -0
  18. {perception-0.8.3 → perception-0.8.4}/perception/benchmarking/image.py +0 -0
  19. {perception-0.8.3 → perception-0.8.4}/perception/benchmarking/video.py +0 -0
  20. {perception-0.8.3 → perception-0.8.4}/perception/benchmarking/video_transforms.py +0 -0
  21. {perception-0.8.3 → perception-0.8.4}/perception/extensions.pyx +0 -0
  22. {perception-0.8.3 → perception-0.8.4}/perception/hashers/hasher.py +0 -0
  23. {perception-0.8.3 → perception-0.8.4}/perception/hashers/image/__init__.py +0 -0
  24. {perception-0.8.3 → perception-0.8.4}/perception/hashers/image/average.py +0 -0
  25. {perception-0.8.3 → perception-0.8.4}/perception/hashers/image/dhash.py +0 -0
  26. {perception-0.8.3 → perception-0.8.4}/perception/hashers/image/opencv.py +0 -0
  27. {perception-0.8.3 → perception-0.8.4}/perception/hashers/image/pdq.py +0 -0
  28. {perception-0.8.3 → perception-0.8.4}/perception/hashers/image/phash.py +0 -0
  29. {perception-0.8.3 → perception-0.8.4}/perception/hashers/image/wavelet.py +0 -0
  30. {perception-0.8.3 → perception-0.8.4}/perception/hashers/tools.py +0 -0
  31. {perception-0.8.3 → perception-0.8.4}/perception/hashers/video/__init__.py +0 -0
  32. {perception-0.8.3 → perception-0.8.4}/perception/hashers/video/framewise.py +0 -0
  33. {perception-0.8.3 → perception-0.8.4}/perception/hashers/video/tmk.py +0 -0
  34. {perception-0.8.3 → perception-0.8.4}/perception/local_descriptor_deduplication.py +0 -0
  35. {perception-0.8.3 → perception-0.8.4}/perception/py.typed +0 -0
  36. {perception-0.8.3 → perception-0.8.4}/perception/testing/__init__.py +0 -0
  37. {perception-0.8.3 → perception-0.8.4}/perception/testing/images/README.md +0 -0
  38. {perception-0.8.3 → perception-0.8.4}/perception/testing/images/image1.jpg +0 -0
  39. {perception-0.8.3 → perception-0.8.4}/perception/testing/images/image10.jpg +0 -0
  40. {perception-0.8.3 → perception-0.8.4}/perception/testing/images/image2.jpg +0 -0
  41. {perception-0.8.3 → perception-0.8.4}/perception/testing/images/image3.jpg +0 -0
  42. {perception-0.8.3 → perception-0.8.4}/perception/testing/images/image4.jpg +0 -0
  43. {perception-0.8.3 → perception-0.8.4}/perception/testing/images/image5.jpg +0 -0
  44. {perception-0.8.3 → perception-0.8.4}/perception/testing/images/image6.jpg +0 -0
  45. {perception-0.8.3 → perception-0.8.4}/perception/testing/images/image7.jpg +0 -0
  46. {perception-0.8.3 → perception-0.8.4}/perception/testing/images/image8.jpg +0 -0
  47. {perception-0.8.3 → perception-0.8.4}/perception/testing/images/image9.jpg +0 -0
  48. {perception-0.8.3 → perception-0.8.4}/perception/testing/logos/README.md +0 -0
  49. {perception-0.8.3 → perception-0.8.4}/perception/testing/logos/logoipsum.png +0 -0
  50. {perception-0.8.3 → perception-0.8.4}/perception/testing/videos/README.md +0 -0
  51. {perception-0.8.3 → perception-0.8.4}/perception/testing/videos/expected_tmk.json.gz +0 -0
  52. {perception-0.8.3 → perception-0.8.4}/perception/testing/videos/extra_channel_attached_pic.mp4 +0 -0
  53. {perception-0.8.3 → perception-0.8.4}/perception/testing/videos/extra_channel_attached_pic_audio.mp4 +0 -0
  54. {perception-0.8.3 → perception-0.8.4}/perception/testing/videos/rgb.m4v +0 -0
  55. {perception-0.8.3 → perception-0.8.4}/perception/testing/videos/v1.m4v +0 -0
  56. {perception-0.8.3 → perception-0.8.4}/perception/testing/videos/v2.m4v +0 -0
  57. {perception-0.8.3 → perception-0.8.4}/perception/testing/videos/v2s.mov +0 -0
  58. {perception-0.8.3 → perception-0.8.4}/perception/tools.py +0 -0
  59. {perception-0.8.3 → perception-0.8.4}/perception/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Perception
3
- Version: 0.8.3
3
+ Version: 0.8.4
4
4
  Summary: Perception provides flexible, well-documented, and comprehensively tested tooling for perceptual hashing research, development, and production use.
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -14,17 +14,17 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
16
  Provides-Extra: benchmarking
17
- Provides-Extra: experimental
18
17
  Provides-Extra: matching
19
18
  Provides-Extra: pdq
20
19
  Requires-Dist: Cython (>=3.0.0,<4.0.0)
21
20
  Requires-Dist: Pillow
22
21
  Requires-Dist: aiohttp ; extra == "matching"
23
22
  Requires-Dist: albumentations (>=2.0.8,<3.0.0) ; extra == "benchmarking"
24
- Requires-Dist: faiss-cpu (>=1.8.0,<2.0.0) ; extra == "experimental"
23
+ Requires-Dist: faiss-cpu (>=1.8.0,<2.0.0)
25
24
  Requires-Dist: ffmpeg-python ; extra == "benchmarking"
26
25
  Requires-Dist: matplotlib ; extra == "benchmarking"
27
- Requires-Dist: networkit (>=11.1,<12.0.0) ; extra == "experimental"
26
+ Requires-Dist: networkit (>=11.1,<12.0.0) ; sys_platform != "darwin"
27
+ Requires-Dist: networkx (>=3.0,<4.0) ; sys_platform == "darwin"
28
28
  Requires-Dist: numpy (>=1.26.4,<3.0.0)
29
29
  Requires-Dist: opencv-contrib-python-headless (>=4.10.0,<5.0.0)
30
30
  Requires-Dist: pandas
@@ -1,7 +1,6 @@
1
1
  from Cython.Build import cythonize
2
2
  import numpy as np
3
3
 
4
-
5
4
  compiler_directives = {"language_level": 3, "embedsignature": True}
6
5
 
7
6
 
@@ -4,11 +4,12 @@ import os.path as op
4
4
  import typing
5
5
 
6
6
  import faiss
7
- import networkit as nk
8
7
  import numpy as np
9
8
  import tqdm
10
9
  import typing_extensions
11
10
 
11
+ from ._graph_backend import get_graph_backend
12
+
12
13
  LOGGER = logging.getLogger(__name__)
13
14
  DEFAULT_PCT_PROBE = 0
14
15
 
@@ -227,16 +228,13 @@ def pairs_to_clusters(
227
228
  node_to_id_map = {v: k for k, v in id_to_node_map.items()}
228
229
 
229
230
  LOGGER.debug("Building graph.")
230
- graph = nk.Graph(len(list_ids))
231
231
  node_pairs = {(id_to_node_map[pair[0]], id_to_node_map[pair[1]]) for pair in pairs}
232
- for node_pair in node_pairs:
233
- graph.addEdge(node_pair[0], node_pair[1])
232
+ backend = get_graph_backend()
233
+ graph = backend.build_graph(len(list_ids), node_pairs)
234
234
 
235
235
  assignments: list[ClusterAssignment] = []
236
236
  cluster_index = 0
237
- cc_query = nk.components.ConnectedComponents(graph)
238
- cc_query.run()
239
- components = cc_query.getComponents()
237
+ components = backend.connected_components(graph)
240
238
 
241
239
  for component in components:
242
240
  LOGGER.debug("Got component with size: %s", len(component))
@@ -246,19 +244,9 @@ def pairs_to_clusters(
246
244
  )
247
245
  cluster_index += 1
248
246
  continue
249
- # Map between node values for a connected component
250
- component_node_map = dict(enumerate(component))
251
- cc_sub_graph = nk.graphtools.subgraphFromNodes(graph, component, compact=True)
252
- algo = nk.community.PLP(cc_sub_graph)
253
- algo.run()
254
- communities = algo.getPartition()
255
- community_map = communities.subsetSizeMap()
256
- for community, size in community_map.items():
257
- LOGGER.debug("Got community with size: %s", size)
258
- community_members = list(
259
- communities.getMembers(community)
260
- ) # Need to do this to do batching.
261
- community_members = [component_node_map[i] for i in community_members]
247
+ communities = backend.communities(graph, component)
248
+ for community_members in communities:
249
+ LOGGER.debug("Got community with size: %s", len(community_members))
262
250
  if strictness == "community":
263
251
  assignments.extend(
264
252
  [
@@ -269,33 +257,20 @@ def pairs_to_clusters(
269
257
  cluster_index += 1
270
258
  continue
271
259
 
272
- for start in range(0, len(community_members), max_clique_batch_size):
273
- community_nodes = community_members[
274
- start : start + max_clique_batch_size
275
- ]
276
- LOGGER.debug("Creating subgraph with %s nodes.", len(community_nodes))
277
- # Map between node values for a community
278
- community_node_map = dict(enumerate(community_nodes))
279
- subgraph = nk.graphtools.subgraphFromNodes(
280
- graph, community_nodes, compact=True
260
+ for clique_members in backend.maximal_cliques(
261
+ graph,
262
+ community_members,
263
+ max_clique_batch_size=max_clique_batch_size,
264
+ ):
265
+ assignments.extend(
266
+ [
267
+ {
268
+ "id": node_to_id_map[n],
269
+ "cluster": cluster_index,
270
+ }
271
+ for n in clique_members
272
+ ]
281
273
  )
282
-
283
- while subgraph.numberOfNodes() > 0:
284
- LOGGER.debug("Subgraph size: %s", subgraph.numberOfNodes())
285
- clique = nk.clique.MaximalCliques(subgraph, maximumOnly=True)
286
- clique.run()
287
- clique_members = clique.getCliques()[0]
288
- assignments.extend(
289
- [
290
- {
291
- "id": node_to_id_map[community_node_map[n]],
292
- "cluster": cluster_index,
293
- }
294
- for n in clique_members
295
- ]
296
- )
297
- cluster_index += 1
298
- for n in clique_members:
299
- subgraph.removeNode(n)
274
+ cluster_index += 1
300
275
 
301
276
  return assignments
@@ -0,0 +1,138 @@
1
+ import sys
2
+ import typing
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+ class GraphBackend(ABC):
7
+ @abstractmethod
8
+ def build_graph(
9
+ self, node_count: int, edges: typing.Iterable[tuple[int, int]]
10
+ ) -> typing.Any: ...
11
+
12
+ @abstractmethod
13
+ def connected_components(self, graph: typing.Any) -> list[list[int]]: ...
14
+
15
+ @abstractmethod
16
+ def communities(
17
+ self, graph: typing.Any, component: list[int]
18
+ ) -> list[list[int]]: ...
19
+
20
+ @abstractmethod
21
+ def maximal_cliques(
22
+ self,
23
+ graph: typing.Any,
24
+ community_nodes: list[int],
25
+ max_clique_batch_size: int,
26
+ ) -> list[list[int]]: ...
27
+
28
+
29
+ class NetworkitGraphBackend(GraphBackend):
30
+ def __init__(self):
31
+ import networkit as nk
32
+
33
+ self.nk = nk
34
+
35
+ def build_graph(
36
+ self, node_count: int, edges: typing.Iterable[tuple[int, int]]
37
+ ) -> typing.Any:
38
+ graph = self.nk.Graph(node_count)
39
+ for start, end in edges:
40
+ graph.addEdge(start, end)
41
+ return graph
42
+
43
+ def connected_components(self, graph: typing.Any) -> list[list[int]]:
44
+ cc_query = self.nk.components.ConnectedComponents(graph)
45
+ cc_query.run()
46
+ return cc_query.getComponents()
47
+
48
+ def communities(self, graph: typing.Any, component: list[int]) -> list[list[int]]:
49
+ component_node_map = dict(enumerate(component))
50
+ subgraph = self.nk.graphtools.subgraphFromNodes(graph, component, compact=True)
51
+ algo = self.nk.community.PLP(subgraph, maxIterations=32)
52
+ algo.run()
53
+ communities = algo.getPartition()
54
+ return [
55
+ [component_node_map[node] for node in communities.getMembers(community)]
56
+ for community in communities.subsetSizeMap().keys()
57
+ ]
58
+
59
+ def maximal_cliques(
60
+ self,
61
+ graph: typing.Any,
62
+ community_nodes: list[int],
63
+ max_clique_batch_size: int,
64
+ ) -> list[list[int]]:
65
+ cliques: list[list[int]] = []
66
+ for start in range(0, len(community_nodes), max_clique_batch_size):
67
+ batch_nodes = community_nodes[start : start + max_clique_batch_size]
68
+ community_node_map = dict(enumerate(batch_nodes))
69
+ subgraph = self.nk.graphtools.subgraphFromNodes(
70
+ graph, batch_nodes, compact=True
71
+ )
72
+
73
+ while subgraph.numberOfNodes() > 0:
74
+ clique = self.nk.clique.MaximalCliques(subgraph, maximumOnly=True)
75
+ clique.run()
76
+ clique_members = clique.getCliques()[0]
77
+ cliques.append([community_node_map[node] for node in clique_members])
78
+ for node in clique_members:
79
+ subgraph.removeNode(node)
80
+
81
+ return cliques
82
+
83
+
84
+ class NetworkxGraphBackend(GraphBackend):
85
+ def __init__(self):
86
+ import networkx as nx
87
+
88
+ self.nx = nx
89
+
90
+ def build_graph(
91
+ self, node_count: int, edges: typing.Iterable[tuple[int, int]]
92
+ ) -> typing.Any:
93
+ graph = self.nx.Graph()
94
+ graph.add_nodes_from(range(node_count))
95
+ graph.add_edges_from(edges)
96
+ return graph
97
+
98
+ def connected_components(self, graph: typing.Any) -> list[list[int]]:
99
+ return [list(component) for component in self.nx.connected_components(graph)]
100
+
101
+ def communities(self, graph: typing.Any, component: list[int]) -> list[list[int]]:
102
+ subgraph = graph.subgraph(component)
103
+ return [
104
+ list(community)
105
+ for community in self.nx.algorithms.community.asyn_lpa_communities(
106
+ subgraph, seed=0
107
+ )
108
+ ]
109
+
110
+ def maximal_cliques(
111
+ self,
112
+ graph: typing.Any,
113
+ community_nodes: list[int],
114
+ max_clique_batch_size: int,
115
+ ) -> list[list[int]]:
116
+ cliques: list[list[int]] = []
117
+ for start in range(0, len(community_nodes), max_clique_batch_size):
118
+ batch_nodes = community_nodes[start : start + max_clique_batch_size]
119
+ subgraph = graph.subgraph(batch_nodes).copy()
120
+
121
+ while subgraph.number_of_nodes() > 0:
122
+ clique_members = max(
123
+ self.nx.find_cliques(subgraph),
124
+ key=lambda clique: (
125
+ len(clique),
126
+ tuple(sorted(clique)),
127
+ ),
128
+ )
129
+ cliques.append(list(clique_members))
130
+ subgraph.remove_nodes_from(clique_members)
131
+
132
+ return cliques
133
+
134
+
135
+ def get_graph_backend() -> GraphBackend:
136
+ if sys.platform == "darwin":
137
+ return NetworkxGraphBackend()
138
+ return NetworkitGraphBackend()
@@ -79,10 +79,8 @@ def vizualize_pair(
79
79
  circle_size=circle_size,
80
80
  )
81
81
  else:
82
- LOGGER.warning(
83
- """No match_metadata provided, recalculating match points,
84
- won't match perception match points."""
85
- )
82
+ LOGGER.warning("""No match_metadata provided, recalculating match points,
83
+ won't match perception match points.""")
86
84
  img_matched = viz_brute_force(features_1, features_2, img1, img2, ratio=ratio)
87
85
 
88
86
  return img_matched
@@ -17,7 +17,7 @@ def apply_watermark(watermark, alpha: float = 1.0, size: float = 1.0):
17
17
 
18
18
  # Why do we have to do this? It's not clear. But the process doesn't work
19
19
  # without it.
20
- (B, G, R, A) = cv2.split(watermark)
20
+ B, G, R, A = cv2.split(watermark)
21
21
  B = cv2.bitwise_and(B, B, mask=A)
22
22
  G = cv2.bitwise_and(G, G, mask=A)
23
23
  R = cv2.bitwise_and(R, R, mask=A)
@@ -25,7 +25,7 @@ def apply_watermark(watermark, alpha: float = 1.0, size: float = 1.0):
25
25
 
26
26
  def transform(image):
27
27
  # Add alpha channel
28
- (h, w) = image.shape[:2]
28
+ h, w = image.shape[:2]
29
29
  wh, ww = watermark.shape[:2]
30
30
  scale = size * min(h / wh, w / ww)
31
31
  image = np.dstack([image, np.ones((h, w), dtype="uint8") * 255])
@@ -7,7 +7,6 @@ from .image.wavelet import WaveletHash
7
7
  from .video.framewise import FramewiseHasher
8
8
  from .video.tmk import TMKL1, TMKL2
9
9
 
10
-
11
10
  __all__ = [
12
11
  "ImageHasher",
13
12
  "VideoHasher",
@@ -10,6 +10,9 @@ dependencies = [
10
10
  "Cython>=3.0.0,<4.0.0",
11
11
  "numpy>=1.26.4,<3.0.0",
12
12
  "opencv-contrib-python-headless>=4.10.0,<5.0.0",
13
+ "faiss-cpu>=1.8.0,<2.0.0",
14
+ "networkit>=11.1,<12.0.0; sys_platform != 'darwin'",
15
+ "networkx>=3.0,<4.0; sys_platform == 'darwin'",
13
16
  "pandas",
14
17
  "Pillow",
15
18
  "pywavelets>=1.5.0,<2.0.0",
@@ -18,7 +21,7 @@ dependencies = [
18
21
  "scipy",
19
22
  "tqdm>=4.67.1,<5.0.0",
20
23
  ]
21
- version = "0.8.3"
24
+ version = "0.8.4"
22
25
 
23
26
 
24
27
  [project.optional-dependencies]
@@ -30,7 +33,6 @@ benchmarking = [
30
33
  "ffmpeg-python",
31
34
  ]
32
35
  matching = ["aiohttp", "python-json-logger"]
33
- experimental = ["networkit>=11.1,<12.0.0", "faiss-cpu>=1.8.0,<2.0.0"]
34
36
  pdq = ["pdqhash>=0.2.7,<0.3.0"]
35
37
 
36
38
 
@@ -38,7 +40,7 @@ pdq = ["pdqhash>=0.2.7,<0.3.0"]
38
40
 
39
41
 
40
42
  [tool.poetry.group.dev.dependencies]
41
- black = "^24"
43
+ black = "^26"
42
44
  coverage = "*"
43
45
  ipython = "*"
44
46
  mypy = "*"
@@ -16,6 +16,7 @@ package_data = \
16
16
  install_requires = \
17
17
  ['Cython>=3.0.0,<4.0.0',
18
18
  'Pillow',
19
+ 'faiss-cpu>=1.8.0,<2.0.0',
19
20
  'numpy>=1.26.4,<3.0.0',
20
21
  'opencv-contrib-python-headless>=4.10.0,<5.0.0',
21
22
  'pandas',
@@ -26,18 +27,19 @@ install_requires = \
26
27
  'validators>=0.22.0,<1.0.0']
27
28
 
28
29
  extras_require = \
29
- {'benchmarking': ['matplotlib',
30
+ {':sys_platform != "darwin"': ['networkit>=11.1,<12.0.0'],
31
+ ':sys_platform == "darwin"': ['networkx>=3.0,<4.0'],
32
+ 'benchmarking': ['matplotlib',
30
33
  'albumentations>=2.0.8,<3.0.0',
31
34
  'tabulate',
32
35
  'scikit-learn',
33
36
  'ffmpeg-python'],
34
- 'experimental': ['networkit>=11.1,<12.0.0', 'faiss-cpu>=1.8.0,<2.0.0'],
35
37
  'matching': ['aiohttp', 'python-json-logger'],
36
38
  'pdq': ['pdqhash>=0.2.7,<0.3.0']}
37
39
 
38
40
  setup_kwargs = {
39
41
  'name': 'Perception',
40
- 'version': '0.8.3',
42
+ 'version': '0.8.4',
41
43
  'description': 'Perception provides flexible, well-documented, and comprehensively tested tooling for perceptual hashing research, development, and production use.',
42
44
  'long_description': "# perception ![ci](https://github.com/thorn-oss/perception/workflows/ci/badge.svg)\n\n`perception` provides flexible, well-documented, and comprehensively tested tooling for perceptual hashing research, development, and production use. See [the documentation](https://perception.thorn.engineering/en/latest/) for details.\n\n## Background\n\n`perception` was initially developed at [Thorn](https://www.thorn.org) as part of our work to eliminate child sexual abuse material from the internet. For more information on the issue, check out [our CEO's TED talk](https://www.thorn.org/blog/time-is-now-eliminate-csam/).\n\n## Getting Started\n\n### Installation\n\n`pip install perception`\n\n### Hashing\n\nHashing with different functions is simple with `perception`.\n\n```python\nfrom perception import hashers\n\nfile1, file2 = 'test1.jpg', 'test2.jpg'\nhasher = hashers.PHash()\nhash1, hash2 = hasher.compute(file1), hasher.compute(file2)\ndistance = hasher.compute_distance(hash1, hash2)\n```\n\n### Examples\n\nSee below for end-to-end examples for common use cases for perceptual hashes.\n\n- [Detecting child sexual abuse material](https://perception.thorn.engineering/en/latest/examples/detecting_csam.html)\n- [Deduplicating media](https://perception.thorn.engineering/en/latest/examples/deduplication.html)\n- [Benchmarking perceptual hashes](https://perception.thorn.engineering/en/latest/examples/benchmarking.html)\n\n## Supported Hashing Algorithms\n\n`perception` currently ships with:\n\n- pHash (DCT hash) (`perception.hashers.PHash`)\n- Facebook's PDQ Hash (`perception.hashers.PDQ`)\n- dHash (difference hash) (`perception.hashers.DHash`)\n- aHash (average hash) (`perception.hashers.AverageHash`)\n- Marr-Hildreth (`perception.hashers.MarrHildreth`)\n- Color Moment (`perception.hashers.ColorMoment`)\n- Block Mean (`perception.hashers.BlockMean`)\n- wHash (wavelet hash) (`perception.hashers.WaveletHash`)\n\n## Contributing\n\nTo work on the project, start by doing the following.\n\n```bash\n# Install local dependencies for\n# code completion, etc.\nmake init\n\n- To do a (close to) comprehensive check before committing code, you can use `make precommit`.\n\nTo implement new features, please first file an issue proposing your change for discussion.\n\nTo report problems, please file an issue with sample code, expected results, actual results, and a complete traceback.\n\n## Alternatives\n\nThere are other packages worth checking out to see if they meet your needs for perceptual hashing. Here are some\nexamples.\n\n- [dedupe](https://github.com/dedupeio/dedupe)\n- [imagededup](https://idealo.github.io/imagededup/)\n- [ImageHash](https://github.com/JohannesBuchner/imagehash)\n- [PhotoHash](https://github.com/bunchesofdonald/photohash)\n```\n",
43
45
  'author': 'Thorn',
File without changes
File without changes