SQL 中的 Delta E(CIE 实验室)计算和排序性能
Performance of Delta E (CIE Lab) calculating and sorting in SQL
我有一个数据库 table,其中每一行都是一种颜色。我的目标:给定一个输入颜色,计算它与 DB table 中每种颜色的距离,并按该距离对结果进行排序。或者,作为用户故事陈述:当我选择一种颜色时,我想查看与我选择的颜色最相似的颜色列表,最接近的匹配项位于列表顶部。
据我所知,为了做到这一点,各种 Delta E (CIE Lab) formulae are the best choice. I wasn't able to find any native SQL implementations of the formulae, so I wrote my own SQL versions of Delta E CIE 1976 and Delta E CIE 2000. I verified the accuracy of my SQL versions of the formulae, against the results generated by the python-colormath 实现。
1976 公式很容易用 SQL 或任何其他语言编写,因为它是一个简单的欧氏距离计算。它对我来说在任何大小的数据集上都表现良好且快速(在具有 100,000 行的颜色 table 上测试它,查询时间不到 1 秒)。
相比之下,2000 年的公式非常长且复杂。我设法在 SQL 中实现了它,但它的性能并不好:查询 10,000 行大约需要 5 秒,查询 100,000 行大约需要 1 分钟。
我写了一篇example app called colorsearchtest (in Python / Flask / Postgres), to play around with my implementations (and I set up a demo on Heroku)。如果您试用此应用程序,您可以清楚地看到 1976 年和 2000 年 Delta E 查询之间的性能差异。
这是颜色 table 的架构(对于每种颜色,它存储各自的 RGB 和 Lab 表示,作为三个数值):
CREATE TABLE color (
id integer NOT NULL,
rgb_r integer,
rgb_g integer,
rgb_b integer,
lab_l double precision,
lab_a double precision,
lab_b double precision
);
这是 table 中的一些数据(所有颜色都是随机的,由我的应用程序中的脚本生成):
INSERT INTO color (id, rgb_r, rgb_g, rgb_b, lab_l, lab_a, lab_b)
VALUES (902, 164, 214, 189, 81.6521019943304793,
-21.2561872439361323, 7.08354581694699004);
INSERT INTO color (id, rgb_r, rgb_g, rgb_b, lab_l, lab_a, lab_b)
VALUES (903, 113, 229, 64, 81.7930860963098212,
-60.5865728472875205, 66.4022741184551819);
INSERT INTO color (id, rgb_r, rgb_g, rgb_b, lab_l, lab_a, lab_b)
VALUES (904, 65, 86, 78, 34.6593864327796624,
-9.95482220634028003, 2.02661293272071719);
...
这是我正在使用的 Delta E CIE 2000 SQL 函数:
CREATE OR REPLACE FUNCTION
DELTA_E_CIE2000(double precision, double precision,
double precision, double precision,
double precision, double precision,
double precision, double precision,
double precision)
RETURNS double precision
AS $$
WITH
c AS (SELECT
(CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR))
AS lab_pair_str,
(( + ) /
2.0)
AS avg_lp,
SQRT(
POW(, 2.0) +
POW(, 2.0))
AS c1,
SQRT(
POW((), 2.0) +
POW((), 2.0))
AS c2),
gs AS (SELECT
c.lab_pair_str,
(0.5 *
(1.0 - SQRT(
POW(((c.c1 + c.c2) / 2.0), 7.0) / (
POW(((c.c1 + c.c2) / 2.0), 7.0) +
POW(25.0, 7.0)))))
AS g
FROM c
WHERE c.lab_pair_str = (
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR))),
ap AS (SELECT
gs.lab_pair_str,
((1.0 + gs.g) * )
AS a1p,
((1.0 + gs.g) * )
AS a2p
FROM gs
WHERE gs.lab_pair_str = (
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR))),
cphp AS (SELECT
ap.lab_pair_str,
SQRT(
POW(ap.a1p, 2.0) +
POW(, 2.0))
AS c1p,
SQRT(
POW(ap.a2p, 2.0) +
POW(, 2.0))
AS c2p,
(
DEGREES(ATAN2(, ap.a1p)) + (
CASE
WHEN DEGREES(ATAN2(, ap.a1p)) < 0.0
THEN 360.0
ELSE 0.0
END))
AS h1p,
(
DEGREES(ATAN2(, ap.a2p)) + (
CASE
WHEN DEGREES(ATAN2(, ap.a2p)) < 0.0
THEN 360.0
ELSE 0.0
END))
AS h2p
FROM ap
WHERE ap.lab_pair_str = (
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR))),
av AS (SELECT
cphp.lab_pair_str,
((cphp.c1p + cphp.c2p) /
2.0)
AS avg_c1p_c2p,
(((CASE
WHEN (ABS(cphp.h1p - cphp.h2p) > 180.0)
THEN 360.0
ELSE 0.0
END) +
cphp.h1p +
cphp.h2p) /
2.0)
AS avg_hp
FROM cphp
WHERE cphp.lab_pair_str = (
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR))),
ts AS (SELECT
av.lab_pair_str,
(1.0 -
0.17 * COS(RADIANS(av.avg_hp - 30.0)) +
0.24 * COS(RADIANS(2.0 * av.avg_hp)) +
0.32 * COS(RADIANS(3.0 * av.avg_hp + 6.0)) -
0.2 * COS(RADIANS(4.0 * av.avg_hp - 63.0)))
AS t,
((
(cphp.h2p - cphp.h1p) +
(CASE
WHEN (ABS(cphp.h2p - cphp.h1p) > 180.0)
THEN 360.0
ELSE 0.0
END))
-
(CASE
WHEN (cphp.h2p > cphp.h1p)
THEN 720.0
ELSE 0.0
END))
AS delta_hlp
FROM av
INNER JOIN cphp
ON av.lab_pair_str = cphp.lab_pair_str
WHERE av.lab_pair_str = (
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR))),
d AS (SELECT
ts.lab_pair_str,
( - )
AS delta_lp,
(cphp.c2p - cphp.c1p)
AS delta_cp,
(2.0 * (
SQRT(cphp.c2p * cphp.c1p) *
SIN(RADIANS(ts.delta_hlp) / 2.0)))
AS delta_hp,
(1.0 + (
(0.015 * POW(c.avg_lp - 50.0, 2.0)) /
SQRT(20.0 + POW(c.avg_lp - 50.0, 2.0))))
AS s_l,
(1.0 + 0.045 * av.avg_c1p_c2p)
AS s_c,
(1.0 + 0.015 * av.avg_c1p_c2p * ts.t)
AS s_h,
(30.0 * EXP(-(POW(((av.avg_hp - 275.0) / 25.0), 2.0))))
AS delta_ro,
SQRT(
(POW(av.avg_c1p_c2p, 7.0)) /
(POW(av.avg_c1p_c2p, 7.0) + POW(25.0, 7.0)))
AS r_c
FROM ts
INNER JOIN cphp
ON ts.lab_pair_str = cphp.lab_pair_str
INNER JOIN c
ON ts.lab_pair_str = c.lab_pair_str
INNER JOIN av
ON ts.lab_pair_str = av.lab_pair_str
WHERE ts.lab_pair_str = (
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR))),
r AS (SELECT
d.lab_pair_str,
(-2.0 * d.r_c * SIN(2.0 * RADIANS(d.delta_ro)))
AS r_t
FROM d
WHERE d.lab_pair_str = (
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR)))
SELECT
SQRT(
POW(d.delta_lp / (d.s_l * ), 2.0) +
POW(d.delta_cp / (d.s_c * ), 2.0) +
POW(d.delta_hp / (d.s_h * ), 2.0) +
r.r_t *
(d.delta_cp / (d.s_c * )) *
(d.delta_hp / (d.s_h * )))
AS delta_e_cie2000
FROM r
INNER JOIN d
ON r.lab_pair_str = d.lab_pair_str
WHERE r.lab_pair_str = (
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR))
$$
LANGUAGE SQL
IMMUTABLE
RETURNS NULL ON NULL INPUT;
(我最初使用大约 10 层深的嵌套子查询编写此函数,但随后我将其重新编写为使用 WITH
语句,即 Postgres CTE。新版本更具可读性和性能和老版本差不多,可以看both versions in the code.)
定义函数后,我在这样的查询中使用它:
SELECT c.rgb_r,
c.rgb_g,
c.rgb_b,
DELTA_E_CIE2000(73.9206633504, -50.2996953437,
23.8259166281,
c.lab_l, c.lab_a, c.lab_b,
1.0, 1.0, 1.0)
AS de2000
FROM color c
ORDER BY de2000
LIMIT 100;
所以,我的问题是:有什么方法可以提高 DELTA_E_CIE2000
函数的性能,使其可实时用于非平凡数据集?或者,考虑到公式的复杂性,它会尽可能快吗?
根据我在我的演示应用程序中所做的测试,我会说对于在网站上进行简单 "similar colors" 搜索的用例,1976 和 1976 之间的结果准确性差异2000的功能其实可以忽略不计。也就是说,我已经确信 1976 年的公式符合我的需要 "good enough"。然而,2000 函数 return 的结果稍微好一些(很大程度上取决于输入颜色在 Lab space 中的位置),实际上,我只是好奇它是否可以加速进一步。
两件事:1) 您没有充分利用数据库,2) 您的问题是自定义 PostgreSQL 扩展的一个很好的例子。原因如下。
您仅使用数据库作为存储,将颜色存储为浮点数。在您当前的配置中,无论查询类型如何,数据库将始终必须检查所有值(进行顺序扫描)。这意味着大量的 IO 和为少数返回的匹配项进行的大量计算。您正试图找到最接近的 N 种颜色,因此有几种方法可以避免对所有数据执行计算。
简单的改进
最简单的方法是将您的计算限制在较小的数据子集中。如果组件差异更大,您可以假设差异会更大。如果您可以找到组件之间的安全差异(结果总是不合适),则可以使用带 btree 索引的范围 WHERE 完全排除这些颜色。但是,由于 L*a*b 颜色的性质space,这可能会使您的结果变差。
首先创建索引:
CREATE INDEX color_lab_l_btree ON color USING btree (lab_l);
CREATE INDEX color_lab_a_btree ON color USING btree (lab_a);
CREATE INDEX color_lab_b_btree ON color USING btree (lab_b);
然后我调整了您的查询以包含一个 WHERE 子句以仅过滤颜色,其中任何组件最多有 20 个不同。
更新: 再看一遍,加个limit 20很可能会使结果变差,因为我在space中至少发现了一个点,为此成立。:
SELECT
c.rgb_r, c.rgb_g, c.rgb_b,
DELTA_E_CIE2000(
25.805780252087963, 53.33446637366859, -45.03961353720049,
c.lab_l, c.lab_a, c.lab_b,
1.0, 1.0, 1.0) AS de2000
FROM color c
WHERE
c.lab_l BETWEEN 25.805780252087963 - 20 AND 25.805780252087963 + 20
AND c.lab_a BETWEEN 53.33446637366859 - 20 AND 53.33446637366859 + 20
AND c.lab_b BETWEEN -45.03961353720049 - 20 AND -45.03961353720049 + 20
ORDER BY de2000 ;
我用你的脚本填充了 table 100000 种随机颜色并进行了测试:
没有索引的时间:44006,851 毫秒
索引和范围查询时间:1293,092 毫秒
您也可以将此 WHERE 子句添加到 delta_e_cie1976_query
,在我的随机数据上,它将查询时间从 ~110 毫秒减少到 ~22 毫秒。
顺便说一句:根据经验,我得到了 20:我尝试了 10,但只得到了 380 条记录,这似乎有点低,并且可能会排除一些更好的选择,因为限制是 100。20 的完整集是 2900 行并且可以相当确定最接近的比赛将在那里。我没有详细研究 DELTA_E_CIE2000 或 L*a*b* 颜色 space 因此阈值可能需要根据不同的组件进行调整才能真正实现,但排除不感兴趣的原则数据保持。
用 C 重写 Delta E CIE 2000
正如您已经说过的,Delta E CIE 2000 很复杂并且相当不适合 table 在 SQL 中实施。它目前在我的笔记本电脑上每次通话使用大约 0.4 毫秒。在 C 中实现它应该会大大加快速度。 PostgreSQL 将默认成本分配给 SQL 函数为 100,C 函数为 1。我猜这是基于实际经验。
更新: 因为这也解决了我的一个问题,我重新实现了 C 中 colormath 模块的 Delta E 函数作为 PostgreSQL 扩展,可在 PGXN。有了这个,当从 table 中查询具有 100k 条记录的所有记录时,我可以看到 CIE2000 的加速大约为 150 倍。
使用此 C 函数,对于 10 万种颜色,我得到的查询时间在 147 毫秒到 160 毫秒之间。有了额外的 WHERE,查询时间大约是 20 毫秒,这对我来说似乎很容易接受table。
最佳但先进的解决方案
但是,由于您的问题是 3 维中的 N 个最近邻搜索 space,您可以使用 PostgreSQL since version 9.1 中的 K-最近邻索引。
为此,您需要将 L*a*b* 组件放入 cube. This extension does not yet support distance operator (it's in the works),但即使可以,它也不支持 Delta E 距离,您需要重新实现它作为 C 扩展。
这意味着实现 GiST 索引运算符 class(btree_gist PostgreSQL extension in contrib does this) to support indexing according to Delta E distances. The good part is you could then use different operators for different versions of Delta E, eg. <->
for Delta E CIE 2000 and <#>
for Delta E CIE 1976 and queries would be really really fast 用于小 LIMIT,即使使用 Delta E CIE 2000。
最终可能取决于您的(业务)要求和限制。
我有一个数据库 table,其中每一行都是一种颜色。我的目标:给定一个输入颜色,计算它与 DB table 中每种颜色的距离,并按该距离对结果进行排序。或者,作为用户故事陈述:当我选择一种颜色时,我想查看与我选择的颜色最相似的颜色列表,最接近的匹配项位于列表顶部。
据我所知,为了做到这一点,各种 Delta E (CIE Lab) formulae are the best choice. I wasn't able to find any native SQL implementations of the formulae, so I wrote my own SQL versions of Delta E CIE 1976 and Delta E CIE 2000. I verified the accuracy of my SQL versions of the formulae, against the results generated by the python-colormath 实现。
1976 公式很容易用 SQL 或任何其他语言编写,因为它是一个简单的欧氏距离计算。它对我来说在任何大小的数据集上都表现良好且快速(在具有 100,000 行的颜色 table 上测试它,查询时间不到 1 秒)。
相比之下,2000 年的公式非常长且复杂。我设法在 SQL 中实现了它,但它的性能并不好:查询 10,000 行大约需要 5 秒,查询 100,000 行大约需要 1 分钟。
我写了一篇example app called colorsearchtest (in Python / Flask / Postgres), to play around with my implementations (and I set up a demo on Heroku)。如果您试用此应用程序,您可以清楚地看到 1976 年和 2000 年 Delta E 查询之间的性能差异。
这是颜色 table 的架构(对于每种颜色,它存储各自的 RGB 和 Lab 表示,作为三个数值):
CREATE TABLE color (
id integer NOT NULL,
rgb_r integer,
rgb_g integer,
rgb_b integer,
lab_l double precision,
lab_a double precision,
lab_b double precision
);
这是 table 中的一些数据(所有颜色都是随机的,由我的应用程序中的脚本生成):
INSERT INTO color (id, rgb_r, rgb_g, rgb_b, lab_l, lab_a, lab_b)
VALUES (902, 164, 214, 189, 81.6521019943304793,
-21.2561872439361323, 7.08354581694699004);
INSERT INTO color (id, rgb_r, rgb_g, rgb_b, lab_l, lab_a, lab_b)
VALUES (903, 113, 229, 64, 81.7930860963098212,
-60.5865728472875205, 66.4022741184551819);
INSERT INTO color (id, rgb_r, rgb_g, rgb_b, lab_l, lab_a, lab_b)
VALUES (904, 65, 86, 78, 34.6593864327796624,
-9.95482220634028003, 2.02661293272071719);
...
这是我正在使用的 Delta E CIE 2000 SQL 函数:
CREATE OR REPLACE FUNCTION
DELTA_E_CIE2000(double precision, double precision,
double precision, double precision,
double precision, double precision,
double precision, double precision,
double precision)
RETURNS double precision
AS $$
WITH
c AS (SELECT
(CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR))
AS lab_pair_str,
(( + ) /
2.0)
AS avg_lp,
SQRT(
POW(, 2.0) +
POW(, 2.0))
AS c1,
SQRT(
POW((), 2.0) +
POW((), 2.0))
AS c2),
gs AS (SELECT
c.lab_pair_str,
(0.5 *
(1.0 - SQRT(
POW(((c.c1 + c.c2) / 2.0), 7.0) / (
POW(((c.c1 + c.c2) / 2.0), 7.0) +
POW(25.0, 7.0)))))
AS g
FROM c
WHERE c.lab_pair_str = (
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR))),
ap AS (SELECT
gs.lab_pair_str,
((1.0 + gs.g) * )
AS a1p,
((1.0 + gs.g) * )
AS a2p
FROM gs
WHERE gs.lab_pair_str = (
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR))),
cphp AS (SELECT
ap.lab_pair_str,
SQRT(
POW(ap.a1p, 2.0) +
POW(, 2.0))
AS c1p,
SQRT(
POW(ap.a2p, 2.0) +
POW(, 2.0))
AS c2p,
(
DEGREES(ATAN2(, ap.a1p)) + (
CASE
WHEN DEGREES(ATAN2(, ap.a1p)) < 0.0
THEN 360.0
ELSE 0.0
END))
AS h1p,
(
DEGREES(ATAN2(, ap.a2p)) + (
CASE
WHEN DEGREES(ATAN2(, ap.a2p)) < 0.0
THEN 360.0
ELSE 0.0
END))
AS h2p
FROM ap
WHERE ap.lab_pair_str = (
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR))),
av AS (SELECT
cphp.lab_pair_str,
((cphp.c1p + cphp.c2p) /
2.0)
AS avg_c1p_c2p,
(((CASE
WHEN (ABS(cphp.h1p - cphp.h2p) > 180.0)
THEN 360.0
ELSE 0.0
END) +
cphp.h1p +
cphp.h2p) /
2.0)
AS avg_hp
FROM cphp
WHERE cphp.lab_pair_str = (
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR))),
ts AS (SELECT
av.lab_pair_str,
(1.0 -
0.17 * COS(RADIANS(av.avg_hp - 30.0)) +
0.24 * COS(RADIANS(2.0 * av.avg_hp)) +
0.32 * COS(RADIANS(3.0 * av.avg_hp + 6.0)) -
0.2 * COS(RADIANS(4.0 * av.avg_hp - 63.0)))
AS t,
((
(cphp.h2p - cphp.h1p) +
(CASE
WHEN (ABS(cphp.h2p - cphp.h1p) > 180.0)
THEN 360.0
ELSE 0.0
END))
-
(CASE
WHEN (cphp.h2p > cphp.h1p)
THEN 720.0
ELSE 0.0
END))
AS delta_hlp
FROM av
INNER JOIN cphp
ON av.lab_pair_str = cphp.lab_pair_str
WHERE av.lab_pair_str = (
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR))),
d AS (SELECT
ts.lab_pair_str,
( - )
AS delta_lp,
(cphp.c2p - cphp.c1p)
AS delta_cp,
(2.0 * (
SQRT(cphp.c2p * cphp.c1p) *
SIN(RADIANS(ts.delta_hlp) / 2.0)))
AS delta_hp,
(1.0 + (
(0.015 * POW(c.avg_lp - 50.0, 2.0)) /
SQRT(20.0 + POW(c.avg_lp - 50.0, 2.0))))
AS s_l,
(1.0 + 0.045 * av.avg_c1p_c2p)
AS s_c,
(1.0 + 0.015 * av.avg_c1p_c2p * ts.t)
AS s_h,
(30.0 * EXP(-(POW(((av.avg_hp - 275.0) / 25.0), 2.0))))
AS delta_ro,
SQRT(
(POW(av.avg_c1p_c2p, 7.0)) /
(POW(av.avg_c1p_c2p, 7.0) + POW(25.0, 7.0)))
AS r_c
FROM ts
INNER JOIN cphp
ON ts.lab_pair_str = cphp.lab_pair_str
INNER JOIN c
ON ts.lab_pair_str = c.lab_pair_str
INNER JOIN av
ON ts.lab_pair_str = av.lab_pair_str
WHERE ts.lab_pair_str = (
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR))),
r AS (SELECT
d.lab_pair_str,
(-2.0 * d.r_c * SIN(2.0 * RADIANS(d.delta_ro)))
AS r_t
FROM d
WHERE d.lab_pair_str = (
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR)))
SELECT
SQRT(
POW(d.delta_lp / (d.s_l * ), 2.0) +
POW(d.delta_cp / (d.s_c * ), 2.0) +
POW(d.delta_hp / (d.s_h * ), 2.0) +
r.r_t *
(d.delta_cp / (d.s_c * )) *
(d.delta_hp / (d.s_h * )))
AS delta_e_cie2000
FROM r
INNER JOIN d
ON r.lab_pair_str = d.lab_pair_str
WHERE r.lab_pair_str = (
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR) || ',' ||
CAST( AS VARCHAR))
$$
LANGUAGE SQL
IMMUTABLE
RETURNS NULL ON NULL INPUT;
(我最初使用大约 10 层深的嵌套子查询编写此函数,但随后我将其重新编写为使用 WITH
语句,即 Postgres CTE。新版本更具可读性和性能和老版本差不多,可以看both versions in the code.)
定义函数后,我在这样的查询中使用它:
SELECT c.rgb_r,
c.rgb_g,
c.rgb_b,
DELTA_E_CIE2000(73.9206633504, -50.2996953437,
23.8259166281,
c.lab_l, c.lab_a, c.lab_b,
1.0, 1.0, 1.0)
AS de2000
FROM color c
ORDER BY de2000
LIMIT 100;
所以,我的问题是:有什么方法可以提高 DELTA_E_CIE2000
函数的性能,使其可实时用于非平凡数据集?或者,考虑到公式的复杂性,它会尽可能快吗?
根据我在我的演示应用程序中所做的测试,我会说对于在网站上进行简单 "similar colors" 搜索的用例,1976 和 1976 之间的结果准确性差异2000的功能其实可以忽略不计。也就是说,我已经确信 1976 年的公式符合我的需要 "good enough"。然而,2000 函数 return 的结果稍微好一些(很大程度上取决于输入颜色在 Lab space 中的位置),实际上,我只是好奇它是否可以加速进一步。
两件事:1) 您没有充分利用数据库,2) 您的问题是自定义 PostgreSQL 扩展的一个很好的例子。原因如下。
您仅使用数据库作为存储,将颜色存储为浮点数。在您当前的配置中,无论查询类型如何,数据库将始终必须检查所有值(进行顺序扫描)。这意味着大量的 IO 和为少数返回的匹配项进行的大量计算。您正试图找到最接近的 N 种颜色,因此有几种方法可以避免对所有数据执行计算。
简单的改进
最简单的方法是将您的计算限制在较小的数据子集中。如果组件差异更大,您可以假设差异会更大。如果您可以找到组件之间的安全差异(结果总是不合适),则可以使用带 btree 索引的范围 WHERE 完全排除这些颜色。但是,由于 L*a*b 颜色的性质space,这可能会使您的结果变差。
首先创建索引:
CREATE INDEX color_lab_l_btree ON color USING btree (lab_l);
CREATE INDEX color_lab_a_btree ON color USING btree (lab_a);
CREATE INDEX color_lab_b_btree ON color USING btree (lab_b);
然后我调整了您的查询以包含一个 WHERE 子句以仅过滤颜色,其中任何组件最多有 20 个不同。
更新: 再看一遍,加个limit 20很可能会使结果变差,因为我在space中至少发现了一个点,为此成立。:
SELECT
c.rgb_r, c.rgb_g, c.rgb_b,
DELTA_E_CIE2000(
25.805780252087963, 53.33446637366859, -45.03961353720049,
c.lab_l, c.lab_a, c.lab_b,
1.0, 1.0, 1.0) AS de2000
FROM color c
WHERE
c.lab_l BETWEEN 25.805780252087963 - 20 AND 25.805780252087963 + 20
AND c.lab_a BETWEEN 53.33446637366859 - 20 AND 53.33446637366859 + 20
AND c.lab_b BETWEEN -45.03961353720049 - 20 AND -45.03961353720049 + 20
ORDER BY de2000 ;
我用你的脚本填充了 table 100000 种随机颜色并进行了测试:
没有索引的时间:44006,851 毫秒
索引和范围查询时间:1293,092 毫秒
您也可以将此 WHERE 子句添加到 delta_e_cie1976_query
,在我的随机数据上,它将查询时间从 ~110 毫秒减少到 ~22 毫秒。
顺便说一句:根据经验,我得到了 20:我尝试了 10,但只得到了 380 条记录,这似乎有点低,并且可能会排除一些更好的选择,因为限制是 100。20 的完整集是 2900 行并且可以相当确定最接近的比赛将在那里。我没有详细研究 DELTA_E_CIE2000 或 L*a*b* 颜色 space 因此阈值可能需要根据不同的组件进行调整才能真正实现,但排除不感兴趣的原则数据保持。
用 C 重写 Delta E CIE 2000
正如您已经说过的,Delta E CIE 2000 很复杂并且相当不适合 table 在 SQL 中实施。它目前在我的笔记本电脑上每次通话使用大约 0.4 毫秒。在 C 中实现它应该会大大加快速度。 PostgreSQL 将默认成本分配给 SQL 函数为 100,C 函数为 1。我猜这是基于实际经验。
更新: 因为这也解决了我的一个问题,我重新实现了 C 中 colormath 模块的 Delta E 函数作为 PostgreSQL 扩展,可在 PGXN。有了这个,当从 table 中查询具有 100k 条记录的所有记录时,我可以看到 CIE2000 的加速大约为 150 倍。
使用此 C 函数,对于 10 万种颜色,我得到的查询时间在 147 毫秒到 160 毫秒之间。有了额外的 WHERE,查询时间大约是 20 毫秒,这对我来说似乎很容易接受table。
最佳但先进的解决方案
但是,由于您的问题是 3 维中的 N 个最近邻搜索 space,您可以使用 PostgreSQL since version 9.1 中的 K-最近邻索引。
为此,您需要将 L*a*b* 组件放入 cube. This extension does not yet support distance operator (it's in the works),但即使可以,它也不支持 Delta E 距离,您需要重新实现它作为 C 扩展。
这意味着实现 GiST 索引运算符 class(btree_gist PostgreSQL extension in contrib does this) to support indexing according to Delta E distances. The good part is you could then use different operators for different versions of Delta E, eg. <->
for Delta E CIE 2000 and <#>
for Delta E CIE 1976 and queries would be really really fast 用于小 LIMIT,即使使用 Delta E CIE 2000。
最终可能取决于您的(业务)要求和限制。