整理毛笔:使用JS通过相邻项的相似性来优化2D网格中物品的排列方式 [更新]

37

更新:问题已经附带具体细节和代码,请见下文。

警告:此问题是关于优化矩阵中项目排列的。它不是关于比较颜色的。最初,我决定提供有关我的问题的背景会有所帮助。现在我后悔了这个决定,因为结果是相反的:太多无关的谈论颜色,几乎没有关于实际算法的内容。


我为孩子买了一盒80支毛笔,但让我很烦恼的是它们没有分类。

enter image description here

我曾经在Android上玩过一个名为Blendoku的游戏,你需要做的就是将颜色排列成一种渐变的方式,使得相邻的颜色最为相似:

enter image description here

使用这些草图标记,将颜色组织在交叉线上就像填字游戏一样容易有趣。但是,颜色不是从均匀的渐变中提取的,这使我无法通过直觉来对毛笔进行分类。我需要用算法来解决它!
这就是我拥有的:
- 熟练掌握JavaScript - 所有笔的颜色值的平坦数组 - 一个function distance(color1, color2),显示颜色对的相似程度。它返回一个介于0100之间的浮点数,其中0表示颜色相同。
我所缺少的只是一个算法。
80的阶乘是一个有118位数字的数字,这排除了蛮力破解。
可能有方法使蛮力破解成为可行的:
- 固定几支笔的位置(例如在角落里)以减少可能的组合数量; - 放弃包含至少一个非常不相似邻居对的分支; - 在找到第一个令人满意的安排后停止。
但是,我仍然缺少实际的算法,更不用说非暴力的算法了。
PS作业:

更新

目标

以这样一种方式将预定义的80种颜色排列在8×10网格中,使得颜色形成漂亮的渐变而不会出现撕裂。

由于下文所述原因,此问题没有明确的解决方案,可能的解决方案容易产生不完美的结果和主观性。这是可以预期的。

请注意,我已经有一个比较两种颜色并告诉它们相似程度的函数。

颜色空间是三维的

人眼有三种类型的感受器来区分颜色。人类的颜色空间是三维的(三色)。

描述颜色的不同模型都是三维的:RGB、HSL、HSV、XYZ、LAB、CMY(请注意,CMYK中的“K”仅需要是因为有色墨水不完全不透明且昂贵)。

例如,这个调色板:

HS palette

这个调色板使用极坐标,色相在角度上表示,饱和度在半径上表示。没有第三个维度(亮度),这个调色板缺少所有明亮和暗淡的颜色:白色、黑色、所有灰色(除了中心的50%灰色)和着色灰色。

这个调色板只是HSL/HSV色彩空间的一个薄片:

enter image description here

不可能在渐变中将所有颜色在2D网格上布置而不出现撕裂

例如,这里列举了所有32位RGB颜色,按字典序排列成2D网格。你可以看到渐变有很多撕裂:

flat RGB palette

