Fruity Eclipse Collections
发布时间:2022-10-13 11:05AM
这篇博文延续了上一篇文章中使用水果 emoji 的主题,但我们不关注深度学习,而是先探索一些 Eclipse Collections 的顶级方法,并使用水果 emoji 示例进行聚类分析(使用 K-Means 算法)。
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 库。这里我们只使用一个方法,它为我们管理线程池生命周期。
探索 emoji 颜色
为了好玩,让我们看看每种水果的指定颜色是否与相关 emoji 的颜色匹配。与之前的博客一样,我们将使用稍好一点的 Noto Color Emoji 字体来显示我们的水果,如下所示
我们将使用 Eclipse Collections 的 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 度范围内的颜色光谱
我们有两个辅助方法来处理颜色。第一个挑选出“主要是黑色”和“主要是白色”的颜色,而第二个使用 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 没有标准的 PURPLE 颜色,因此我们通过为 MAGENTA 选择合适的广谱来将紫色与洋红色组合。
我们将尝试 3 种方法来确定每个 emoji 的主要颜色
-
最常见的颜色:我们找到每个点的颜色光谱值并计算每种颜色的点数。将选择点数最多的颜色。这很简单,在许多情况下都有效,但如果一个苹果或樱桃有 100 种红色深浅,但只有一种绿色深浅用于茎或叶,则可能会选择绿色。
-
最常见的范围:我们将每个点分组到颜色范围中。将选择点数最多的范围。
-
最大簇的质心:我们将 emoji 图像分成一个子图像网格。我们将对子图像中每个点的 RGB 值执行 K-Means 聚类。这将把颜色相似的点聚集到一个簇中。将选择点数最多的簇,并将其质心选为选定的主要颜色。这种方法会影响按颜色对子图像进行像素化。这种方法受到这篇Python 文章的启发。
最常见的颜色
忽略背景白色,我们 PEACH emoji 最常见的颜色是橙色。下图显示了每种颜色的计数:
最常见的范围
如果不是计算每种颜色,而是将颜色分组到它们的范围中并计算每个范围中的数字,我们得到 PEACH 的以下图表:
K-Means
K-Means 是一种用于查找簇质心的算法。对于 k=3,我们将从选择 3 个随机点作为起始质心开始。
我们将所有点分配到它们最近的质心
根据此分配,我们从其所有点重新计算每个质心
我们重复此过程,直到找到稳定的质心选择,或者我们已达到一定的迭代次数。我们使用了 Apache Commons Math 中的 K-Means 算法。
这是在 PEACH emoji 的完整点集上运行时我们期望得到的结果。黑点是质心。它找到了一个绿色、一个橙色和一个红色质心。分配到其上的点数最多的质心应该是最主要的颜色。(这是另一个交互式 3D 散点图。)
我们可以将分配给每个簇的点数绘制成条形图。(我们使用了一个 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"
}
这是结果图像
这是最终结果
在我们的例子中,所有三种方法都产生了相同的结果。其他 emoji 的结果可能会有所不同。
更多信息
-
包含示例代码的仓库:https://github.com/paulk-asert/fruity-eclipse-collections
-
K-Means 聚类的更多示例:https://github.com/paulk-asert/groovy-data-science/tree/master/subprojects/Whiskey
-
聚类相关幻灯片:https://speakerdeck.com/paulk/groovy-data-science?slide=94
-
Eclipse collections 主页:https://www.eclipse.org/collections/