简介
最近接触到了部分与数学建模和图形学的知识,由于自己使用Unity非常久了,所以想尝试一下如何使用Unity+C#来实现所需要的功能。这一次的目的是为了实现图形分割方面较为基本的算法,用的也是较为简易的KMeans算法,同时也熟悉一下Unity中如何使用脚本对贴图进行更改。这也是第一次尝试写技术方向的东西,一些不完善的地方也希望能够指正。
实现流程
了解什么是kMeans,可以看
wiki上的官方解释
youtube上的解释(这个讲的就是在图像上的应用,非常清晰)
流程步骤
- 分配。如果要做图像上的处理,实际上就是对当前整个贴图的所有像素进行记录,并分配到目标的聚类中,其中的实现主要是依靠对每个贴图中的像素判断当前的颜色值和目标值的差距,由于基本颜色由rgba构成,而a都是为1的所以在比较中主要看rgb的差别,在这里我将它处理成vector3,方便使用一些计算方法,将当前的颜色生成vector3与K个目标颜色聚类颜色对应生成的vector3相比较,最终得到距离最近的那个作为自己的归属。
- 更新。在每个像素点确定自己的归属后,聚类中心点需要对所有那些选择自己做归属的点做更新位置,方式就是取那些点的平均值。
- 收敛。在所有的聚类中心更新后,判断是否更新变化是否小到一定程度,如果点的位置基本不变了那么就认定收敛成功,停止循环,否则不断重复1-3的步骤。
核心代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
//记录聚类点的拥有像素点的对应vector3之和以及数量,方便运算统计,减少内存占用
public class ColorInfo
{
public Vector3 colorSum;
public int ColorCount;
public void Clear()
{
colorSum = Vector3.zero;
ColorCount = 0;
}
}
public class KMeanSegmentation : MonoBehaviour {
//原始图像
public Texture rawTex;
//获取K值
public InputField K_InputField;
//生成的新的图像
private Texture2D tex;
//随机生成的聚类颜色
Color[] randomColors;
//存储聚类index与对应的像素点信息
Dictionary<int, ColorInfo> colorDict = new Dictionary<int, ColorInfo>();
public void KMeansImage(int k)
{
//获取InputField的值,k的赋值
if (K_InputField != null && K_InputField.text != "")
{
int tempK = int.Parse(K_InputField.text);
k = tempK;
}
//获取原始图像
tex = Instantiate(rawTex) as Texture2D;
//获取图像中所有像素的颜色
Color[] texPixColors = tex.GetPixels();
//刷新colorDict
colorDict = new Dictionary<int, ColorInfo>();
//通过不同的k值,随机生成聚类点
randomColors = new Color[k];
for(int i = 0; i < k; i++)
{
randomColors[i] = new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f));
colorDict.Add(i, new ColorInfo());
}
//当所有聚类点的变化值小于0.1时表示生成完毕,否则不断进行更新和分配
float diff = 100;
while (diff > 0.1f)
{
for (int i = 0; i < texPixColors.Length; i++)
{
int groupIndex = GetColorGroup(texPixColors[i]);
colorDict[groupIndex].colorSum += new Vector3(texPixColors[i].r, texPixColors[i].g, texPixColors[i].b);
colorDict[groupIndex].ColorCount++;
}
diff = ReplaceColors();
}
//得到所有的正确聚类颜色对所有的像素点依次赋值,并生成新的贴图
Color[] finalColors = new Color[texPixColors.Length];
for (int i = 0; i < texPixColors.Length; i++)
{
int groupIndex = GetColorGroup(texPixColors[i]);
finalColors[i] = randomColors[groupIndex];
}
tex.SetPixels(finalColors);
tex.Apply();
//贴上新的贴图
GetComponent<Renderer>().material.mainTexture = tex;
}
//切回原图显示
public void ResetImage()
{
GetComponent<Renderer>().material.mainTexture = rawTex;
colorDict = new Dictionary<int, ColorInfo>();
}
//判断所属的聚类块
int GetColorGroup(Color c)
{
int belongedGroup = -1;
float distance = 10;
for(int i = 0; i < randomColors.Length; i++)
{
Vector3 v1 = new Vector3(c.r, c.g, c.b);
Vector3 v2 = new Vector3(randomColors[i].r, randomColors[i].g, randomColors[i].b);
float tempDistance = Vector3.SqrMagnitude(v1 - v2);
if (distance > tempDistance)
{
distance = tempDistance;
belongedGroup = i;
}
}
return belongedGroup;
}
//更新聚类点,返回变化程度diff
float ReplaceColors()
{
float diff = 0;
for(int i = 0; i < randomColors.Length; i++)
{
Vector3 v = new Vector3(colorDict[i].colorSum.x, colorDict[i].colorSum.y, colorDict[i].colorSum.z);
if (colorDict[i].ColorCount == 0)
{
v = Vector3.zero;
}
else
{
v = v / colorDict[i].ColorCount;
}
diff += Vector3.Distance(v , new Vector3(randomColors[i].r, randomColors[i].g, randomColors[i].b));
randomColors[i] = new Color(v.x, v.y, v.z, 1);
}
foreach(var v in colorDict.Values)
{
v.Clear();
}
return diff;
}
}
总结
效果:
GitHub地址:https://github.com/cyclons/ImageSegmentation_Unity
实际上这次用到的Unity相关的不是很多,效率也不搞,主要还是代码的算法设计,实现的算法也并不复杂,主要还是一次尝试,也会随着深入了解对算法进行优化。谢谢。