因此,我的目标是找到一个任意的“足够好”的排列方式,使邻居之间更相似。我宁愿牺牲一点相似性,也不想有几个非常相似的聚类之间出现分裂。
这个问题是关于在JavaScript中优化网格,而不是比较颜色!
我已经选择了一个函数来确定颜色的相似性:Delta E 2000。该函数专门设计用于反映人类对颜色相似度的主观感知。这里有一篇whitepaper描述它的工作原理。
这个问题是关于优化二维网格中每对相邻项(垂直和水平)的相似度尽可能低的排列方式。
“优化”一词并不是指让算法运行得更快。它是指Mathematical optimization的意义:
在最简单的情况下,optimization problem由通过系统地选择允许集合内的输入值并计算函数值来最大化或最小化实数函数组成。
在我的情况下:
这里的“函数”是运行DeltaE.getDeltaE00(color1, color2)函数以计算所有相邻项的输出,输出是一堆数字(大约142个数字),反映出所有相邻对之间的不相似程度。
“最大化或最小化”- 目标是将“函数”的输出最小化。
“输入值”- 是在8×10网格中80个预定义项的特定排列。共有80!个输入值,这使得在家用电脑上无法通过暴力破解来完成任务。
请注意,我没有明确定义“函数”的最小化标准。如果我们只使用所有数字的最小总和,那么获胜的结果可能是总和最低的情况,但是有些相邻的项对非常不相似。
因此,“函数”应该考虑不仅所有比较的总和,而且还要确保没有比较偏离得太远。
解决问题的可能路径:
从我之前在这个问题上尝试的赏金中,我学到了以下路径:
遗传算法
优化器/求解器库
带有某种算法帮助的手动排序
其他东西?
优化器/求解器库的解决方案是我最初希望的。但是成熟的库,如CPLEX和Gurobi不支持JS。虽然有一些JS库,但它们没有很好的文档,并且没有新手教程。
遗传算法方法非常令人兴奋。但它需要构思突变和交配样本(网格排列)的算法。突变似乎很琐碎:只需交换相邻项即可。但我对交配一无所知。而且我对整个过程的理解很少。
手动排序建议乍一看很有前途,但深入研究后不足为奇。它们还假设使用算法来解决某些步骤,却没有提供实际算法。
代码样板和颜色样本
我已经准备了JS代码样板:https://codepen.io/lolmaus/pen/oNxGmqz?editors=0010 注意:代码运行需要一段时间。为了更轻松地处理它,请执行以下操作:
  • 登录/注册CodePen以便能够fork模板。
  • Fork模板。
  • 前往设置/行为并确保自动更新已禁用。
  • 调整窗格大小,最大化JS窗格并最小化其他窗格。
  • 前往查看更改/调试模式以在单独的选项卡中打开结果。这将启用console.log()。此外,如果代码执行停止,您可以关闭渲染选项卡而不失去访问编码选项卡的权利。
  • 在对代码进行更改后,在代码选项卡中点击保存,然后刷新渲染选项卡并等待。
  • 为了包含JS库,请前往设置/JS。我使用这个CDN来链接到来自GitHub的代码:https://www.jsdelivr.com/?docs=gh

源数据:

const data = [
  {index: 1, id: "1", name: "Wine Red", rgb: "#A35A6E"},
  {index: 2, id: "3", name: "Rose Red", rgb: "#F3595F"},
  {index: 3, id: "4", name: "Vivid Red", rgb: "#F4565F"},
  // ...
];

Index是基于1的颜色编号,按照它们在盒子中出现的顺序排序,但在代码中未使用。

Id是笔制造商颜色的编号。由于某些编号以WG3形式出现,因此它们是字符串。


颜色类。

该类提供了一些抽象方法来处理单个颜色。它使得比较给定颜色和另一个颜色变得容易。

  index;
  id;
  name;
  rgbStr;
  collection;
  
  constructor({index, id, name, rgb}, collection) {
    this.index = index;
    this.id = id;
    this.name = name;
    this.rgbStr = rgb;
    this.collection = collection;
  }
  
  // Representation of RGB color stirng in a format consumable by the `rgb2lab` function
  @memoized
  get rgbArr() {
    return [
      parseInt(this.rgbStr.slice(1,3), 16),
      parseInt(this.rgbStr.slice(3,5), 16),
      parseInt(this.rgbStr.slice(5,7), 16)
    ];
  }
  
  // LAB value of the color in a format consumable by the DeltaE function
  @memoized
  get labObj() {
    const [L, A, B] = rgb2lab(this.rgbArr);
    return {L, A, B};
  }

  // object where distances from current color to all other colors are calculated
  // {id: {distance, color}}
  @memoized
  get distancesObj() {
    return this.collection.colors.reduce((result, color) => {
      if (color !== this) {      
        result[color.id] = {
          distance: this.compare(color),
          color,
        };
      }
      
      return result;
    }, {});
  }
    
  // array of distances from current color to all other colors
  // [{distance, color}]
  @memoized
  get distancesArr() {
    return Object.values(this.distancesObj);
  }
  
  // Number reprtesenting sum of distances from this color to all other colors
  @memoized
  get totalDistance() {
    return this.distancesArr.reduce((result, {distance}) => {      
      return result + distance;
    }, 0); 
  }

  // Accepts another color instance. Returns a number indicating distance between two numbers.
  // Lower number means more similarity.
  compare(color) {
    return DeltaE.getDeltaE00(this.labObj, color.labObj);
  }
}

集合:一个用于存储所有颜色并对其进行排序的类。

class Collection {
  // Source data goes here. Do not mutate after setting in the constructor!
  data;
  
  constructor(data) {
    this.data = data;
  }
  
