Fruity Eclipse Collections

作者: Paul King
发布: 2022-10-13 11:05AM


这篇博客文章继续使用 上一篇帖子 中的水果表情符号,但这次我们将使用 k-means 聚类来探索水果的集群,而不是深度学习,首先我们将使用水果表情符号示例探索一些 Eclipse Collections 的顶级方法。

Eclipse Collections 水果沙拉

首先,我们将定义一个 Fruit 枚举(与相关的 Eclipse Collections kata 相比,它添加了一种额外的水果)

enum Fruit {
    APPLE('🍎', Color.RED),
    PEACH('🍑', Color.ORANGE),
    BANANA('🍌', Color.YELLOW),
    CHERRY('🍒', Color.RED),
    ORANGE('🍊', Color.ORANGE),
    GRAPE('🍇', Color.MAGENTA)

    public static ImmutableList<Fruit> ALL = Lists.immutable.with(values())
    public static ImmutableList<String> ALL_EMOJI = Lists.immutable.with(*values()*.emoji)
    final String emoji
    final Color color

    Fruit(String emoji, Color color) {
        this.emoji = emoji
        this.color = color
    }

    static Fruit of(String emoji) {
        values().find{it.emoji == emoji }
    }
}

我们可以在以下示例中使用此枚举,这些示例展示了许多常见的 Eclipse Collections 方法

assert Lists.mutable.with('🍎', '🍎', '🍌', '🍌').distinct() ==
        Lists.mutable.with('🍎', '🍌')

var onlyBanana = Sets.immutable.with('🍌')

assert Fruit.ALL_EMOJI.select(onlyBanana::contains) == List.of('🍌')

assert Fruit.ALL_EMOJI.reject(onlyBanana::contains) ==
        List.of('🍎', '🍑', '🍒', '🍊', '🍇')

assert Fruit.ALL.groupBy(Fruit::getColor) ==
        Multimaps.mutable.list.empty()
                .withKeyMultiValues(RED, Fruit.of('🍎'), Fruit.of('🍒'))
                .withKeyMultiValues(YELLOW, Fruit.of('🍌'))
                .withKeyMultiValues(ORANGE, Fruit.of('🍑'), Fruit.of('🍊'))
                .withKeyMultiValues(MAGENTA, Fruit.of('🍇'))

assert Fruit.ALL.countBy(Fruit::getColor) ==
        Bags.immutable.withOccurrences(RED, 2, YELLOW, 1, ORANGE, 2, MAGENTA, 1)

Fruit.ALL_EMOJI.chunk(4).with {
    assert first == Lists.mutable.with('🍎', '🍑', '🍌', '🍒')
    assert last == Lists.mutable.with('🍊', '🍇')
}

// For normal threads, replace 'withExistingPool' line of code with:
//GParsExecutorsPool.withPool { pool ->
GParsExecutorsPool.withExistingPool(Executors.newVirtualThreadPerTaskExecutor()) { pool ->
    var parallelFruit = Fruit.ALL.asParallel(pool, 1)
    var redFruit = parallelFruit.select(fruit -> fruit.color == RED).toList()
    assert redFruit == Lists.mutable.with(Fruit.of('🍎'), Fruit.of('🍒'))
}

最后一个示例在并行线程中计算红色水果。在代码中,它在启用了预览功能的 JDK19 上运行时使用虚拟线程。您可以按照注释中的建议在其他 JDK 版本上运行,或使用普通线程运行。除了 Eclipse Collections 之外,我们还在类路径上添加了 GPars 库。在这里,我们只使用一种方法来管理我们的池生命周期。

探索表情符号颜色

为了好玩,让我们看看每个水果的命名颜色是否与相关表情符号的颜色匹配。与之前的博客一样,我们将使用更漂亮的 Noto Color Emoji 字体来显示我们的水果,如这里所示

Noto Color Emoji

我们将使用 Eclipse Collection BiMap 在颜色名称和 java.awt 颜色之间来回切换

@Field public static COLOR_OF = BiMaps.immutable.ofAll([
    WHITE: WHITE, RED: RED, GREEN: GREEN, BLUE: BLUE,
    ORANGE: ORANGE, YELLOW: YELLOW, MAGENTA: MAGENTA
])
@Field public static NAME_OF = COLOR_OF.inverse()

