diff options
| author | Hunternif <hunternif@gmail.com> | 2022-10-02 03:25:10 +0100 |
|---|---|---|
| committer | Hunternif <hunternif@gmail.com> | 2022-10-02 04:18:27 +0100 |
| commit | a82fa8940158784285872434659b5cb7fb9b817a (patch) | |
| tree | d0ffd2795ef2bca181801f04aa83aef9f8031527 | |
| parent | bc44d8055bf3b65365bf5329a8a4ad4ac4177cfc (diff) | |
client,server: reverse search post by signature
| -rw-r--r-- | client/css/post-main-view.styl | 2 | ||||
| -rw-r--r-- | client/html/post_readonly_sidebar.tpl | 5 | ||||
| -rw-r--r-- | client/js/controls/post_readonly_sidebar_control.js | 29 | ||||
| -rw-r--r-- | client/js/models/post_list.js | 19 | ||||
| -rw-r--r-- | server/szurubooru/api/post_api.py | 27 | ||||
| -rw-r--r-- | server/szurubooru/func/posts.py | 18 |
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 [] |