组织毡尖笔:使用JS通过相邻项目的相似性来优化2D网格中项目的排列方式[更新]

问题描述

UPD:该问题已使用详细信息和代码进行了更新,请参见下文。

警告:这个问题是关于优化矩阵中项目的排列。并不是要比较颜色。最初,我认为提供有关我的问题的上下文会有所帮助。我现在对这个决定感到遗憾,因为结果恰恰相反:关于颜色的太多无关紧要的讨论,关于实际算法几乎没有什么。 ?


我为我的孩子准备了一盒80支毡尖笔,这让我非常恼火,以至于它们没有被分类。

enter image description here

我曾经在Android上玩过名为Blendoku的游戏,您只需要这样做:以一种可以形成渐变的方式排列颜色,而附近的颜色则最相似:

enter image description here

在诸如填字游戏的相交线中组织颜色既简单又有趣。但是有了这些素描标记,我有了一个完整的2D网格。更糟糕的是,颜色不是从均匀的渐变中提取的。

这使我无法凭直觉对毡尖笔进行分类。我需要通过算法实现!

这就是我所拥有的:

  • JavaScript的扎实知识
  • 所有笔的颜色值的平面数组
  • 一个function distance(color1,color2),显示颜色对的相似程度。它返回0100之间的浮点数,其中0表示颜色相同。

我所缺少的只是一种算法。

阶乘80是一个具有118位数字的数字,它排除了蛮力。

可能有一些方法可以使暴力破解变得可行:

  • 固定几支笔的位置(例如,在角落)以减少可能的组合数量;
  • 丢弃包含至少一对非常不同的邻居的分支;
  • 找到第一个令人满意的安排后停止。

但是我仍然缺少一个实际的算法,更不用说非蛮力了。

PS作业:

更新

目标

在8×10网格中排列一组预定义的80种颜色,以使颜色形成漂亮的渐变而不会撕裂。

由于下面描述的原因,没有一个确定的解决方案,可能的解决方案倾向于不完美的结果和主观性。这是预期的。

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

色彩空间是3D

人眼具有三种区分颜色的受体。人类色彩空间是三维(三色)的。

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

例如,此调色板:

HS palette

...使用极坐标,角度为色相,半径为饱和度。如果没有三维尺寸(亮度),则此色板将缺少所有明暗颜色:白色,黑色,所有灰色(中间的50%灰色除外)和有色灰色。

此调色板只是HSL / HSV颜色空间的一小部分:

enter image description here

不可能在不撕开渐变的情况下以渐变方式在2D网格上布置所有颜色

例如,这是lexicographic order中枚举成2D网格的所有32位RGB颜色。您会看到渐变具有很多撕裂感:

flat RGB palette

因此,我的目标是找到一个任意的,“足够好”的安排,使邻居或多或少地相似。我宁愿牺牲一点相似性,也不愿拥有一些非常相似的簇,它们之间会相互撕裂。

这个问题是关于用JavaScript优化网格,而不是比较颜色!

我已经选择了一个确定颜色相似度的函数:Delta E 2000。此功能经过专门设计,可以反映人类对颜色相似性的主观感觉。这是whitepaper的描述方式。

这个问题是关于优化2D网格中项目的排列方式,以使每对相邻项目(垂直和水平)的相似度尽可能低。

“优化”一词的使用并不是使算法运行得更快。从某种意义上说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以便能够创建样板。
  • 分叉样板。
  • 转到“设置/行为”,并确保禁用自动更新。
  • 调整窗格大小以最大化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"},// ...
];

索引是一种基于颜色的编号,按ID排序时,颜色在框中显示的顺序。它未在代码中使用。

Id是笔制造商提供的颜色编号。由于某些数字采用WG3的形式,因此id是字符串。


颜色类别。

此类提供了一些抽象来处理各种颜色。可以轻松地将给定颜色与另一种颜色进行比较。

  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),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,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);
  }
}

Collection:用于存储所有颜色并对它们进行排序的类。

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

解决方法