我们还将使用一些辅助函数在 RGB 和 HSB 颜色值之间进行切换

static hsb(int r, int g, int b) {
    float[] hsb = new float[3]
    RGBtoHSB(r, g, b, hsb)
    hsb
}

static rgb(BufferedImage image, int x, int y) {
    int rgb = image.getRGB(x, y)
    int r = (rgb >> 16) & 0xFF
    int g = (rgb >> 8) & 0xFF
    int b = rgb & 0xFF
    [r, g, b]
}

HSB 颜色空间以 0 到 360 度的光谱表示颜色

图片来源:https://nycdoe-cs4all.github.io/units/1/lessons/lesson_3.2

Color Circle

我们有两个辅助方法来帮助处理颜色。第一个方法挑选出“几乎是黑色”和“几乎是白色”的颜色,而第二个方法使用 switch 表达式来划分我们感兴趣的颜色的一些颜色空间区域

static range(float[] hsb) {
    if (hsb[1] < 0.1 && hsb[2] > 0.9) return [0, WHITE]
    if (hsb[2] < 0.1) return [0, BLACK]
    int deg = (hsb[0] * 360).round()
    return [deg, range(deg)]
}

static range(int deg) {
    switch (deg) {
        case 0..<16 -> RED
        case 16..<35 -> ORANGE
        case 35..<75 -> YELLOW
        case 75..<160 -> GREEN
        case 160..<250 -> BLUE
        case 250..<330 -> MAGENTA
        default -> RED
    }
}

请注意,JDK 没有标准的紫色,因此我们将紫色与洋红色结合在一起,为洋红色选择一个合适的广谱。

我们使用了一个 Plotly 3D 交互式散点图(如 Tablesaw Java 数据框和可视化库支持的那样)来可视化我们的表情符号颜色(以颜色光谱上的度数表示)与 XY 坐标:颜色与 xy 图

我们将尝试三种方法来确定每个表情符号的主要颜色

  • 最常见颜色:我们找到每个点的颜色光谱值,并计算每个颜色点的数量。拥有最多点的颜色将被选中。这很简单,在许多情况下都适用,但如果一个苹果或樱桃有 100 种红色阴影,而茎或叶子的绿色阴影只有一种,那么绿色可能会被选中。

  • 最常见范围:我们将每个点分组到一个颜色范围内。拥有最多点的范围将被选中。

  • 最大集群的质心:我们将表情符号图像划分为一个子图像网格。我们将对子图像中每个点的 RGB 值执行 k-means 聚类。这将把类似颜色的点聚集成一个集群。将选择拥有最多点的集群,并将其质心作为选定的主要颜色。这种方法具有通过颜色将我们的子图像像素化的效果。这种方法的灵感来自于这篇文章 python 文章

最常见颜色

忽略背景白色,我们桃子表情符号中最常见的颜色是橙色阴影。下面的图形显示了每种颜色的计数:桃子的颜色直方图

最常见范围

如果我们不是计算每种颜色的数量,而是将颜色分组到它们的范围内并计算每个范围中的数量,那么我们就会得到桃子的以下图形:桃子的范围直方图

K-Means

K-Means 是一种用于查找集群质心的算法。对于 k=3,我们将从选择 3 个随机点作为我们起始质心开始。

kmeans step 1

我们将所有点分配到距离它们最近的质心

kmeans step 2

根据这种分配,我们将从所有点重新计算每个质心

kmeans step 3

我们将重复此过程,直到找到一个稳定的质心选择,或者我们达到了某个迭代次数。我们使用了 Apache Commons Math 中的 K-Means 算法。

如果对桃子表情符号的完整点集运行,我们会期望得到这样的结果。黑点是质心。它找到了一个绿色、一个橙色和一个红色质心。分配到它最多点的质心应该是最主要的颜色。(这是另一个交互式 3D 散点图。)

RgbPeach3d 我们可以将分配到每个集群的点数绘制为条形图。(我们使用了一个 Scala 绘图库 来显示 Groovy 与 Scala 的集成。) 桃子颜色质心大小

绘制以上图表代码如下

var trace = new Bar(intSeq([1, 2, 3]), intSeq(sizes))
        .withMarker(new Marker().withColor(oneOrSeq(colors)))

var traces = asScala([trace]).toSeq()

