个人博客查看。
原文查看。


通过一个小Trick实现shader的像素识别/统计操

1.简介

将一张大图分成多个小块逐步处理并逐步合并,保留关键像素的向下采样:

但我在思考一种更简便的方法,于是想到在顶点shader里做判断检测,在像素shader里获取结果这样一个形式:

用一组顶点去读单个像素,判断失败的顶点坐标提交到屏幕外,而判断成功的顶点坐标放在屏幕内。

最后在CPU中获取是否有屏幕内顶点这样一个结果,来进行简单的识别操作。

而在开启透明之后,还可以用透明度叠加来获取更复杂的结果。

2.实践

首先实践结果并没有想象的那么好,因为如果纯用三角面来做顶点部分的判断未免太费效率了。

所以我改成了传入顶点判断并生成面的方式,并且缩小了传入图片的像素大小。

1
Graphics.DrawProcedural(MeshTopology.Points, blueTex.width * blueTex.height, 1);

毕竟更多的运用场合是用来做刮刮卡或者擦除的识别。只需要检测mask图片。

上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
Shader "Hidden/FooShader"
{
Properties
{
}
SubShader
{
Blend One One

tags
{
"Queue" = "Transparent"
"RenderType" = "Transparent"
}

Pass
{
CGPROGRAM
#pragma target 4.0
#pragma vertex vert
#pragma geometry geom
#pragma fragment frag

#include "UnityCG.cginc"

struct v2f
{
float4 color : COLOR;
float4 vertex : SV_POSITION;
};

sampler2D _Image;
float4 _ImageSize;

v2f vert(uint vid : SV_VertexID)
{
v2f o = (v2f)0;

half y = floor(vid / _ImageSize.x);
half x = (vid - y * _ImageSize.x) / _ImageSize.x;
y = y / _ImageSize.y;

o.vertex = 0;

float4 image_col = tex2Dlod(_Image, half4(x,y,0,0));

if (all(image_col.rgb == half3(0, 0, 1)))
//if (all(image_col.rgb == half3(0, 1, 1))) /*error*/
{
o.color = 1;
}
else
{
o.color = 0;
}

return o;
}

[maxvertexcount(4)]
void geom(point v2f vertElement[1], inout TriangleStream<v2f> triStream)
{
if (vertElement[0].color.r <= 0) return;

float size = 10;

float4 v1 = vertElement[0].vertex + float4(-size, -size, 0, 0);
float4 v2 = vertElement[0].vertex + float4(-size, size, 0, 0);
float4 v3 = vertElement[0].vertex + float4(size, -size, 0, 0);
float4 v4 = vertElement[0].vertex + float4(size, size, 0, 0);

v2f r = (v2f)0;

r.vertex = mul(UNITY_MATRIX_VP, v1);
r.color = vertElement[0].color;
triStream.Append(r);

r.vertex = mul(UNITY_MATRIX_VP, v2);
r.color = vertElement[0].color;
triStream.Append(r);

r.vertex = mul(UNITY_MATRIX_VP, v3);
r.color = vertElement[0].color;
triStream.Append(r);

r.vertex = mul(UNITY_MATRIX_VP, v4);
r.color = vertElement[0].color;
triStream.Append(r);
}

fixed4 frag(v2f i) : SV_Target
{
return i.color;
}
ENDCG
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

namespace Hont
{
public class Foo : MonoBehaviour
{
void Start()
{
var blueTex = new Texture2D(64, 64);
for (int x = 0; x < blueTex.width; x++)
for (int y = 0; y < blueTex.height; y++)
blueTex.SetPixel(x, y, Color.blue);
blueTex.Apply();

var mat = new Material(Shader.Find("Hidden/FooShader"));
mat.SetTexture("_Image", blueTex);
mat.SetVector("_ImageSize", new Vector4(blueTex.width, blueTex.height));
mat.SetPass(0);
var tempRT = RenderTexture.GetTemporary(16, 16, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB, 1);
tempRT.filterMode = FilterMode.Point;
tempRT.autoGenerateMips = false;
tempRT.anisoLevel = 0;
tempRT.wrapMode = TextureWrapMode.Clamp;
var cacheRT = RenderTexture.active;
RenderTexture.active = tempRT;
Graphics.DrawProcedural(MeshTopology.Points, blueTex.width * blueTex.height, 1);
var tex2D = new Texture2D(16, 16, TextureFormat.ARGB32, false, false);
tex2D.wrapMode = TextureWrapMode.Clamp;
tex2D.anisoLevel = 0;
tex2D.filterMode = FilterMode.Point;
tex2D.ReadPixels(new Rect(0, 0, 16, 16), 0, 0);
var firstPixel = tex2D.GetPixel(0, 0);
Debug.Log("firstPixel: " + firstPixel);
RenderTexture.active = cacheRT;
RenderTexture.ReleaseTemporary(tempRT);
}
}
}

跑了一下代码之后我发现了三个问题,也是没解决的问题,一个是计算结果有误差

1
o.color = float4(0.05, 0, 0, 0);

输出是0.05结果却有一些出入。

特别是当返回颜色小于0.1之后,我尝试改变图像格式或者RT等参数依旧没能解决

第二个问题是开启透明后,透明图片的叠加是有上限的,毕竟深度有限,堆叠二十多层后,后面层会丢失。

第三个问题是传入图片尺寸过大直接导致带宽爆炸,以至于unity直接假死了,512x512的图片就是26万多的像素要处理,也就是26万多的顶点。

第三个问题很好解决,控制图片尺寸+让单个顶点采样更多像素即可。

对于第一个问题,目前还不需要太精确所以没解决但也能用。第二个问题可以用一些方法来缓解

比如在顶点shader中增加运算量,把返回值分散到rgba四个通道上去。

1
2
3
4
5
6
7
8
9
10
11
12
13
uint roll = (roll_width + roll_height) % 4;

if (roll == 0)
result = float4(GAIN_VALUE, 0, 0, 0);

if (roll == 1)
result = float4(0, GAIN_VALUE, 0, 0);

if (roll == 2)
result = float4(0, 0, GAIN_VALUE, 0);

if (roll == 3)
result = float4(0, 0, 0, GAIN_VALUE);

把更多的像素遍历放入顶点中,这样处理图片的顶点数量是原大小/n:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
v2f vert(uint vid : SV_VertexID)
{
v2f o = (v2f)0;

o.vertex = 0;

half2 image_size = half2(GRID_SIZE_X * LOOP_IMAGE_SIZE_X, GRID_SIZE_Y * LOOP_IMAGE_SIZE_Y);

half y = floor(vid / LOOP_IMAGE_SIZE_X);
half x = (vid - y * LOOP_IMAGE_SIZE_X) / LOOP_IMAGE_SIZE_X;
y = y / LOOP_IMAGE_SIZE_Y;
//将vid转化为x,y坐标

for (half rx = 0; rx < GRID_SIZE_X; rx++)
{
for (half ry = 0; ry < GRID_SIZE_Y; ry++)
{
half xx = x + rx;
half yy = y + ry;

float4 r = Statistics_sample(_Image, _Rec_Color, half4(xx, yy, 0, 0), image_size);

o.color += r;
}
}
//一个顶点处理多个像素

return o;
}

3.测试结果

最终达到了一个比较不错的结果,我把相关函数封装成了一个类。

我写了一个涂抹效果demo来测试一下,它通过识别白色像素的数量来判断是否为全部涂完:

工程文件我丢在了github上: https://github.com/hont127/Image-Rec-Base-unity-shader-

GUGI 中通过改变像素实现擦除

这种方法需要改精灵的设置,如下:

话不多说,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
using System.Collections.Generic;
using System.Reflection.Emit;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Profiling;
using UnityEngine.UI;


public class ChangeTexturePixel : MonoBehaviour, IDragHandler
{
/// <summary> 擦除的像素数量 </summary>
private int m_PixelAcount = 0;

/// <summary> 是否擦除成功 </summary>
private bool m_IsDrag = false;

/// <summary> 擦除范围大小 </summary>
[SerializeField][Range(10,100)]
private int Radius = 10;

/// <summary> 擦除完成度(不超过1)</summary>
[SerializeField][Range(0,1)]
private float m_Complete;

private RawImage m_UITex;

private Texture2D m_MyTex;

[SerializeField]
private Color m_Col = Color.clear;

private int[][] m_PixelArray;

private Dictionary<int, TexturePixel> m_TexPixelDic = new Dictionary<int, TexturePixel>();

void Start()
{
m_IsDrag = false;
m_UITex = GetComponent<RawImage>();
var tex = m_UITex.texture as Texture2D;

m_MyTex = new Texture2D(tex.width, tex.height, TextureFormat.ARGB32,
false);

m_MyTex.SetPixels(tex.GetPixels());
m_MyTex.Apply();
m_UITex.texture = m_MyTex;

int value = 0;
m_PixelArray = new int[m_MyTex.width][];
for (int i = 0; i < m_PixelArray.Length; i++)
{
m_PixelArray[i] = new int[m_MyTex.height];
for (int j = 0; j < m_MyTex.height; j++)
{
m_PixelArray[i][j] = value;

m_TexPixelDic.Add(value, new TexturePixel(m_MyTex, i, j));
value++;
}
}

}

/// <summary>
/// 改变Texture2D像素点颜色
/// </summary>
/// <param name="x">Texture2D像素点X轴位置</param>
/// <param name="y">Texture2D像素点Y轴位置</param>
/// <param name="radius">改变像素的范围</param>
/// <param name="col">改变后的颜色</param>
void ChangePixelColorByCircle(int x, int y, int radius, Color col)
{
for (int i = -Radius; i < Radius; i++)
{
var py = y + i;
if (py < 0 || py >= m_MyTex.height)
{
continue;
}

for (int j = -Radius; j < Radius; j++)
{
var px = x + j;
if (px < 0 || px >= m_MyTex.width)
{
continue;
}

if (new Vector2(px - x, py - y).magnitude > Radius)
{
continue;
}

Profiler.BeginSample("text1");
TexturePixel tp; //= texPixelDic[pixelArray[MyTex.width - 1][py]];

if (px == 0)
{
tp = m_TexPixelDic[m_PixelArray[m_MyTex.width - 1][py]];
tp.Scratch(m_Col);

}

tp = m_TexPixelDic[m_PixelArray[px][py]];
if (!tp.GetPixel())
{
m_PixelAcount++;
}
tp.Scratch(m_Col);

Profiler.EndSample();
}
}

Profiler.BeginSample("text2");
m_MyTex.Apply();
Profiler.EndSample();
Profiler.BeginSample("text3");
Profiler.EndSample();
}

/// <summary>
/// 擦除点
/// </summary>
/// <param name="mousePos">鼠标位置</param>
/// <returns>擦除点</returns>
Vector2 ScreenPoint2Pixel(Vector2 mousePos)
{
float imageWidth = m_UITex.rectTransform.sizeDelta.x;
float imageHeight = m_UITex.rectTransform.sizeDelta.y;
Vector3 imagePos = m_UITex.rectTransform.anchoredPosition3D;
//求鼠标在image上的位置
float HorizontalPercent =
(mousePos.x - (Screen.width / 2 + imagePos.x - imageWidth / 2)) / imageWidth; //鼠标在Image 水平上的位置 %
float verticalPercent =
(mousePos.y - (Screen.height / 2 + imagePos.y - imageHeight / 2)) / imageHeight; //鼠标在Image 垂直上的位置 %
float x = HorizontalPercent * m_MyTex.width;
float y = verticalPercent * m_MyTex.height;
return new Vector2(x, y);
}


/// <summary>
/// 拖拽中。。。
/// </summary>
/// <param name="eventData">拖拽数据</param>
public void OnDrag(PointerEventData eventData)
{
if (!m_IsDrag)
{
var posA = ScreenPoint2Pixel(eventData.position);
ChangePixelColorByCircle((int) posA.x, (int) posA.y, Radius, m_Col);
SetAllPixelFadeAlpha();

}
}

/// <summary>
/// 擦除完成时调用
/// </summary>
public void SetAllPixelFadeAlpha()
{
if (++m_PixelAcount >= m_MyTex.height*m_MyTex.width*m_Complete)
{
m_UITex.color = Color.clear;
m_IsDrag = true;
Debug.Log("擦除完成");
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
sing System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TexturePixel
{
public Texture2D myTex;
//float alpha = 1; //当前透明度
// int scratchedTime = 0;//被刮的次数
private int x; //像素坐标X
private int y; //像素坐标Y
//private bool scratcedPrevious = false;
//private bool scratcedCurrent = false;
public TexturePixel(Texture2D tex,int x,int y)
{
myTex = tex;
this.x = x;
this.y = y;
}

public void Scratch( Color targetCol)
{
myTex.SetPixel(x,y,targetCol);
// scratcedCurrent = true;
//Debug.Log("x:"+x+" y:"+y+" a "+ targetCol.a);
}

public bool GetPixel()
{
Color color = myTex.GetPixel(x, y);

return color.a <= 0;
}

以上方法是通过改变Texture2D像素点颜色实现擦除,主要内容:

1
2
3
4
// 设置像素点
myTex.SetPixel(x,y,targetCol);
// 获取像素点
myTex.GetPixel(x,y,targetCol);

项目文件我放在了gitee: https://gitee.com/ondaly/eraser_-master.git
项目案例查看。