Delta E (CIE Lab) 在 SQL 中计算和排序的性能

Posted

技术标签:

【中文标题】Delta E (CIE Lab) 在 SQL 中计算和排序的性能【英文标题】:Performance of Delta E (CIE Lab) calculating and sorting in SQL 【发布时间】:2015-08-04 00:31:12 【问题描述】:

我有一个数据库表,其中每一行都是一种颜色。我的目标:给定输入颜色,计算其与 DB 表中每种颜色的距离,并按该距离对结果进行排序。或者,作为用户故事陈述:当我选择一种颜色时,我希望看到与我选择的颜色最相似的颜色列表,最接近的匹配位于列表顶部。

我了解,为了做到这一点,各种Delta E(CIE 实验室)公式are the best choice。我找不到公式的任何本机 SQL 实现,所以我编写了自己的 SQL 版本的 Delta E CIE 1976 和 Delta E CIE 2000。我根据python-colormath 实现生成的结果验证了公式的 SQL 版本的准确性。

1976 年的公式很容易用 SQL 或任何其他语言编写,因为它是一个简单的欧几里得距离计算。它在任何大小的数据集上对我来说都执行得又快又好(在有 100,000 行的颜色表上对其进行了测试,查询时间不到 1 秒)。

相比之下,2000 年的公式非常冗长且复杂。我设法用SQL实现了它,但它的性能不是很好:查询10,000行大约5秒,查询100,000行大约1分钟。

我写了一个 example app called colorsearchtest(在 Python / Flask / Postgres 中)来玩弄我的实现(我是 set up a demo on Heroku)。如果您试用这个应用程序,您可以清楚地看到 1976 年和 2000 年 Delta E 查询之间的性能差异。

这是颜色表的架构(对于每种颜色,它将各自的 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
);