  // Instantiates all colors
  @memoized
  get colors() {
    const colors = [];

    data.forEach((datum) => {
      const color = new Color(datum, this);
      colors.push(color);
    });
  
    return colors;    
  }

  // Copy of the colors array, sorted by total distance
  @memoized
  get colorsSortedByTotalDistance() {
    return this.colors.slice().sort((a, b) => a.totalDistance - b.totalDistance);
  }

  // Copy of the colors array, arranged by similarity of adjacent items
  @memoized
  get colorsLinear() {
    // Create copy of colors array to manipualte with
    const colors = this.colors.slice();
    
    // Pick starting color
    const startingColor = colors.find((color) => color.id === "138");
    
    // Remove starting color
    const startingColorIndex = colors.indexOf(startingColor);
    colors.splice(startingColorIndex, 1);
    
    // Start populating ordered array
    const result = [startingColor];
    
    let i = 0;
    
    while (colors.length) {
      
      if (i >= 81) throw new Error('Too many iterations');

      const color = result[result.length - 1];
      colors.sort((a, b) => a.distancesObj[color.id].distance - b.distancesObj[color.id].distance);
      
      const nextColor = colors.shift();
      result.push(nextColor);
    }
    
    return result;
  }

  // Accepts name of a property containing a flat array of colors.
  // Renders those colors into HTML. CSS makes color wrap into 8 rows, with 10 colors in every row.
  render(propertyName) {
    const html =
      this[propertyName]
        .map((color) => {
          return `
          <div
            class="color"
            style="--color: ${color.rgbStr};"
            title="${color.name}\n${color.rgbStr}"
          >
            <span class="color-name">
              ${color.id}
            </span>
          </div>
          `;
        })
        .join("\n\n");
    
    document.querySelector('#box').innerHTML = html;
    document.querySelector('#title').innerHTML = propertyName;
  }
}

使用方法:

const collection = new Collection(data);

console.log(collection);

collection.render("colorsLinear"); // Implement your own getter on Collection and use its name here

样例输出:

enter image description here


3
我认为在设计算法时,有一个明确的目标会有很大帮助。最终结果具有哪些特性?我能想到的一个可能性是相邻单元格之间的颜色距离总和应该最小化。这是否符合您的直觉? - SaiBot
3
如其他问题所指出的那样,对于一般度量来说,这是一个 NP 难问题。因为你关心的是特定实例,获取该实例比试图推理特定启发式算法的效果要有帮助得多(即使对于经验丰富的算法设计人员来说,我也觉得这很困难)。 - David Eisenstat
1
从80种毛毡笔中选取距离最远的4种颜色进行采样,然后使用您的距离函数对毛毡笔进行k-means聚类。完成后,从每个生成的4个聚类中选择4种颜色,并创建每个聚类5支毛毡笔的另外4个聚类。最后,对于每个聚类中的5支毛毡笔,生成一个“混合”颜色和一个比较器来比较这些混合颜色,按照此排序聚类,然后转置到矩阵上。 - Thomas Cook
2
我尝试使用OR-Tools进行了一个直接的CP-SAT公式,但相对于我期望在相同时间内进行本地搜索所能达到的结果,它并没有找到特别好的解决方案。 - David Eisenstat
3
通过实施简单的2-opt局部搜索,确认之前的评论:https://imgur.com/a/J2nbZ6L。如果找不到更好的内容,我会把它写出来。 - David Eisenstat
显示剩余10条评论
6个回答

10
我将几个想法结合在一起,找到了目标值为1861.54的解决方案。
1. 通过查找最小成本匹配并连接匹配的子簇,在重复三次的过程中形成大小为8的无序颜色簇。我们使用d(C1,C2)=∑c1 in C1c2 in C2 d(c1,c2)作为子簇C1和C2的距离函数。 2. 根据上述距离函数查找最佳的2×5簇布局。这涉及暴力枚举10!排列(如果利用对称性,则实际上是10!/4,但我没有费心去做)。 3. 单独考虑每个簇,通过暴力枚举8!个排列来找到最佳的4×2布局。(可以进行更多的对称性打破,但我没有费心。) 4. 暴力尝试翻转簇的410种可能方式。(可以进行更多对称性的打破,但我没有费心。) 5. 使用本地搜索改进此布局。我交替进行两种类型的轮回:一种是2-opt轮回,其中考虑交换每对位置;另一种是大邻域轮回,我们选择一个随机的最大独立集,并使用匈牙利方法重新分配最优解(当我们尝试移动的所有物品都不能彼此相邻时,这个问题很容易)。
输出如下图所示:

