summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHunternif <hunternif@gmail.com>2022-10-02 03:25:10 +0100
committerHunternif <hunternif@gmail.com>2022-10-02 04:18:27 +0100
commita82fa8940158784285872434659b5cb7fb9b817a (patch)
treed0ffd2795ef2bca181801f04aa83aef9f8031527
parentbc44d8055bf3b65365bf5329a8a4ad4ac4177cfc (diff)
client,server: reverse search post by signature
-rw-r--r--client/css/post-main-view.styl2
-rw-r--r--client/html/post_readonly_sidebar.tpl5
-rw-r--r--client/js/controls/post_readonly_sidebar_control.js29
-rw-r--r--client/js/models/post_list.js19
-rw-r--r--server/szurubooru/api/post_api.py27
-rw-r--r--server/szurubooru/func/posts.py18
6 files changed, 93 insertions, 7 deletions
diff --git a/client/css/post-main-view.styl b/client/css/post-main-view.styl
index 5fa36c5..6b011f0 100644
--- a/client/css/post-main-view.styl
+++ b/client/css/post-main-view.styl
@@ -125,7 +125,7 @@
order: 2
margin-top: 1em
- .relations, .similar
+ .relations, .similar, .lookalikes
h1
margin-bottom: 0.5em
.thumbnail
diff --git a/client/html/post_readonly_sidebar.tpl b/client/html/post_readonly_sidebar.tpl
index 6c6cbbe..ac22580 100644
--- a/client/html/post_readonly_sidebar.tpl
+++ b/client/html/post_readonly_sidebar.tpl
@@ -88,6 +88,11 @@
<ul></ul>
<a href='<%- ctx.formatClientLink("posts", {query: "similar:" + ctx.post.id}) %>'>See more</a>
</nav>
+
+ <nav class='lookalikes'>
+ <h1>Look-alikes</h1>
+ <ul></ul>
+ </nav>
<% } %>
<nav class='tags'>
diff --git a/client/js/controls/post_readonly_sidebar_control.js b/client/js/controls/post_readonly_sidebar_control.js
index 35cad8f..fc26281 100644
--- a/client/js/controls/post_readonly_sidebar_control.js
+++ b/client/js/controls/post_readonly_sidebar_control.js
@@ -50,6 +50,7 @@ class PostReadonlySidebarControl extends events.EventTarget {
);
}
this._loadSimilarPosts();
+ this._loadLookalikePosts();
}
get _scoreContainerNode() {
@@ -100,6 +101,14 @@ class PostReadonlySidebarControl extends events.EventTarget {
return this._hostNode.querySelector("nav.similar ul");
}
+ get _lookalikesNode() {
+ return this._hostNode.querySelector("nav.lookalikes");
+ }
+
+ get _lookalikesListNode() {
+ return this._hostNode.querySelector("nav.lookalikes ul");
+ }
+
_installFitButtons() {
this._fitBothButtonNode.addEventListener(
"click",
@@ -257,6 +266,26 @@ class PostReadonlySidebarControl extends events.EventTarget {
}
});
}
+
+ _loadLookalikePosts() {
+ const limit = parseInt(settings.get().similarPosts);
+ const fields = ["id", "thumbnailUrl"];
+ const threshold = 1;
+ return PostList.reverseSearch(this._post.id, limit, threshold, fields)
+ .then((response) => {
+ if (response.results.length === 0) {
+ this._lookalikesNode.style.display = "none";
+ }
+ const listNode = this._lookalikesListNode;
+ for (let post of response.results) {
+ let poseNode = similarItemTemplate({
+ id: post.id,
+ thumbnailUrl: post.thumbnailUrl,
+ });
+ listNode.appendChild(poseNode);
+ }
+ });
+ }
}
module.exports = PostReadonlySidebarControl;
diff --git a/client/js/models/post_list.js b/client/js/models/post_list.js
index 1220c7a..a94ff7f 100644
--- a/client/js/models/post_list.js
+++ b/client/js/models/post_list.js
@@ -54,6 +54,25 @@ class PostList extends AbstractList {
});
}
+ static reverseSearch(id, limit, threshold, fields) {
+ return api
+ .get(
+ uri.formatApiLink("post", id, "reverse-search", {
+ limit: limit,
+ threshold: threshold,
+ fields: fields.join(","),
+ })
+ )
+ .then((response) => {
+ const results = response.similarPosts.map((sim) => sim.post);
+ return Promise.resolve(
+ Object.assign({}, response, {
+ results: PostList.fromResponse(results)
+ })
+ );
+ });
+ }
+
static decorateSearchQuery(text) {
const browsingSettings = settings.get();
const disabledSafety = [];
diff --git a/server/szurubooru/api/post_api.py b/server/szurubooru/api/post_api.py
index 2134b3b..3a35bf8 100644
--- a/server/szurubooru/api/post_api.py
+++ b/server/szurubooru/api/post_api.py
@@ -14,7 +14,7 @@ from szurubooru.func import (
similar,
snapshots,
tags,
- versions,
+ versions, image_hash,
)
_search_executor_config = search.configs.PostSearchConfig()
@@ -321,6 +321,31 @@ def get_posts_by_image(
}
+@rest.routes.get("/post/(?P<post_id>[^/]+)/reverse-search/?")
+def get_posts_lookalikes(
+ ctx: rest.Context, params: Dict[str, str] = {}
+) -> rest.Response:
+ auth.verify_privilege(ctx.user, "posts:reverse_search")
+ limit = ctx.get_param_as_int("limit", default=10, min=1, max=100)
+ threshold = ctx.get_param_as_float("threshold", default=1, min=0, max=100)
+ post_id = _get_post_id(params)
+ post = posts.get_post_by_id(post_id)
+ sig = image_hash.unpack_signature(post.signature.signature)
+ lookalikes = posts.search_by_signature(sig, limit, threshold)
+ # exclude the same post:
+ lookalikes = filter(lambda la: la[1].post_id != post_id, lookalikes)
+ lookalikes = sorted(lookalikes, key=lambda la: la[0])
+ return {
+ "similarPosts": [
+ {
+ "distance": distance,
+ "post": _serialize_post(ctx, post),
+ }
+ for distance, post in lookalikes
+ ],
+ }
+
+
@rest.routes.get("/posts/median/?")
def get_posts_median(
ctx: rest.Context, _params: Dict[str, str] = {}
diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py
index 107fc5d..fac7cd5 100644
--- a/server/szurubooru/func/posts.py
+++ b/server/szurubooru/func/posts.py
@@ -21,6 +21,7 @@ from szurubooru.func import (
users,
util,
)
+from szurubooru.func.image_hash import NpMatrix
logger = logging.getLogger(__name__)
@@ -954,8 +955,15 @@ def search_by_image_exact(image_content: bytes) -> Optional[model.Post]:
def search_by_image(image_content: bytes) -> List[Tuple[float, model.Post]]:
query_signature = image_hash.generate_signature(image_content)
- query_words = image_hash.generate_words(query_signature)
+ return search_by_signature(query_signature)
+
+def search_by_signature(
+ signature: NpMatrix,
+ limit: int = 100,
+ distance_cutoff: float = image_hash.DISTANCE_CUTOFF,
+) -> List[Tuple[float, model.Post]]:
+ query_words = image_hash.generate_words(signature)
"""
The unnest function is used here to expand one row containing the 'words'
array into multiple rows each containing a singular word.
@@ -969,10 +977,10 @@ def search_by_image(image_content: bytes) -> List[Tuple[float, model.Post]]:
FROM post_signature AS s, unnest(s.words, :q) AS a(word, query)
WHERE a.word = a.query
GROUP BY s.post_id
- ORDER BY score DESC LIMIT 100;
+ ORDER BY score DESC LIMIT :limit;
"""
- candidates = db.session.execute(dbquery, {"q": query_words})
+ candidates = db.session.execute(dbquery, {"q": query_words, "limit": limit})
data = tuple(
zip(
*[
@@ -983,13 +991,13 @@ def search_by_image(image_content: bytes) -> List[Tuple[float, model.Post]]:
)
if data:
candidate_post_ids, sigarray = data
- distances = image_hash.normalized_distance(sigarray, query_signature)
+ distances = image_hash.normalized_distance(sigarray, signature)
return [
(distance, try_get_post_by_id(candidate_post_id))
for candidate_post_id, distance in zip(
candidate_post_ids, distances
)
- if distance < image_hash.DISTANCE_CUTOFF
+ if distance < distance_cutoff
]
else:
return []