Closed shinyford closed 2 years ago
You are probably applying it on a full-sized image, which is very wasteful. It is much better to scale the image down to a small size first.
Ah, that makes sense. Thanks. (Actually, I've been scaling down to really small - 24x24 - and it's certainly speeded things up; but I think that may be too far. Not the most accurate blurs for the images behind them.)
Could I ask what size you resize images to on the website, to make it so fast to encode the blurhash? I've been trying 50x50, and it's still very slow.
You should tell us what do you mean by slow (in ms or seconds). Something is likely wrong in your implementation
So, for a 500x333 image being compressed to 50x50 and then encoded, it's taking my Elixir implementation c. 9000ms (fetching the image from its url source and scaling to 50x50 using Mogrify takes c. 25ms prior to that).
If I compress the image to 20x20 instead of 50x50, it comes down to c. 250ms.
Is 20x20 a reasonable scaling for an image?
EDIT: Note that this implementation uses xComponents = 6
and yComponents = 6
, which will also make it larger and slower than the normal 4x3 I guess.
Why don't you post your implementation so everyone can help ?
That's a very good idea. :D
defmodule Blurhash do
@characters "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"
@comp_x 6
@comp_y 6
@width 20
@height 20
def hash(url) do
url = URI.encode(url)
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{body: body}} ->
body
|> get_rgb_data(@width, @height)
|> generate_blurhash(@comp_x, @comp_y, @width, @height)
_ -> "error"
end
end
defp get_rgb_data(body, width, height) do
{:ok, path} = Temp.path(suffix: ".jpg")
File.write(path, body)
%Mogrify.Image{path: rgbpath} =
Mogrify.open(path)
|> Mogrify.resize("#{width}x#{height}!")
|> Mogrify.format("rgb")
|> Mogrify.save()
data =
File.read!(rgbpath)
|> :binary.bin_to_list()
File.rm!(path)
File.rm!(rgbpath)
data
end
defp generate_blurhash(data, xc, yc, width, height) do
[dc | ac] = get_factors(0, 0, xc, yc, width, height, data)
qmx = quantised_max_value_of(ac)
mx = (qmx + 1.0) / 166.0
[
encode_value((xc - 1) + (yc - 1) * 9, 1),
encode_value(qmx, 1),
encode_dc(dc)
|
encode_acs(ac, mx)
]
|> Enum.join()
end
defp get_factors(_, yc, _, yc, _, _, _), do: []
defp get_factors(xc, y, xc, yc, w, h, d), do: get_factors(0, y + 1, xc, yc, w, h, d)
defp get_factors(x, y, xc, yc, width, height, data) do
[
multiply_basis_function(x, y, width, height, data)
|
get_factors(x + 1, y, xc, yc, width, height, data)
]
end
defp multiply_basis_function(x, y, w, h, data) do
pi_x = :math.pi() * x / w
pi_y = :math.pi() * y / h
calc_rgb(0, 0, w, h, pi_x, pi_y, data)
|> scale(x, y, w, h)
end
defp calc_rgb(_, h, _, h, _, _, _), do: {0, 0, 0}
defp calc_rgb(w, y, w, h, pi_x, pi_y, data), do: calc_rgb(0, y + 1, w, h, pi_x, pi_y, data)
defp calc_rgb(x, y, w, h, pi_x, pi_y, data) do
{r, g, b} = calc_rgb(x + 1, y, w, h, pi_x, pi_y, data)
basis = :math.cos(pi_x * x) * :math.cos(pi_y * y)
position = 3 * (x + y * w)
{
r + basis * s_rgb_to_linear(Enum.at(data, 0 + position)),
g + basis * s_rgb_to_linear(Enum.at(data, 1 + position)),
b + basis * s_rgb_to_linear(Enum.at(data, 2 + position)),
}
end
defp scale({r, g, b}, x, y, w, h) do
scl = normalisation(x, y) / (w * h)
{r * scl, g * scl, b * scl}
end
defp normalisation(0, 0), do: 1
defp normalisation(_, _), do: 2
defp encode_dc({r, g, b}) do
r = linear_to_s_rgb(r)
g = linear_to_s_rgb(g)
b = linear_to_s_rgb(b)
encode_value((r * 0x10000) + (g * 0x100) + b, 4)
end
defp encode_acs([], _), do: []
defp encode_acs([ac | remaining_acs], mx) do
[encode_ac(ac, mx) | encode_acs(remaining_acs, mx)]
end
defp encode_ac({r, g, b}, mx) do
quant_r = quant(r / mx)
quant_g = quant(g / mx)
quant_b = quant(b / mx)
encode_value((quant_r * 19 * 19) + (quant_g * 19) + quant_b, 2)
end
defp quant(value) do
value
|> sign_pow(0.5)
|> fn v -> v * 9 + 9.5 end.()
|> Kernel.trunc()
|> min(18)
|> max(0)
end
defp sign_pow(v, exp) do
av = abs(v)
:math.pow(av, exp) * (v / av)
end
defp quantised_max_value_of(ac) do
Enum.map(ac, fn {r, g, b} -> [abs(r), abs(g), abs(b)] end)
|> List.flatten()
|> Enum.max()
|> fn x -> Kernel.trunc(x * 166 - 0.5) end.()
|> min(82)
|> max(0)
end
defp s_rgb_to_linear(value) do
v = value / 255.0
if v <= 0.04045 do
v / 12.92
else
:math.pow((v + 0.055) / 1.055, 2.4)
end
end
defp linear_to_s_rgb(value) do
v = max(0, min(1, value))
if v <= 0.0031308 do
v * 12.92 * 255 + 0.5
else
(1.055 * :math.pow(v, 1 / 2.4) - 0.055) * 255 + 0.5
end
end
defp encode_value(_, 0), do: ""
defp encode_value(value, length) do
value = Kernel.trunc(value)
encode_value(div(value, 83), length - 1) <> String.at(@characters, rem(value, 83))
end
end
TBH the blurhashes this produces are a bit lurid, so I'm guessing I've messed up the maths somewhere along the line. But I'm pretty sure that's doing what the C and Dart implementations (which I've used as reference) are doing.
And, having come back to this after six months, finally worked out what was making it lurid: when encoding the dc I was working in floats rather than ints. trunc
ing everything fixes it.
@perzanko Glad to see someone else has got an elixir version out there - nice one
Hi
I'm trying to port the blurhash encoder to Elixir (happy to share when it works) and seem to be getting somewhere. However, it's noticeably slow, which is unusual for Elixir to say the least. (Also tried to implement in Dart/Flutter, and again it appears slow.)
Your Javascript (?) implementation on https://blurha.sh looks like it's pretty much instantaneous. So my question is: am I possibly doing something stupid, or is there something special happening on the website?
Many thanks for this BTW!
Cheers
Nic