felt tip pen arrangement

Python实现代码位于https://github.com/eisenstatdavid/felt-tip-pens


1
说实话,这对我来说仍然很混乱。我认为目标函数是问题的一部分——我们已经避免了最严格的并置,但梯度仍然混乱。尽管如此,这个特定的目标是OP要求优化的。 - David Eisenstat

8
这个技巧是暂时不把它看作一个数组,而是将自己锚定在角落上。
首先,你需要定义你要解决的问题。普通颜色有三个维度:色调、饱和度和价值(深浅),所以你不能在二维网格上考虑所有三个维度。不过,你可以接近这个目标。
如果你想从白色到黑色和红色到紫色排列,你可以定义距离函数来处理深浅的差异,并将其视为距离,以及色调值的差异(没有扭曲!)。这将为您提供一组适用于四个角的排序颜色。
现在,像这样将每种颜色锚定到四个角落,将(0:0)定义为黑色,(1:1)定义为白色,(0,1)定义为红色(0 色调),(1:0)定义为紫红色(350+ 色调)。就像这样(为了简单起见,我们假设紫红色是紫色):

enter image description here

现在,您有两个极端的度量标准:黑暗和色调。但是等等……如果我们将盒子旋转45度...

enter image description here

你看到了吗?没有?X轴和Y轴已经与我们的两个度量标准对齐了!现在我们所需要做的就是将每种颜色与白色的距离除以黑色与白色的距离,将每种颜色与紫色的距离除以红色与紫色的距离,分别得到我们的Y和X坐标!

让我们再添加一些笔:

enter image description here

现在,使用O(n)^2迭代所有笔,在经过旋转的网格中找到任何一支笔和最终笔位置之间的最短距离。我们可以保留这些距离的映射,如果相应的笔位置已被占用,则替换任何距离。这将使我们能够在多项式时间O(n)^3内将笔插入其最近的位置。

enter image description here

然而,我们还没有完成。HSV是三维的,我们可以并且应该将第三个维度考虑进我们的模型中!为了做到这一点,在计算最接近距离之前,我们通过在模型中引入第三个维度来扩展先前的算法。我们通过将2D平面与两种颜色极端和白色和黑色之间的水平线相交,将其放入3D空间中。这可以通过找到两种颜色极端的中点并稍微调暗来简单地完成。然后,在这个平面上均匀地生成适合我们笔的插槽。我们可以根据它们的HSV值直接将我们的笔放置在这个3D空间中-H代表X,V代表Y,S代表Z。

enter image description here

现在我们有了包括饱和度的笔的3D表示,我们可以再次迭代笔的位置,在多项式时间内找到每个最接近的笔。
好了!笔已经被很好地排序了。如果您想要将结果放入数组中,只需再次均匀生成每个数组索引的坐标并按顺序使用即可!
现在停止排序笔,开始编写代码吧!

注意:这个算法不会完美,特别是当两种颜色具有相同的色调和值,但饱和度不同时。这是将三维向量投影到二维世界中的一个问题。但对于像分类笔这样的事情,它已经足够好了。 - id01
你能解释一下旋转45度是什么意思吗?我不太明白... - ldog
@Idog,如果你旋转45度,X轴将与色调值对齐,Y轴将与暗度值对齐。因此,我们可以使用(相对)色调和暗度作为X/Y值来放置其余的笔。也可以不旋转45度来完成操作;您只需要通过向量乘法将每个色调和暗度值乘以一个向量即可获得笔的X/Y值。旋转使其更简单,可以省去向量乘法。 - id01
这个标准并不新鲜。它从一开始就在我的问题中。如果你不相信,请查看修订历史记录。 - Andrey Mikhaylov - lolmaus
@AndreyMikhaylov-lolmaus 我认为你有能力定义自己的距离函数,而不是使用特定的函数。随着问题变得更加通用,像这样利用所涉及数据结构的巧妙解决方案变得更加困难。 - id01
显示剩余4条评论

