diff --git a/lib/philomena/duplicate_reports.ex b/lib/philomena/duplicate_reports.ex index 4c8332d3..ec3a05e5 100644 --- a/lib/philomena/duplicate_reports.ex +++ b/lib/philomena/duplicate_reports.ex @@ -30,7 +30,7 @@ defmodule Philomena.DuplicateReports do 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.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 end diff --git a/lib/philomena/processors.ex b/lib/philomena/processors.ex index c9f0d576..3f745795 100644 --- a/lib/philomena/processors.ex +++ b/lib/philomena/processors.ex @@ -6,31 +6,40 @@ defmodule Philomena.Processors do alias Philomena.Mime alias Philomena.Sha512 - @mimes %{ - "image/gif" => "image/gif", - "image/jpeg" => "image/jpeg", - "image/png" => "image/png", - "image/svg+xml" => "image/svg+xml", - "video/webm" => "video/webm", - "image/svg" => "image/svg+xml", - "audio/webm" => "video/webm" - } + def mimes(type) do + %{ + "image/gif" => "image/gif", + "image/jpeg" => "image/jpeg", + "image/png" => "image/png", + "image/svg+xml" => "image/svg+xml", + "video/webm" => "video/webm", + "image/svg" => "image/svg+xml", + "audio/webm" => "video/webm" + } + |> Map.get(type) + end - @analyzers %{ - "image/gif" => Philomena.Analyzers.Gif, - "image/jpeg" => Philomena.Analyzers.Jpeg, - "image/png" => Philomena.Analyzers.Png, - "image/svg+xml" => Philomena.Analyzers.Svg, - "video/webm" => Philomena.Analyzers.Webm - } + def analyzers(type) do + %{ + "image/gif" => Philomena.Analyzers.Gif, + "image/jpeg" => Philomena.Analyzers.Jpeg, + "image/png" => Philomena.Analyzers.Png, + "image/svg+xml" => Philomena.Analyzers.Svg, + "video/webm" => Philomena.Analyzers.Webm + } + |> Map.get(type) + end - @processors %{ - "image/gif" => Philomena.Processors.Gif, - "image/jpeg" => Philomena.Processors.Jpeg, - "image/png" => Philomena.Processors.Png, - "image/svg+xml" => Philomena.Processors.Svg, - "video/webm" => Philomena.Processors.Webm - } + def processors(type) do + %{ + "image/gif" => Philomena.Processors.Gif, + "image/jpeg" => Philomena.Processors.Jpeg, + "image/png" => Philomena.Processors.Png, + "image/svg+xml" => Philomena.Processors.Svg, + "video/webm" => Philomena.Processors.Webm + } + |> Map.get(type) + end @versions [ thumb_tiny: {50, 50}, @@ -47,8 +56,8 @@ defmodule Philomena.Processors do with upload when not is_nil(upload) <- params["image"], file <- upload.path, {:ok, mime} <- Mime.file(file), - mime <- @mimes[mime], - analyzer when not is_nil(analyzer) <- @analyzers[mime], + mime <- mimes(mime), + analyzer when not is_nil(analyzer) <- analyzers(mime), analysis <- analyzer.analyze(file), changes <- analysis_to_changes(analysis, file, upload.filename) do @@ -73,9 +82,9 @@ defmodule Philomena.Processors do mime = image.image_mime_type file = image_file(image) - analyzer = @analyzers[mime] + analyzer = analyzers(mime) analysis = analyzer.analyze(file) - processor = @processors[mime] + processor = processors(mime) process = processor.process(analysis, file, @versions) apply_edit_script(image, process) diff --git a/lib/philomena/processors/gif.ex b/lib/philomena/processors/gif.ex index 1135c749..4ddb6bef 100644 --- a/lib/philomena/processors/gif.ex +++ b/lib/philomena/processors/gif.ex @@ -21,6 +21,12 @@ defmodule Philomena.Processors.Gif do %{replace_original: optimize(file)} end + def intensities(analysis, file) do + {:ok, intensities} = Intensities.file(preview(analysis.duration, file)) + intensities + end + + defp optimize(file) do optimized = Briefly.create!(extname: ".gif") diff --git a/lib/philomena/processors/jpeg.ex b/lib/philomena/processors/jpeg.ex index 2ccd25b8..eae7ab08 100644 --- a/lib/philomena/processors/jpeg.ex +++ b/lib/philomena/processors/jpeg.ex @@ -18,6 +18,12 @@ defmodule Philomena.Processors.Jpeg do def post_process(_analysis, _file), do: %{} + def intensities(_analysis, file) do + {:ok, intensities} = Intensities.file(file) + intensities + end + + defp strip(file) do stripped = Briefly.create!(extname: ".jpg") diff --git a/lib/philomena/processors/png.ex b/lib/philomena/processors/png.ex index 9e56d9e7..83ca0d59 100644 --- a/lib/philomena/processors/png.ex +++ b/lib/philomena/processors/png.ex @@ -18,6 +18,12 @@ defmodule Philomena.Processors.Png do %{replace_original: optimize(file)} end + def intensities(_analysis, file) do + {:ok, intensities} = Intensities.file(file) + intensities + end + + defp optimize(file) do optimized = Briefly.create!(extname: ".png") diff --git a/lib/philomena/processors/svg.ex b/lib/philomena/processors/svg.ex index cdae03db..8a1d72b4 100644 --- a/lib/philomena/processors/svg.ex +++ b/lib/philomena/processors/svg.ex @@ -16,6 +16,12 @@ defmodule Philomena.Processors.Svg do def post_process(_analysis, _file), do: %{} + def intensities(_analysis, file) do + {:ok, intensities} = Intensities.file(preview(file)) + intensities + end + + defp preview(file) do preview = Briefly.create!(extname: ".png") diff --git a/lib/philomena/processors/webm.ex b/lib/philomena/processors/webm.ex index aad0923d..96d3b412 100644 --- a/lib/philomena/processors/webm.ex +++ b/lib/philomena/processors/webm.ex @@ -20,6 +20,12 @@ defmodule Philomena.Processors.Webm 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 preview = Briefly.create!(extname: ".png") diff --git a/lib/philomena_web/controllers/search/reverse_controller.ex b/lib/philomena_web/controllers/search/reverse_controller.ex new file mode 100644 index 00000000..56c56344 --- /dev/null +++ b/lib/philomena_web/controllers/search/reverse_controller.ex @@ -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 \ No newline at end of file diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index 4f99d933..a75ed0de 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -98,6 +98,9 @@ defmodule PhilomenaWeb.Router do resources "/autocomplete", AutocompleteController, only: [:show], singleton: true end 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 "/forums", ForumController, only: [:index, :show] do resources "/topics", TopicController, only: [:show] do diff --git a/lib/philomena_web/templates/search/reverse/index.html.slime b/lib/philomena_web/templates/search/reverse/index.html.slime new file mode 100644 index 00000000..0d485b84 --- /dev/null +++ b/lib/philomena_web/templates/search/reverse/index.html.slime @@ -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. \ No newline at end of file diff --git a/lib/philomena_web/views/search/reverse_view.ex b/lib/philomena_web/views/search/reverse_view.ex new file mode 100644 index 00000000..498deefa --- /dev/null +++ b/lib/philomena_web/views/search/reverse_view.ex @@ -0,0 +1,5 @@ +defmodule PhilomenaWeb.Search.ReverseView do + use PhilomenaWeb, :view + + alias Philomena.Tags.Tag +end