我通过将几个想法合在一起找到了目标值为1861.54的解决方案。

  1. 通过查找最小成本匹配项并将匹配的子集群连接在一起,重复3次,形成大小为8的无序颜色簇。我们使用d(C1,C2)= C1中的∑ c1 c2 C2中的子 d(c1,c2)作为子群集C1和C2的距离函数。

  2. 根据上述距离函数找到最佳的2×5簇排列。这涉及强行强制10!排列(如果有人利用对称性,则为10!/ 4,这是我不介意的)。

  3. 分别考虑每个群集,通过强制8来找到最佳4×2排列!排列。 (更可能打破对称,我没有打扰。)

  4. Brute强制4 10 可能的方式翻转群集。 (甚至有可能打破对称,我也没有打扰。)

  5. 通过本地搜索改善这种安排。我交错了两种回合:一次2 opt回合,其中每对仓位都考虑互换,而一次大邻居回合,我们选择一个随机的最大独立集,并使用匈牙利方法进行最佳分配(当我们试图移动的所有东西都不能彼此相邻。

输出看起来像这样:

felt tip pen arrangement

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

,

诀窍是暂时停止将其视为数组并将其锚定在角落。

首先,您需要定义要解决的问题。普通颜色具有三个维度:色相,饱和度和值(暗度),因此您将无法考虑二维网格上的所有三个维度。但是,您可以靠近。

如果要从白色→黑色和红色→紫色进行排列,则可以定义距离函数,以将暗度差异视为距离以及色相值差异(不变形 !)。这将为您提供一套与四个角兼容的颜色分类。

现在,将每种颜色锚定到四个角,如下所示,将(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是3维的,我们也可以也应该在模型中权衡第3维!为此,我们在计算最接近距离之前通过在模型中引入第三维来扩展先前的算法。通过将我们的2d平面与两个极端的颜色以及白色和黑色之间的水平线相交,将其放入3d空间。只需找到两个颜色极限的中点,然后略微轻拂黑暗,即可完成此操作。然后,生成均匀安装​​在该平面上的笔槽。我们可以根据其HSV值将笔直接放置在此3D空间中-H为X,V为Y,S为Z。

enter image description here

现在我们已经包含饱和度的笔的3d表示,我们可以再次遍历笔的位置,找到多项式时间中最接近的笔。

我们去了!排序很好的笔。如果要将结果存储在数组中,只需再次为每个数组索引统一生成坐标,然后按顺序使用它们即可!

现在停止分类笔并开始编写代码!

,

正如在某些评论中向您指出的那样,您似乎对找到离散的optimization problem的全局最小值之一感兴趣。如果您对此不太了解,则可能需要仔细阅读。

想象一下,您有一个错误(目标)函数,该函数只是所有(c1,c2)对相邻笔的距离(c1,c2)的总和。最佳解决方案(笔的布置)是其误差函数最小的解决方案。可能有多个最佳解决方案。请注意,不同的错误函数可能会提供不同的解决方案,您可能对我刚刚介绍的简单化错误函数所提供的结果不满意。

您可以使用现成的优化器(例如CPLEX或Gurobi),并为它提供问题的有效表述。它可能找到最佳解决方案。但是,即使不这样做,它仍然可能会提供对您的眼睛非常好的次优解决方案。

您还可以编写自己的启发式算法(例如专门的genetic algorithm),以获得比求解器可以在您所拥有的时间和空间限制内找到的解决方案更好的解决方案。鉴于您的武器似乎是输入数据,一种用于测量颜色差异的函数以及JavaScript,因此实施启发式算法可能是您最熟悉的路径。


我的答案最初没有代码,因为与大多数现实问题一样,该问题没有简单的复制粘贴解决方案。

使用JavaScript进行这种计算很奇怪,而在浏览器上进行计算甚至很奇怪。但是,由于作者明确要求here is a JavaScript implementation of a simple evolutionary algorithm hosted on CodePen

由于输入大小比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.文章是这种方法的一个很好的例子。

,

如果您可以在两种颜色之间定义总排序函数以告诉您哪种颜色是“较暗”颜色,则可以使用此总排序函数对颜色数组进行排序(从暗到亮(或从亮到暗))。 / p>

您从左上方开始,使用已排序数组中的第一种颜色,继续沿对角线穿过网格,并使用后续元素填充网格。您将获得一个渐变填充的矩形网格,其中相邻的颜色将相似。

Grid fill with color gradient

您认为这会满足您的目标吗?

您可以通过更改总体订购功能的行为来更改外观。例如,如果使用颜色图按相似性排列颜色,如下所示,则可以将总排序定义为从一个单元格到下一个单元格的映射遍历。通过更改遍历中接下来要拾取的单元格,可以获得不同的颜色相似的渐变网格填充。

enter image description here

,

我认为,将每种颜色放置在周围颜色的近似平均值上,可能会为该问题提供一个简单的近似解决方案。像这样:

C [j]〜sum_ {i = 1 ... 8}(C [i])/ 8

哪个是离散拉普拉斯算子,即求解此方程等效于在颜色矢量空间上定义一个离散谐波函数,即谐波函数具有均值属性,该均值属性表示该函数在邻域中的平均值为等于它在中间的值。

为了找到特定的解决方案,我们需要设置边界条件,即,我们必须在网格中固定至少两种颜色。在我们的案例中,选择4种极值颜色并将它们固定到网格的角落看起来很方便。

解决拉普拉斯方程式的一种简单方法是松弛法(这相当于解决线性方程组)。松弛方法是一次求解一个线性方程的迭代算法。当然,在这种情况下,我们不能直接使用松弛方法(例如,高斯·塞德尔),因为它实际上是组合问题,而不是数值问题。但是我们仍然可以尝试使用松弛来解决它。

想法如下。开始固定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

,

我将定义颜色区域的概念,即距离(P1,P2)

现在,从可能是无序的颜色网格开始。我的算法要做的第一件事是识别可以一起作为颜色区域的项目。根据定义,每个区域都非常适合,因此我们得出了区域间兼容性的第二个问题。由于一个区域的排列方式非常有序,并且我们将中间颜色放到中间,所以它的边缘将是“锋利的”,即变化的。因此,如果区域1和区域2从一侧比另一侧放在一起,则它们可能更兼容。因此,我们需要确定将区域胶合到哪一侧,以及是否由于某种原因无法“连接”这些侧(例如,区域1应该在区域2上方),但是由于其他区域的边界和计划位置),则可以“旋转”一个(或两个)区域。

第三步是在进行必要的旋转后检查区域之间的边界。可能仍需要对边界上的项目进行一些重新定位。

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...