5
正如一些评论中指出的那样,您似乎对寻找离散优化问题的全局最小值很感兴趣。如果您对此不太了解,可能需要先阅读相关资料。
想象一下,您有一个误差(目标)函数,它仅是相邻笔之间距离(c1,c2)总和。一个最优解(笔的排列方式)是其误差函数最小的解。可能存在多个最优解。请注意,不同的误差函数可能会给出不同的解,您可能不会对我刚刚介绍的简单误差函数提供的结果满意。
您可以使用现成的优化器(如CPLEX或Gurobi),并只需提供您问题的有效公式。它可能会找到最优解。但是,即使它没有找到最优解,它仍然可能提供一个对您来说非常好的次优解。
你也可以编写自己的启发式算法(例如专门的遗传算法),并获得比求解器在其时间和空间限制内找到的解决方案更好的解决方案。鉴于你的武器似乎是输入数据、测量颜色差异的函数和JavaScript,实现一种启发式算法可能是最熟悉的路径。

我的答案原本没有代码,因为大多数实际问题都没有简单的复制粘贴解决方案。

使用JavaScript进行这种计算很奇怪,在浏览器上进行这种计算更加奇怪。然而,由于作者明确要求,这里是一个托管在CodePen上的简单演化算法的JavaScript实现

由于输入大小比我最初演示该算法时的5x5要大得多,算法需要进行多少代演化以及代码执行速度有多慢,所以需要一段时间才能完成。我更新了突变代码,以防止突变导致解决方案成本被重新计算,但迭代仍然需要相当长的时间。以下解决方案在我的浏览器中通过CodePen的调试模式运行大约需要45分钟。

Result with the specified parameters.

它的目标函数略小于2060,并使用以下参数生成。

const SelectionSize = 100;
const MutationsFromSolution = 50;
const MutationCount = 5;
const MaximumGenerationsWithoutImprovement = 5;

值得指出的是,对参数进行微调可能会对算法的结果产生重大影响。增加变异数或选择大小都将显著增加程序的运行时间,但也可能导致更好的结果。您可以(并且应该)尝试使用不同的参数来寻找更好的解决方案,但它们可能需要更多的计算时间。
在许多情况下,最好的改进来自于算法的改变,而不仅仅是更多的计算能力,因此关于如何执行变异和重组的聪明想法通常是获得更好解决方案的途径,同时仍使用遗传算法。
使用明确种子和可重现的PRNG(而不仅仅是Math.random())非常好,因为这将允许您根据需要重放程序以进行调试和可重现性证明。
您还可以设置算法的可视化(而不仅仅是console.log(),正如您所示),以便您可以看到其进展,而不仅仅是最终结果。
此外,允许人类交互(这样您就可以向算法提出变异,并以自己对颜色相似性的感知指导搜索),也可能帮助您获得所需结果。这将引导您进入交互式遗传算法(IGA)。文章J. C. Quiroz, S. J. Louis, A. Shankar and S. M. Dascalu, "Interactive Genetic Algorithms for User Interface Design," 2007 IEEE Congress on Evolutionary Computation, Singapore, 2007, pp. 1366-1373, doi: 10.1109/CEC.2007.4424630. 是这种方法的一个很好的例子。

1
谢谢你的回答。我已经把赏金分配给了你,因为你的回答是唯一考虑到我问题所有限制的答案。但是你没有提供任何实际的代码示例,所以这不算是一个真正的答案,而更像是一些建议。我已经更新了我的问题,并提供了具体的代码示例,请看一下。现在,你能否提供一个遗传算法和/或优化器/求解器的代码示例,例如 optimization-js? - Andrey Mikhaylov - lolmaus
@AndreyMikhaylov-lolmaus,我提供了一个最小的进化算法,至少可以作为启发式算法的起点。我将输出添加为片段,因为SO不允许在答案正文中使用太多HTML。 - Bernardo Sulzbach
再次感谢您。我授予您第二个赏金,因为没有其他答案:1.足够通用且2.实现所需的结果且3.具有工作的JS演示文稿。但是您的演示文稿未使用问题中的实际颜色,并且未缩放网格(您具有宽度和高度常量,但结果仍为5×5网格)。请更新您的解决方案,使用我的样板中的颜色和可调整大小的网格。请通过codepen.io、jsbin.com、jsfiddle.com或类似的Web服务共享它。谢谢! - Andrey Mikhaylov - lolmaus