这是表格中的一些数据(所有只是随机颜色,由我的应用程序中的脚本生成):

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($1 AS VARCHAR) || ',' ||
            CAST($2 AS VARCHAR) || ',' ||
            CAST($3 AS VARCHAR) || ',' ||
            CAST($4 AS VARCHAR) || ',' ||
            CAST($5 AS VARCHAR) || ',' ||
            CAST($6 AS VARCHAR))
        AS lab_pair_str,
            (($1 + $4) /
                2.0)
        AS avg_lp,
            SQRT(
                POW($2, 2.0) +
                POW($3, 2.0))
        AS c1,
            SQRT(
                POW(($5), 2.0) +
                POW(($6), 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($1 AS VARCHAR) || ',' ||
            CAST($2 AS VARCHAR) || ',' ||
            CAST($3 AS VARCHAR) || ',' ||
            CAST($4 AS VARCHAR) || ',' ||
            CAST($5 AS VARCHAR) || ',' ||
            CAST($6 AS VARCHAR))),
    ap AS (SELECT
            gs.lab_pair_str,
            ((1.0 + gs.g) * $2)
        AS a1p,
            ((1.0 + gs.g) * $5)
        AS a2p
        FROM gs
        WHERE gs.lab_pair_str = (
            CAST($1 AS VARCHAR) || ',' ||
            CAST($2 AS VARCHAR) || ',' ||
            CAST($3 AS VARCHAR) || ',' ||
            CAST($4 AS VARCHAR) || ',' ||
            CAST($5 AS VARCHAR) || ',' ||
            CAST($6 AS VARCHAR))),
    cphp AS (SELECT
            ap.lab_pair_str,
            SQRT(
                POW(ap.a1p, 2.0) +
                POW($3, 2.0))
        AS c1p,
            SQRT(
                POW(ap.a2p, 2.0) +
                POW($6, 2.0))
        AS c2p,
            (
                DEGREES(ATAN2($3, ap.a1p)) + (
                    CASE
                        WHEN DEGREES(ATAN2($3, ap.a1p)) < 0.0
                        THEN 360.0
                        ELSE 0.0
                        END))
        AS h1p,
            (
                DEGREES(ATAN2($6, ap.a2p)) + (
                    CASE
                        WHEN DEGREES(ATAN2($6, ap.a2p)) < 0.0
                        THEN 360.0
                        ELSE 0.0
                        END))
        AS h2p
        FROM ap
        WHERE ap.lab_pair_str = (
            CAST($1 AS VARCHAR) || ',' ||
            CAST($2 AS VARCHAR) || ',' ||
            CAST($3 AS VARCHAR) || ',' ||
            CAST($4 AS VARCHAR) || ',' ||
            CAST($5 AS VARCHAR) || ',' ||
            CAST($6 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($1 AS VARCHAR) || ',' ||
            CAST($2 AS VARCHAR) || ',' ||
            CAST($3 AS VARCHAR) || ',' ||
            CAST($4 AS VARCHAR) || ',' ||
            CAST($5 AS VARCHAR) || ',' ||
            CAST($6 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($1 AS VARCHAR) || ',' ||
            CAST($2 AS VARCHAR) || ',' ||
            CAST($3 AS VARCHAR) || ',' ||
            CAST($4 AS VARCHAR) || ',' ||
            CAST($5 AS VARCHAR) || ',' ||
            CAST($6 AS VARCHAR))),
    d AS (SELECT
            ts.lab_pair_str,
            ($4 - $1)
        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($1 AS VARCHAR) || ',' ||
            CAST($2 AS VARCHAR) || ',' ||
            CAST($3 AS VARCHAR) || ',' ||
            CAST($4 AS VARCHAR) || ',' ||
            CAST($5 AS VARCHAR) || ',' ||
            CAST($6 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($1 AS VARCHAR) || ',' ||
            CAST($2 AS VARCHAR) || ',' ||
            CAST($3 AS VARCHAR) || ',' ||
            CAST($4 AS VARCHAR) || ',' ||
            CAST($5 AS VARCHAR) || ',' ||
            CAST($6 AS VARCHAR)))
SELECT
        SQRT(
            POW(d.delta_lp / (d.s_l * $7), 2.0) +
            POW(d.delta_cp / (d.s_c * $8), 2.0) +
            POW(d.delta_hp / (d.s_h * $9), 2.0) +
            r.r_t *
            (d.delta_cp / (d.s_c * $8)) *
            (d.delta_hp / (d.s_h * $9)))
    AS delta_e_cie2000
FROM          r
INNER JOIN    d
ON            r.lab_pair_str = d.lab_pair_str
WHERE         r.lab_pair_str = (
          CAST($1 AS VARCHAR) || ',' ||
          CAST($2 AS VARCHAR) || ',' ||
          CAST($3 AS VARCHAR) || ',' ||
          CAST($4 AS VARCHAR) || ',' ||
          CAST($5 AS VARCHAR) || ',' ||
          CAST($6 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 函数的性能,使其可实时用于非平凡数据集?或者,考虑到公式的复杂性,它会尽快达到吗?

根据我在演示应用中所做的测试,我想说,对于在网站上进行简单“相似颜色”搜索的用例,1976 年和 2000 年函数之间的结果准确性差异是实际上可以忽略不计。也就是说,我已经确信,对于我的需要,1976 年的公式“足够好”。但是,2000 函数确实返回了稍微好一点的结果(很大程度上取决于输入颜色在 Lab 空间中的位置),实际上,我只是好奇它是否可以进一步加速。

【问题讨论】:

在 Python / javascript 中执行计算并将结果发送回数据库不是更快吗? %timeit colour.delta_E_CIE2000(np.random.rand(100000, 3), np.random.rand(100000, 3)) 10 次循环,最好的 3 次:每个循环 75.8 毫秒 确实会@kel-solaar。我理解需要在数据库中保存颜色并查询它们的接近度(我发现这篇文章正是在寻找那个),但我搜索得越多,似乎使用 python 的颜色越多,矩阵和向量是一个更可取的选择在性能方面。 做了一些基准测试,我得到了大约 10,000 行:DE (Delta E) 2000 (py-colormath): 4s; DE 2000 (DB):6s; DE 1976(py-colormath):0.7s; DE 1976 (DB):0.01 秒。约 100,000 行:DE 2000(py-colormath):39s; DE 2000 (DB):56s; DE 1976(py-colormath):8s; DE 1976 (DB):0.07 秒。还更新了 heroku 上的应用程序以显示 py-colormath 结果和时间。因此,对于普通或非普通大小的数据集,DE 2000 的 Python 或 DB impl 都明显慢。对于琐碎的数据集,DE 1976 impl 都可以;对于非平凡数据集,DE 1976 的 DB impl 是唯一快速的选择。 所以,@LinoSilva - 基于这些统计数据,我不得不不同意:对于约 10,000 个数据集,使用 python/矩阵/向量的性能与使用 SQL 大致相同(对于 DE 2000 或 DE 1976);对于约 100,000 的数据集,使用 python / 矩阵 / 向量的性能与 DE 2000 的 SQL 相当,它的性能比 DE 1976 的 SQL 差得多。可能理想的高性能和高可扩展性解决方案将涉及使用SQL 中原生的矩阵/向量。但不确定这在 Postgres 或任何其他主要 RDBMS 中是否可行。有知道这方面的人吗? 很抱歉,这与我得到的数字相差甚远! DE2000 在具有精确 197525 个条目的数据集中使用 colormath 查找相似颜色的时间不超过 200 毫秒。将尝试在 SQL 上使用相同的数据集运行您的数据库查询,您让我很好奇! :D 【参考方案1】:

两件事:1)您没有充分使用数据库,2)您的问题是自定义 PostgreSQL 扩展的一个很好的例子。原因如下。

您仅将数据库用作存储,将颜色存储为浮点数。在您当前的配置中,无论查询类型如何,数据库都必须检查所有值(进行顺序扫描)。这意味着大量的 IO 和针对少数返回匹配项的大量计算。您正在尝试找到最接近的 N 种颜色,因此有几种可能性可以避免对所有数据执行计算。

简单改进

最简单的方法是将计算限制在较小的数据子集上。如果组件差异更大,您可以假设差异会更大。如果您可以找到组件之间的安全差异,结果总是不合适的,您可以使用带 btree 索引的 ranged WHERE 完全排除这些颜色。但是,由于 L*a*b 颜色空间的性质,这可能会使您的结果变差。

首先创建索引:

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 个。

更新:再看一遍,添加 20 的限制很可能会使结果变差,因为我在空间中发现了至少一个点,这是正确的。:

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 ;

我用你的脚本在表格中填充了 100000 种随机颜色并进行了测试:

没有索引的时间:44006,851 毫秒

索引和范围查询时间:1293,092 毫秒

您也可以将此 WHERE 子句添加到 delta_e_cie1976_query,在我的随机数据上,它会将查询时间从 ~110 毫秒降至 ~22 毫秒。

顺便说一句:我凭经验得到了 20 号:我尝试了 10 条,但只得到了 380 条记录,这似乎有点低,可能会排除一些更好的选择,因为限制是 100 条。使用 20 条时,全套是 2900 行和 1 行可以相当肯定最接近的匹配将在那里。我没有详细研究 DELTA_E_CIE2000 或 L*a*b* 颜色空间,因此阈值可能需要沿着不同的组件进行调整才能真正做到这一点,但排除不感兴趣的数据的原则是成立的。

用 C 重写 Delta E CIE 2000

正如您已经说过的,Delta E CIE 2000 很复杂,而且相当不适合在 SQL 中实现。它目前在我的笔记本电脑上每次通话使用大约 0.4 毫秒。在 C 中实现它应该会大大加快速度。 PostgreSQL 将 SQL 函数的默认成本分配为 100,将 C 函数分配为 1。我猜这是基于实际经验。

更新:因为这也引起了我的困扰,我将 C 中 colormath 模块中的 Delta E 函数重新实现为 PostgreSQL 扩展,可在 PGXN 上找到。有了这个,我可以看到 CIE2000 在查询具有 100k 条记录的表中的所有记录时,速度提高了大约 150 倍。

使用这个 C 函数,我得到 100k 颜色的查询时间在 147 毫秒到 160 毫秒之间。加上额外的 WHERE,查询时间约为 20 毫秒,这对我来说似乎是可以接受的。

最好但先进的解决方案

但是,由于您的问题是 3 维空间中的 N 个最近邻搜索,您可以使用 PostgreSQL since version 9.1 中的 K-Nearest-Neighbor Indexing。

为此,您需要将 L*a*b* 组件放入 cube。此扩展尚不支持距离运算符 (it's in the works),但即使支持,它也不支持 Delta E 距离,您需要将其重新实现为 C 扩展。

这意味着实现 GiST 索引运算符类(btree_gist PostgreSQL extension 在 contrib 中执行此操作)以支持根据 Delta E 距离进行索引。好的部分是您可以为不同版本的 Delta E 使用不同的运算符,例如。 &lt;-&gt; 用于 Delta E CIE 2000,&lt;#&gt; 用于 Delta E CIE 1976,即使使用 Delta E CIE 2000,查询也将是 really really fast 用于小 LIMIT。

最终,它可能取决于您的(业务)要求和限制。

【讨论】:

感谢@hruske 的出色回答,这远远超出了我的预期!我更新了我的“colorsearchtest”应用程序以在 Delta E 2000 查询中包含 BETWEEN 子句,lab 值的阈值为 += 20。但是,我省略了lab 列上的索引,因为根据我的测试,这对查询时间没有影响。 回复:您的 PGXN“颜色”扩展。很棒的工作——如果有一天我真的需要在生产应用程序中进行 Delta E 查询,我肯定会使用它。我没有使用 Postgres 自定义扩展的经验(而且我的 C 编程也很生疏),所以我自己编写对我来说非常具有挑战性。 Re:使用自定义 GiST 索引将 Delta E 查询实现为 K-Nearest-Neighbor 搜索。是的,那会很酷,但我认为实现起来会很复杂,特别是因为L*a*b* 不是一个简单形状的颜色空间(即不容易表示为立方体、球体、圆柱体等,与大多数其他颜色空间),它是一个大致圆锥形,模拟人眼的实际感知/视觉处理 - 请参阅 ccm.net/contents/724-cie-lab-l-a-b-coding 的图表。 回复:自定义 GiST 索引(续)。因此,在L*a*b* 色彩空间中进行空间索引之类的操作必然是一个非常复杂的过程。我想这有点类似于地图投影所需的东西。但是,是的,我想如果做得好,这将是执行 Delta E 查询的最终方式。 首先 - 在没有先检查的情况下不要使用中间值,这不会使您的结果恶化。运行另一次检查后,似乎 +/- 20 是一个非常糟糕的选择,因为 Delta E 可能明显小于组件之间的差异,尤其是对于 a 和 b。

以上是关于Delta E (CIE Lab) 在 SQL 中计算和排序的性能的主要内容,如果未能解决你的问题,请参考以下文章

色差仪中的lab分别是啥意思

色彩管理Lab色彩模式详解

Lab空间中不同距离函数的Kmeans聚类

[转]Lab颜色空间

Lab颜色空间

MongoDB为Lombard Odier&Cie“E-MERGING”社交网络提供灵活的数据存储