reverse search

This commit is contained in:
byte[] 2019-11-28 19:11:05 -05:00
parent 109f3f781a
commit 85b1f2b1ec
11 changed files with 214 additions and 28 deletions

View file

@ -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

View file

@ -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)

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -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")

View 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

View file

@ -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

View 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 &nbsp;
th Image
th &nbsp;
= 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.

View file

@ -0,0 +1,5 @@
defmodule PhilomenaWeb.Search.ReverseView do
use PhilomenaWeb, :view
alias Philomena.Tags.Tag
end