4
如果你能定义一个完全有序的函数来比较两种颜色,告诉你哪种颜色更“暗”,你就可以使用这个函数将颜色数组从暗到亮(或从亮到暗)进行排序。
从排好序的数组中开始,在左上角开始,沿着对角线走,并用接下来的元素填充网格。你会得到一个渐变填充的矩形网格,相邻颜色会相似。

Grid fill with color gradient

您认为这样能够达到您的目标吗?

您可以通过改变总排序函数的行为来改变外观。例如,如果使用如下所示的颜色映射按相似性排列颜色,则可以将总排序定义为从一个单元格到下一个单元格的遍历。通过更改在遍历中选择哪个单元格,您可以获得不同的颜色相似渐变网格填充。

enter image description here


没有单一的排序函数。由于三种受体类型:红色、绿色和蓝色,人类对颜色的感知是三维的。还有其他指定调色板的方法,但它们也是三维的,例如色相、饱和度和亮度。您展示的调色板使用极坐标:角度是色相,半径是亮度。它缺少的是饱和度:没有灰色的阴影和着色的灰色。在右上方有一个灰色,显然不合适。 - Andrey Mikhaylov - lolmaus
我的问题是,我知道在不撕裂(相似颜色出现在附近)的情况下,将3D网格适配到2D是不可能的。我的目标是找到一种排列方式,使颜色自然地(对于人类感知)融合在一起,没有明显的边界。我已经有一个比较函数。这更像是数学优化问题,而不是颜色问题。 - Andrey Mikhaylov - lolmaus
请查看以下 Stack Overflow 的查询和回复:https://dev59.com/h2435IYBdhLWcg3weQBy 他们建议使用 CIE 色彩空间来模拟人眼的色彩感知。 - vvg
那个问题是关于选择比较函数的。我已经选择了我的(与您链接中得到最多赞同的答案相同)。我的问题是如何使用这个函数来优化网格。 - Andrey Mikhaylov - lolmaus
我已经更新了我的问题,并提供了具体的代码示例,请查看。 - Andrey Mikhaylov - lolmaus

3
我认为,基于将每种颜色放置在其周围颜色的近似平均值处,可能存在一个简单的近似解决方案来解决这个问题。例如:
C[j] ~ sum_{i=1...8}(C[i])/8
这是离散拉普拉斯算子,即解决此方程等价于在颜色向量空间中定义离散谐函数,即谐函数具有平均值属性,即邻域内的函数平均值等于其在中心处的值。
为了找到特定的解决方案,我们需要设置边界条件,即至少在网格中固定两种颜色。在我们的情况下,选择4个极值颜色并将它们固定在网格的角落看起来很方便。
解决拉普拉斯方程的一种简单方法是松弛法(这相当于解决一组线性方程)。松弛法是一种逐步迭代的算法,每次解决一个线性方程。当然,在这种情况下,我们不能直接使用松弛法(例如,高斯-赛德尔方法),因为它实际上更像是一个组合问题而不是数值问题。但是,我们仍然可以尝试使用松弛法来解决它。
这个想法是这样的。首先确定四个角落的颜色(我们稍后再讨论这些颜色),然后用这些颜色的双线性插值来填充网格。接着随机选择一种颜色C_j,并计算相应的拉普拉斯颜色L_j,即周围邻居的平均颜色。从输入颜色集中找到最接近L_j的颜色。如果该颜色与C_j不同,则用其替换C_j。重复此过程,直到搜索完所有颜色C_j且不需要进行任何颜色替换(收敛标准)。
寻找最接近输入集中颜色的函数必须遵守一些规则,以避免出现平凡解(例如在所有邻居和中心都有相同的颜色)。
首先,要找到的颜色必须是欧几里得度量下离L_j最近的颜色。其次,该颜色不能与任何邻居颜色相同,也就是要排除邻居的颜色。你可以把这种匹配看作是将投影操作器到颜色输入集中。
预计在严格意义上不会达到收敛。因此,将迭代次数限制在一个大数字是可以接受的(如网格中单元格数量的10倍)。由于颜色C_j是随机选择的,因此输入中可能有从未放置在网格中的颜色(这对应于调和函数中的不连续性)。同时,在网格中可能存在来自初始插值猜测的颜色,以及重复颜色(如果该函数不是双射函数)。

