mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-23 20:18:00 +01:00
Convert reverse search parsing to changeset-backed form
This commit is contained in:
parent
e6a63f4c25
commit
7a5d26144a
7 changed files with 163 additions and 83 deletions
|
@ -8,6 +8,8 @@ defmodule Philomena.DuplicateReports do
|
|||
alias Philomena.Repo
|
||||
|
||||
alias Philomena.DuplicateReports.DuplicateReport
|
||||
alias Philomena.DuplicateReports.SearchQuery
|
||||
alias Philomena.DuplicateReports.Uploader
|
||||
alias Philomena.ImageIntensities.ImageIntensity
|
||||
alias Philomena.Images.Image
|
||||
alias Philomena.Images
|
||||
|
@ -47,6 +49,63 @@ defmodule Philomena.DuplicateReports do
|
|||
limit: ^limit
|
||||
end
|
||||
|
||||
@doc """
|
||||
Executes the reverse image search query from parameters.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> execute_search_query(%{"image" => ..., "distance" => "0.25"})
|
||||
{:ok, [%Image{...}, ....]}
|
||||
|
||||
iex> execute_search_query(%{"image" => ..., "distance" => "asdf"})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def execute_search_query(attrs \\ %{}) do
|
||||
%SearchQuery{}
|
||||
|> SearchQuery.changeset(attrs)
|
||||
|> Uploader.analyze_upload(attrs)
|
||||
|> Ecto.Changeset.apply_action(:create)
|
||||
|> case do
|
||||
{:ok, search_query} ->
|
||||
intensities = generate_intensities(search_query)
|
||||
aspect = search_query.image_aspect_ratio
|
||||
limit = search_query.limit
|
||||
dist = search_query.distance
|
||||
|
||||
images =
|
||||
{intensities, aspect}
|
||||
|> find_duplicates(dist: dist, aspect_dist: dist, limit: limit)
|
||||
|> preload([:user, :intensity, [:sources, tags: :aliases]])
|
||||
|> Repo.all()
|
||||
|
||||
{:ok, images}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp generate_intensities(search_query) do
|
||||
analysis = SearchQuery.to_analysis(search_query)
|
||||
file = search_query.uploaded_image
|
||||
|
||||
PhilomenaMedia.Processors.intensities(analysis, file)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking search query changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_search_query(search_query)
|
||||
%Ecto.Changeset{source: %SearchQuery{}}
|
||||
|
||||
"""
|
||||
def change_search_query(%SearchQuery{} = search_query) do
|
||||
SearchQuery.changeset(search_query)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single duplicate_report.
|
||||
|
||||
|
|
59
lib/philomena/duplicate_reports/search_query.ex
Normal file
59
lib/philomena/duplicate_reports/search_query.ex
Normal file
|
@ -0,0 +1,59 @@
|
|||
defmodule Philomena.DuplicateReports.SearchQuery do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
embedded_schema do
|
||||
field :distance, :float, default: 0.25
|
||||
field :limit, :integer, default: 10
|
||||
|
||||
field :image_width, :integer
|
||||
field :image_height, :integer
|
||||
field :image_format, :string
|
||||
field :image_duration, :float
|
||||
field :image_mime_type, :string
|
||||
field :image_is_animated, :boolean
|
||||
field :image_aspect_ratio, :float
|
||||
field :uploaded_image, :string, virtual: true
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(search_query, attrs \\ %{}) do
|
||||
search_query
|
||||
|> cast(attrs, [:distance, :limit])
|
||||
|> validate_number(:distance, greater_than_or_equal_to: 0, less_than_or_equal_to: 1)
|
||||
|> validate_number(:limit, greater_than_or_equal_to: 1, less_than_or_equal_to: 50)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def image_changeset(search_query, attrs \\ %{}) do
|
||||
search_query
|
||||
|> cast(attrs, [
|
||||
:image_width,
|
||||
:image_height,
|
||||
:image_format,
|
||||
:image_duration,
|
||||
:image_mime_type,
|
||||
:image_is_animated,
|
||||
:image_aspect_ratio,
|
||||
:uploaded_image
|
||||
])
|
||||
|> validate_number(:image_width, greater_than: 0)
|
||||
|> validate_number(:image_height, greater_than: 0)
|
||||
|> validate_inclusion(
|
||||
:image_mime_type,
|
||||
~W(image/gif image/jpeg image/png image/svg+xml video/webm),
|
||||
message: "(#{attrs["image_mime_type"]}) is invalid"
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def to_analysis(search_query) do
|
||||
%PhilomenaMedia.Analyzers.Result{
|
||||
animated?: search_query.image_is_animated,
|
||||
dimensions: {search_query.image_width, search_query.image_height},
|
||||
duration: search_query.image_duration,
|
||||
extension: search_query.image_format,
|
||||
mime_type: search_query.image_mime_type
|
||||
}
|
||||
end
|
||||
end
|
17
lib/philomena/duplicate_reports/uploader.ex
Normal file
17
lib/philomena/duplicate_reports/uploader.ex
Normal file
|
@ -0,0 +1,17 @@
|
|||
defmodule Philomena.DuplicateReports.Uploader do
|
||||
@moduledoc """
|
||||
Upload and processing callback logic for SearchQuery images.
|
||||
"""
|
||||
|
||||
alias Philomena.DuplicateReports.SearchQuery
|
||||
alias PhilomenaMedia.Uploader
|
||||
|
||||
def analyze_upload(search_query, params) do
|
||||
Uploader.analyze_upload(
|
||||
search_query,
|
||||
"image",
|
||||
params["image"],
|
||||
&SearchQuery.image_changeset/2
|
||||
)
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
defmodule PhilomenaWeb.Api.Json.Search.ReverseController do
|
||||
use PhilomenaWeb, :controller
|
||||
|
||||
alias PhilomenaWeb.ImageReverse
|
||||
alias Philomena.DuplicateReports
|
||||
alias Philomena.Interactions
|
||||
|
||||
plug PhilomenaWeb.ScraperCachePlug
|
||||
|
@ -14,7 +14,14 @@ defmodule PhilomenaWeb.Api.Json.Search.ReverseController do
|
|||
image_params
|
||||
|> Map.put("distance", conn.params["distance"])
|
||||
|> Map.put("limit", conn.params["limit"])
|
||||
|> ImageReverse.images()
|
||||
|> DuplicateReports.execute_search_query()
|
||||
|> case do
|
||||
{:ok, images} ->
|
||||
images
|
||||
|
||||
{:error, _changeset} ->
|
||||
[]
|
||||
end
|
||||
|
||||
interactions = Interactions.user_interactions(images, user)
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
defmodule PhilomenaWeb.Search.ReverseController do
|
||||
use PhilomenaWeb, :controller
|
||||
|
||||
alias PhilomenaWeb.ImageReverse
|
||||
alias Philomena.DuplicateReports.SearchQuery
|
||||
alias Philomena.DuplicateReports
|
||||
|
||||
plug PhilomenaWeb.ScraperCachePlug
|
||||
plug PhilomenaWeb.ScraperPlug, params_key: "image", params_name: "image"
|
||||
|
@ -12,12 +13,18 @@ defmodule PhilomenaWeb.Search.ReverseController do
|
|||
|
||||
def create(conn, %{"image" => image_params})
|
||||
when is_map(image_params) and image_params != %{} do
|
||||
images = ImageReverse.images(image_params)
|
||||
case DuplicateReports.execute_search_query(image_params) do
|
||||
{:ok, images} ->
|
||||
changeset = DuplicateReports.change_search_query(%SearchQuery{})
|
||||
render(conn, "index.html", title: "Reverse Search", images: images, changeset: changeset)
|
||||
|
||||
render(conn, "index.html", title: "Reverse Search", images: images)
|
||||
{:error, changeset} ->
|
||||
render(conn, "index.html", title: "Reverse Search", images: nil, changeset: changeset)
|
||||
end
|
||||
end
|
||||
|
||||
def create(conn, _params) do
|
||||
render(conn, "index.html", title: "Reverse Search", images: nil)
|
||||
changeset = DuplicateReports.change_search_query(%SearchQuery{})
|
||||
render(conn, "index.html", title: "Reverse Search", images: nil, changeset: changeset)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
defmodule PhilomenaWeb.ImageReverse do
|
||||
alias PhilomenaMedia.Analyzers
|
||||
alias PhilomenaMedia.Processors
|
||||
alias Philomena.DuplicateReports
|
||||
alias Philomena.Repo
|
||||
import Ecto.Query
|
||||
|
||||
def images(image_params) do
|
||||
image_params
|
||||
|> Map.get("image")
|
||||
|> analyze()
|
||||
|> intensities()
|
||||
|> case do
|
||||
:error ->
|
||||
[]
|
||||
|
||||
{analysis, intensities} ->
|
||||
{width, height} = analysis.dimensions
|
||||
aspect = width / height
|
||||
dist = parse_dist(image_params)
|
||||
limit = parse_limit(image_params)
|
||||
|
||||
{intensities, aspect}
|
||||
|> DuplicateReports.find_duplicates(dist: dist, aspect_dist: dist, limit: limit)
|
||||
|> preload([:user, :intensity, [:sources, tags: :aliases]])
|
||||
|> Repo.all()
|
||||
end
|
||||
end
|
||||
|
||||
defp analyze(%Plug.Upload{path: path}) do
|
||||
case Analyzers.analyze(path) do
|
||||
{:ok, analysis} -> {analysis, path}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp analyze(_upload), do: :error
|
||||
|
||||
defp intensities(:error), do: :error
|
||||
|
||||
defp intensities({analysis, path}) do
|
||||
{analysis, Processors.intensities(analysis, path)}
|
||||
end
|
||||
|
||||
# The distance metric is taxicab distance, not Euclidean,
|
||||
# because this is more efficient to index.
|
||||
defp parse_dist(%{"distance" => distance}) do
|
||||
distance
|
||||
|> Decimal.parse()
|
||||
|> case do
|
||||
{value, _rest} -> Decimal.to_float(value)
|
||||
_ -> 0.25
|
||||
end
|
||||
|> clamp(0.01, 1.0)
|
||||
end
|
||||
|
||||
defp parse_dist(_params), do: 0.25
|
||||
|
||||
defp parse_limit(%{"limit" => limit}) do
|
||||
limit
|
||||
|> Integer.parse()
|
||||
|> case do
|
||||
{limit, _rest} -> limit
|
||||
_ -> 10
|
||||
end
|
||||
|> clamp(1, 50)
|
||||
end
|
||||
|
||||
defp parse_limit(_params), do: 10
|
||||
|
||||
defp clamp(n, min, _max) when n < min, do: min
|
||||
defp clamp(n, _min, max) when n > max, do: max
|
||||
defp clamp(n, _min, _max), do: n
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
h1 Reverse Search
|
||||
|
||||
= form_for :image, ~p"/search/reverse", [multipart: true], fn f ->
|
||||
= form_for @changeset, ~p"/search/reverse", [multipart: true, as: :image], fn f ->
|
||||
p
|
||||
' Basic image similarity search. Finds uploaded images similar to the one
|
||||
' provided based on simple intensities and uses the median frame of
|
||||
|
@ -13,6 +13,10 @@ h1 Reverse Search
|
|||
p Upload a file from your computer, or provide a link to the page containing the image and click Fetch.
|
||||
.field
|
||||
= file_input f, :image, class: "input js-scraper"
|
||||
= error_tag f, :image
|
||||
= error_tag f, :image_width
|
||||
= error_tag f, :image_height
|
||||
= error_tag f, :image_mime_type
|
||||
|
||||
.field.field--inline
|
||||
= url_input f, :url, name: "url", class: "input input--wide js-scraper", placeholder: "Link a deviantART page, a Tumblr post, or the image directly"
|
||||
|
@ -26,9 +30,10 @@ h1 Reverse Search
|
|||
.field
|
||||
= label f, :distance, "Match distance (suggested values: between 0.2 and 0.5)"
|
||||
br
|
||||
= number_input f, :distance, value: 0.25, min: 0, max: 1, step: 0.01, class: "input"
|
||||
= number_input f, :distance, min: 0, max: 1, step: 0.01, class: "input"
|
||||
= error_tag f, :distance
|
||||
|
||||
= hidden_input f, :limit, value: 10
|
||||
= error_tag f, :limit
|
||||
|
||||
.field
|
||||
= submit "Reverse Search", class: "button"
|
||||
|
|
Loading…
Reference in a new issue