var layout = new Layout()
        .withTitle("Centroid sizes for $fruit")
        .withShowlegend(false)
        .withHeight(600)
        .withWidth(800)

Plotly.plot(path, traces, layout, defaultConfig, false, false, true)

带有子图像的 K-Means

我们将在第三种方法中采用的方法增强了 K-Means。我们不是像刚刚显示的图表那样为整个图像找到质心,而是将图像划分为子图像,并在每个子图像上执行 K-Means。我们的总体主要颜色被确定为预测到我们所有子图像中最常见的颜色。

把所有东西放在一起

以下是涵盖所有三种方法的最终代码(包括打印一些漂亮的图像以突出显示第三种方法和 Plotly 3D 散点图)

var results = Fruit.ALL.collect { fruit ->
    var file = getClass().classLoader.getResource("${fruit.name()}.png").file as File
    var image = ImageIO.read(file)

    var colors = [:].withDefault { 0 }
    var ranges = [:].withDefault { 0 }
    for (x in 0..<image.width) {
        for (y in 0..<image.height) {
            def (int r, int g, int b) = rgb(image, x, y)
            float[] hsb = hsb(r, g, b)
            def (deg, range) = range(hsb)
            if (range != WHITE) { // ignore white background
                ranges[range]++
                colors[deg]++
            }
        }
    }
    var maxRange = ranges.max { e -> e.value }.key
    var maxColor = range(colors.max { e -> e.value }.key)

    int cols = 8, rows = 8
    int grid = 5 // thickness of black "grid" between subimages
    int stepX = image.width / cols
    int stepY = image.height / rows
    var splitImage = new BufferedImage(image.width + (cols - 1) * grid, image.height + (rows - 1) * grid, image.type)
    var g2a = splitImage.createGraphics()
    var pixelated = new BufferedImage(image.width + (cols - 1) * grid, image.height + (rows - 1) * grid, image.type)
    var g2b = pixelated.createGraphics()

    ranges = [:].withDefault { 0 }
    for (i in 0..<rows) {
        for (j in 0..<cols) {
            def clusterer = new KMeansPlusPlusClusterer(5, 100)
            List<DoublePoint> data = []
            for (x in 0..<stepX) {
                for (y in 0..<stepY) {
                    def (int r, int g, int b) = rgb(image, stepX * j + x, stepY * i + y)
                    var dp = new DoublePoint([r, g, b] as int[])
                    var hsb = hsb(r, g, b)
                    def (deg, col) = range(hsb)
                    data << dp
                }
            }
            var centroids = clusterer.cluster(data)
            var biggestCluster = centroids.max { ctrd -> ctrd.points.size() }
            var ctr = biggestCluster.center.point*.intValue()
            var hsb = hsb(*ctr)
            def (_, range) = range(hsb)
            if (range != WHITE) ranges[range]++
            g2a.drawImage(image, (stepX + grid) * j, (stepY + grid) * i, stepX * (j + 1) + grid * j, stepY * (i + 1) + grid * i,
                    stepX * j, stepY * i, stepX * (j + 1), stepY * (i + 1), null)
            g2b.color = new Color(*ctr)
            g2b.fillRect((stepX + grid) * j, (stepY + grid) * i, stepX, stepY)
        }
    }
    g2a.dispose()
    g2b.dispose()

    var swing = new SwingBuilder()
    var maxCentroid = ranges.max { e -> e.value }.key
    swing.edt {
        frame(title: 'Original vs Subimages vs K-Means',
                defaultCloseOperation: DISPOSE_ON_CLOSE, pack: true, show: true) {
            flowLayout()
            label(icon: imageIcon(image))
            label(icon: imageIcon(splitImage))
            label(icon: imageIcon(pixelated))
        }
    }

    [fruit, maxRange, maxColor, maxCentroid]
}

println "Fruit  Expected      By max color  By max range  By k-means"
results.each { fruit, maxRange, maxColor, maxCentroid ->
    def colors = [fruit.color, maxColor, maxRange, maxCentroid].collect {
        NAME_OF[it].padRight(14)
    }.join().trim()
    println "${fruit.emoji.padRight(6)} $colors"
}

以下是生成的图像

peach images banana images cherry images orange images grape images apple images

以下是最终结果

results

在我们的例子中,所有三种方法都得到了相同的结果。其他表情符号的结果可能会有所不同。