必须将这些情况视为特殊情况(因为它们是奇点)。因此,我们必须用未放置在网格中的颜色替换来自初始猜测和重复颜色。这是一个搜索子问题,除了使用距离函数猜测替换之外,我没有明确的启发式方法可供遵循。

现在,如何选择前2或4个角落的颜色。一种可能的方法是根据欧几里得度量选择最不同的颜色。如果您将颜色视为向量空间中的点,则可以对点云执行常规PCA(主成分分析)。这相当于计算协方差矩阵的特征向量和相应的特征值。对应于最大特征值的特征向量是一个单位向量,指向最大颜色变化的方向。另外两个特征向量按顺序指向第二个和第三个最大颜色变化的方向。特征向量彼此正交,特征值在某种意义上像那些向量的“长度”。这些向量和长度可以用来确定大致包围点云(甚至离群值)的椭球体(蛋形表面),从而选取该椭球体的极端处的4种颜色作为谐波函数的边界条件。
我还没有测试这种方法,但我的直觉是,如果输入颜色平滑变化(颜色对应于颜色向量空间中的平滑曲面),则它应该给出一个好的近似解,否则解决方案将具有“奇点”,这意味着某些颜色将从邻居突然跳跃。
编辑:
我已经(部分地)实现了我的方法,视觉比较如下图所示。我的奇异点处理还不够好,你可以看到有跳跃和离群值。我没有使用你的JS工具(我的代码是用C++编写的),如果你发现这个结果有用,我会尝试将其写成JS。

enter image description here


我已经更新了我的问题并提供了代码示例,请查看。现在,您能否提供一个演示解决方案的代码示例? - Andrey Mikhaylov - lolmaus

1
我将定义颜色区域的概念,即颜色组,其中距离(P1,P2)<=容差。在这样的区域中间,您将找到通过平均值最接近所有其他颜色的点。
现在,您从一个可能无序的颜色网格开始。我的算法首先要做的是识别适合放在一起的颜色区域。根据定义,每个区域都会很好地配合在一起,因此我们来到了第二个问题,即区域间的兼容性。由于区域的非常有序的方式以及在它的中间放置中心颜色,因此它的边缘会是“锐利”的,即不同的。因此,如果将区域1和区域2放在一起,从一侧放置比从另一侧放置更具兼容性,则它们之间可能会更加兼容。因此,我们需要确定哪一侧的区域应该被黏合在一起,并且如果由于边界和其他区域的计划位置而“连接”这些侧面是不可能的(例如,区域1应该在区域2的“上方”,但由于边界,不能),则可以“旋转”一个(或两个)区域。
第三步是在进行必要的旋转后检查区域之间的边界。可能仍需要重新定位边界上的项目。

将相似的项目放到一起是很容易的。但是得到的 3×3 区域不能兼容。旋转和翻转也无法解决这个问题。这就像单独创建拼图的碎片一样:最终你会得到一堆彼此不匹配的碎片。 - Andrey Mikhaylov - lolmaus
@AndreyMikhaylov-lolmaus,我的回答并没有假设这些区域的大小为3x3。这个答案的想法是,你可以创建区域,即拼图的项目,其中心点会相当接近每个区域的项目,以便这些区域的边缘颜色更接近其他区域的边缘颜色。因此,事实上,这些边缘与其他区域的边缘非常接近,至少与这些区域的中心相比如此。正如你指出的那样,这些区域可能仍然不兼容。 - Lajos Arpad
@AndreyMikhaylov-lolmaus 为此,我们可以在这些区域的两侧进行更改,使它们更加兼容。如果这些部分仍然不适合,那么我们可以更改设置以确定“足够接近颜色”的含义。 - Lajos Arpad
我已经更新了我的问题并提供了代码示例,请查看。现在,您能否提供一个演示解决方案的代码示例? - Andrey Mikhaylov - lolmaus
@AndreyMikhaylov-lolmaus 我非常乐意这样做,但我担心正确实现它需要比我为此答案分配的时间更多的时间。这就是我主要概述我在这里应用的想法的原因。我90%确定这里的想法是可行的。当然,在看到它工作之前,有疑虑是合理的。因此,如果在我的专业工作中必须实现类似的东西,那么我一定会带着更多信息回到这里。 - Lajos Arpad

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接