mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-30 14:57:59 +01:00
reverse search
This commit is contained in:
parent
109f3f781a
commit
85b1f2b1ec
11 changed files with 214 additions and 28 deletions
|
@ -30,7 +30,7 @@ defmodule Philomena.DuplicateReports do
|
||||||
where: it.ne >= ^(intensities.ne - dist) and it.ne <= ^(intensities.ne + dist),
|
where: it.ne >= ^(intensities.ne - dist) and it.ne <= ^(intensities.ne + dist),
|
||||||
where: it.sw >= ^(intensities.sw - dist) and it.sw <= ^(intensities.sw + dist),
|
where: it.sw >= ^(intensities.sw - dist) and it.sw <= ^(intensities.sw + dist),
|
||||||
where: it.se >= ^(intensities.se - dist) and it.se <= ^(intensities.se + dist),
|
where: it.se >= ^(intensities.se - dist) and it.se <= ^(intensities.se + dist),
|
||||||
where: i.image_aspect_ratio >= ^(aspect_ratio - aspect_dist) and i.image_aspect_ratio >= ^(aspect_ratio + aspect_dist),
|
where: i.image_aspect_ratio >= ^(aspect_ratio - aspect_dist) and i.image_aspect_ratio <= ^(aspect_ratio + aspect_dist),
|
||||||
limit: 20
|
limit: 20
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -6,31 +6,40 @@ defmodule Philomena.Processors do
|
||||||
alias Philomena.Mime
|
alias Philomena.Mime
|
||||||
alias Philomena.Sha512
|
alias Philomena.Sha512
|
||||||
|
|
||||||
@mimes %{
|
def mimes(type) do
|
||||||
"image/gif" => "image/gif",
|
%{
|
||||||
"image/jpeg" => "image/jpeg",
|
"image/gif" => "image/gif",
|
||||||
"image/png" => "image/png",
|
"image/jpeg" => "image/jpeg",
|
||||||
"image/svg+xml" => "image/svg+xml",
|
"image/png" => "image/png",
|
||||||
"video/webm" => "video/webm",
|
"image/svg+xml" => "image/svg+xml",
|
||||||
"image/svg" => "image/svg+xml",
|
"video/webm" => "video/webm",
|
||||||
"audio/webm" => "video/webm"
|
"image/svg" => "image/svg+xml",
|
||||||
}
|
"audio/webm" => "video/webm"
|
||||||
|
}
|
||||||
|
|> Map.get(type)
|
||||||
|
end
|
||||||
|
|
||||||
@analyzers %{
|
def analyzers(type) do
|
||||||
"image/gif" => Philomena.Analyzers.Gif,
|
%{
|
||||||
"image/jpeg" => Philomena.Analyzers.Jpeg,
|
"image/gif" => Philomena.Analyzers.Gif,
|
||||||
"image/png" => Philomena.Analyzers.Png,
|
"image/jpeg" => Philomena.Analyzers.Jpeg,
|
||||||
"image/svg+xml" => Philomena.Analyzers.Svg,
|
"image/png" => Philomena.Analyzers.Png,
|
||||||
"video/webm" => Philomena.Analyzers.Webm
|
"image/svg+xml" => Philomena.Analyzers.Svg,
|
||||||
}
|
"video/webm" => Philomena.Analyzers.Webm
|
||||||
|
}
|
||||||
|
|> Map.get(type)
|
||||||
|
end
|
||||||
|
|
||||||
@processors %{
|
def processors(type) do
|
||||||
"image/gif" => Philomena.Processors.Gif,
|
%{
|
||||||
"image/jpeg" => Philomena.Processors.Jpeg,
|
"image/gif" => Philomena.Processors.Gif,
|
||||||
"image/png" => Philomena.Processors.Png,
|
"image/jpeg" => Philomena.Processors.Jpeg,
|
||||||
"image/svg+xml" => Philomena.Processors.Svg,
|
"image/png" => Philomena.Processors.Png,
|
||||||
"video/webm" => Philomena.Processors.Webm
|
"image/svg+xml" => Philomena.Processors.Svg,
|
||||||
}
|
"video/webm" => Philomena.Processors.Webm
|
||||||
|
}
|
||||||
|
|> Map.get(type)
|
||||||
|
end
|
||||||
|
|
||||||
@versions [
|
@versions [
|
||||||
thumb_tiny: {50, 50},
|
thumb_tiny: {50, 50},
|
||||||
|
@ -47,8 +56,8 @@ defmodule Philomena.Processors do
|
||||||
with upload when not is_nil(upload) <- params["image"],
|
with upload when not is_nil(upload) <- params["image"],
|
||||||
file <- upload.path,
|
file <- upload.path,
|
||||||
{:ok, mime} <- Mime.file(file),
|
{:ok, mime} <- Mime.file(file),
|
||||||
mime <- @mimes[mime],
|
mime <- mimes(mime),
|
||||||
analyzer when not is_nil(analyzer) <- @analyzers[mime],
|
analyzer when not is_nil(analyzer) <- analyzers(mime),
|
||||||
analysis <- analyzer.analyze(file),
|
analysis <- analyzer.analyze(file),
|
||||||
changes <- analysis_to_changes(analysis, file, upload.filename)
|
changes <- analysis_to_changes(analysis, file, upload.filename)
|
||||||
do
|
do
|
||||||
|
@ -73,9 +82,9 @@ defmodule Philomena.Processors do
|
||||||
|
|
||||||
mime = image.image_mime_type
|
mime = image.image_mime_type
|
||||||
file = image_file(image)
|
file = image_file(image)
|
||||||
analyzer = @analyzers[mime]
|
analyzer = analyzers(mime)
|
||||||
analysis = analyzer.analyze(file)
|
analysis = analyzer.analyze(file)
|
||||||
processor = @processors[mime]
|
processor = processors(mime)
|
||||||
process = processor.process(analysis, file, @versions)
|
process = processor.process(analysis, file, @versions)
|
||||||
|
|
||||||
apply_edit_script(image, process)
|
apply_edit_script(image, process)
|
||||||
|
|
|
@ -21,6 +21,12 @@ defmodule Philomena.Processors.Gif do
|
||||||
%{replace_original: optimize(file)}
|
%{replace_original: optimize(file)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def intensities(analysis, file) do
|
||||||
|
{:ok, intensities} = Intensities.file(preview(analysis.duration, file))
|
||||||
|
intensities
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
defp optimize(file) do
|
defp optimize(file) do
|
||||||
optimized = Briefly.create!(extname: ".gif")
|
optimized = Briefly.create!(extname: ".gif")
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,12 @@ defmodule Philomena.Processors.Jpeg do
|
||||||
|
|
||||||
def post_process(_analysis, _file), do: %{}
|
def post_process(_analysis, _file), do: %{}
|
||||||
|
|
||||||
|
def intensities(_analysis, file) do
|
||||||
|
{:ok, intensities} = Intensities.file(file)
|
||||||
|
intensities
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
defp strip(file) do
|
defp strip(file) do
|
||||||
stripped = Briefly.create!(extname: ".jpg")
|
stripped = Briefly.create!(extname: ".jpg")
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,12 @@ defmodule Philomena.Processors.Png do
|
||||||
%{replace_original: optimize(file)}
|
%{replace_original: optimize(file)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def intensities(_analysis, file) do
|
||||||
|
{:ok, intensities} = Intensities.file(file)
|
||||||
|
intensities
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
defp optimize(file) do
|
defp optimize(file) do
|
||||||
optimized = Briefly.create!(extname: ".png")
|
optimized = Briefly.create!(extname: ".png")
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,12 @@ defmodule Philomena.Processors.Svg do
|
||||||
|
|
||||||
def post_process(_analysis, _file), do: %{}
|
def post_process(_analysis, _file), do: %{}
|
||||||
|
|
||||||
|
def intensities(_analysis, file) do
|
||||||
|
{:ok, intensities} = Intensities.file(preview(file))
|
||||||
|
intensities
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
defp preview(file) do
|
defp preview(file) do
|
||||||
preview = Briefly.create!(extname: ".png")
|
preview = Briefly.create!(extname: ".png")
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,12 @@ defmodule Philomena.Processors.Webm do
|
||||||
|
|
||||||
def post_process(_analysis, _file), do: %{}
|
def post_process(_analysis, _file), do: %{}
|
||||||
|
|
||||||
|
def intensities(analysis, file) do
|
||||||
|
{:ok, intensities} = Intensities.file(preview(analysis.duration, file))
|
||||||
|
intensities
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
defp preview(duration, file) do
|
defp preview(duration, file) do
|
||||||
preview = Briefly.create!(extname: ".png")
|
preview = Briefly.create!(extname: ".png")
|
||||||
|
|
||||||
|
|
65
lib/philomena_web/controllers/search/reverse_controller.ex
Normal file
65
lib/philomena_web/controllers/search/reverse_controller.ex
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
defmodule PhilomenaWeb.Search.ReverseController do
|
||||||
|
use PhilomenaWeb, :controller
|
||||||
|
|
||||||
|
alias Philomena.Processors
|
||||||
|
alias Philomena.DuplicateReports
|
||||||
|
alias Philomena.Repo
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
plug PhilomenaWeb.ScraperPlug, [params_key: "image", params_name: "image"] when action in [:create]
|
||||||
|
|
||||||
|
def index(conn, _params) do
|
||||||
|
render(conn, "index.html", images: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create(conn, %{"image" => image_params}) do
|
||||||
|
images =
|
||||||
|
image_params["image"].path
|
||||||
|
|> mime()
|
||||||
|
|> analyze()
|
||||||
|
|> intensities()
|
||||||
|
|> case do
|
||||||
|
:error ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
{analysis, intensities} ->
|
||||||
|
{width, height} = analysis.dimensions
|
||||||
|
aspect = width / height
|
||||||
|
dist = normalize_dist(image_params)
|
||||||
|
|
||||||
|
DuplicateReports.duplicates_of(intensities, aspect, dist, dist)
|
||||||
|
|> preload(:tags)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> render("index.html", images: images)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp mime(file) do
|
||||||
|
{:ok, mime} = Philomena.Mime.file(file)
|
||||||
|
|
||||||
|
{mime, file}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp analyze({mime, file}) do
|
||||||
|
case Processors.analyzers(mime) do
|
||||||
|
nil -> :error
|
||||||
|
a -> {a.analyze(file), mime, file}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp intensities(:error), do: :error
|
||||||
|
defp intensities({analysis, mime, file}) do
|
||||||
|
{analysis, Processors.processors(mime).intensities(analysis, file)}
|
||||||
|
end
|
||||||
|
|
||||||
|
# The distance metric is taxicab distance, not Euclidean,
|
||||||
|
# because this is more efficient to index.
|
||||||
|
defp normalize_dist(%{"distance" => distance}) do
|
||||||
|
distance
|
||||||
|
|> String.to_float()
|
||||||
|
|> max(0.01)
|
||||||
|
|> min(1.0)
|
||||||
|
end
|
||||||
|
end
|
|
@ -98,6 +98,9 @@ defmodule PhilomenaWeb.Router do
|
||||||
resources "/autocomplete", AutocompleteController, only: [:show], singleton: true
|
resources "/autocomplete", AutocompleteController, only: [:show], singleton: true
|
||||||
end
|
end
|
||||||
resources "/tags", TagController, only: [:index, :show]
|
resources "/tags", TagController, only: [:index, :show]
|
||||||
|
scope "/search", Search, as: :search do
|
||||||
|
resources "/reverse", ReverseController, only: [:index, :create]
|
||||||
|
end
|
||||||
resources "/search", SearchController, only: [:index]
|
resources "/search", SearchController, only: [:index]
|
||||||
resources "/forums", ForumController, only: [:index, :show] do
|
resources "/forums", ForumController, only: [:index, :show] do
|
||||||
resources "/topics", TopicController, only: [:show] do
|
resources "/topics", TopicController, only: [:show] do
|
||||||
|
|
74
lib/philomena_web/templates/search/reverse/index.html.slime
Normal file
74
lib/philomena_web/templates/search/reverse/index.html.slime
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
h1 Reverse Search
|
||||||
|
|
||||||
|
= form_for :image, Routes.search_reverse_path(@conn, :create), [multipart: true], 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 GIFs;
|
||||||
|
' very low contrast images (such as sketches) will produce poor results
|
||||||
|
' and, regardless of contrast, results may include seemingly random images
|
||||||
|
' that look very different.
|
||||||
|
|
||||||
|
.image-other
|
||||||
|
#js-image-upload-previews
|
||||||
|
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"
|
||||||
|
|
||||||
|
.field.field--inline
|
||||||
|
= url_input f, :scraper_url, class: "input input--wide js-scraper", placeholder: "Link a deviantART page, a Tumblr post, or the image directly"
|
||||||
|
button.button.button--separate-left#js-scraper-preview type="button" title="Fetch the image at the specified URL" data-disable-with="Fetch"
|
||||||
|
' Fetch
|
||||||
|
|
||||||
|
.field-error-js.hidden.js-scraper
|
||||||
|
|
||||||
|
h4 Optional settings
|
||||||
|
|
||||||
|
.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"
|
||||||
|
|
||||||
|
.field
|
||||||
|
= submit "Reverse Search", class: "button"
|
||||||
|
|
||||||
|
= cond do
|
||||||
|
- is_nil(@images) ->
|
||||||
|
|
||||||
|
- Enum.any?(@images) ->
|
||||||
|
h2 Results
|
||||||
|
|
||||||
|
table
|
||||||
|
tr
|
||||||
|
th
|
||||||
|
th Image
|
||||||
|
th
|
||||||
|
|
||||||
|
= for match <- @images do
|
||||||
|
tr
|
||||||
|
td
|
||||||
|
h3 = link "##{match.id}", to: Routes.image_path(@conn, :show, match)
|
||||||
|
p
|
||||||
|
= if match.source_url not in [nil, ""] do
|
||||||
|
span.source_url
|
||||||
|
= link "Source", to: match.source_url
|
||||||
|
- else
|
||||||
|
' Unknown source
|
||||||
|
|
||||||
|
td
|
||||||
|
= render PhilomenaWeb.ImageView, "_image_container.html", image: match, size: :thumb, conn: @conn
|
||||||
|
|
||||||
|
td
|
||||||
|
h3
|
||||||
|
= match.image_width
|
||||||
|
| x
|
||||||
|
=> match.image_height
|
||||||
|
' -
|
||||||
|
=> round(match.image_size / 1024)
|
||||||
|
' KiB
|
||||||
|
|
||||||
|
= render PhilomenaWeb.TagView, "_tag_list.html", tags: Tag.display_order(match.tags)
|
||||||
|
|
||||||
|
- true ->
|
||||||
|
h2 Results
|
||||||
|
p
|
||||||
|
' We couldn't find any images matching this in our image database.
|
5
lib/philomena_web/views/search/reverse_view.ex
Normal file
5
lib/philomena_web/views/search/reverse_view.ex
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
defmodule PhilomenaWeb.Search.ReverseView do
|
||||||
|
use PhilomenaWeb, :view
|
||||||
|
|
||||||
|
alias Philomena.Tags.Tag
|
||||||
|
end
|
Loading…
Reference in a new issue