一、遍历边界法
二、优缺点
weight
控制比较值compare
的大小,当像素r通道的值小于该值时则认为像素处于符号遮罩里true
和false
存储遮罩信息var wg sync.WaitGroup
// GetPhotoMask 获取灰度图遮罩数据
func GetPhotoMask(img image.Image, length, width int, weight float64) (masks [][]bool, err error) {
if weight < 0 || weight > 1 {
return nil, fmt.Errorf("weight must between 0 and 1")
}
masks = make([][]bool, width)
compare := uint32(32767 * weight)
for i := 0; i < width; i++ {
//异步处理
wg.Add(1)
go func(img image.Image, length, lineIdx int, compare uint32) {
defer wg.Done()
maskArray := traversalArray(img, length, lineIdx, compare)
masks[lineIdx] = maskArray
}(img, length, i, compare)
}
wg.Wait()
return
}
// 遍历图片
func traversalArray(img image.Image, length, lineIdx int, compare uint32) []bool {
temp := make([]bool, 0, length)
for j := 0; j < length; j++ {
if r, _, _, _ := img.At(j, lineIdx).RGBA(); r <= compare {
temp = append(temp, false)
} else {
temp = append(temp, true)
}
}
return temp
}
根据遮罩信息判断像素是否在边界,需要从横纵两方向进行遍历
遍历时需要在二维数组的周围虚拟1个true元素,用于判断数组边缘元素是否属于边界
pre
设置为true
cur
设为true执行两次遍历时会得到重复的元素需要对元素去重,golang里常用哈希map
实现
坐标过滤公式:$key=x*width+y$(对于不同的坐标其标记是不同的,对于相同坐标拥有相同的标记)
为方便后续计算定义结构体存储预计算结果,计算前需要对x
和y
归一化处理(x除以长,y除以宽)
x2y2
:$x^2y^2$2x
:$2x$2y
:$2y$数组索引(i,j)映射到坐标(x,y)的公式:$(i,j)→(x,y)=(j,i)$
type BorderBlock struct {
NormalizeX2y2 float64
Normalize2x float64
Normalize2y float64
}
//构造函数
func createBlock(x, y, length, width int) (block *BorderBlock) {
block = &BorderBlock{}
//归一化
a := float64(x) / float64(length)
b := float64(y) / float64(width)
block.NormalizeX2y2 = a*a + b*b
block.Normalize2x = a + a
block.Normalize2y = b + b
return
}
//寻找边界元素
func FindBorder(masks [][]bool) (border []*BorderBlock) {
width := len(masks)
length := len(masks[0])
borderChan := make(chan *deliver, length)
border = make([]*BorderBlock, 0, length*4)
//边界元素收集者
wg2.Add(1)
go func() {
defer wg2.Done()
collector(borderChan, length, &border)
}()
//异步遍历
for i := 0; i < width; i++ {
wg1.Add(1)
go func(i int) {
defer wg1.Done()
traversalL2R(borderChan, masks, width, length, i)
}(i)
}
//异步遍历
for j := 0; j < length; j++ {
wg1.Add(1)
go func(j int) {
defer wg1.Done()
traversalT2B(borderChan, masks, width, length, j)
}(j)
}
//同步
wg1.Wait()
close(borderChan)
wg2.Wait()
return
}
// 左到右遍历
func traversalL2R(borderChan chan *deliver, photoMask [][]bool, width, length, i int) {
var pre, cur bool
pre = true
for j := 0; j <= width; j++ {
if j == width {
cur = true
} else {
cur = photoMask[i][j]
}
if pre && !cur {
borderChan <- &deliver{key: i*width + j, block: createBlock(j, i, length, width)}
} else if !pre && cur {
borderChan <- &deliver{key: i*width + j, block: createBlock(j-1, i, length, width)}
}
pre = cur
}
}
// 上到下遍历
func traversalT2B(borderChan chan *deliver, photoMask [][]bool, width, length, j int) {
var pre, cur bool
pre = true
for i := 0; i <= width; i++ {
if i == width {
cur = true
} else {
cur = photoMask[i][j]
}
if pre && !cur {
borderChan <- &deliver{key: i*width + j, block: createBlock(j, i, length, width)}
} else if !pre && cur {
borderChan <- &deliver{key: i*width + j, block: createBlock(j, i-1, length, width)}
}
pre = cur
}
}
ndc
空间的值([-1,1]
)转为图像空间的值([0,1]
),最后输出灰度像素用于存储在图像里var wg sync.WaitGroup
// GenerateSDF 获取sdf
func GenerateSDF(width, length int, borders []*border.BorderBlock, masks [][]bool) [][]float64 {
sdf := make([][]float64, len(masks))
for i := 0; i < width; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
arr := make([]float64, length)
for j := 0; j < length; j++ {
arr[j] = computeSDF(float64(j), float64(i), float64(length), float64(width), borders, !masks[i][j])
}
sdf[i] = arr
}(i)
}
wg.Wait()
return sdf
}
// 计算sdf的值
func computeSDF(x, y, length, width float64, borders []*border.BorderBlock, isMask bool) float64 {
a := x / length
b := y / width
a2b2 := a*a + b*b
var _min, temp float64
_min = math.Inf(1)
for _, p := range borders {
distance := p.NormalizeX2y2 + a2b2 - p.Normalize2x*a - p.Normalize2y*b
temp = math.Sqrt(distance)
_min = Min(_min, temp)
}
if isMask {
_min *= -1
}
//归一化距离
return _min / math.Sqrt2
}
//不要使用内置的min,否则不能兼容Go 1.21之前的版本
func Min(a, b float64) float64 {
if a < b {
return a
}
return b
}
func SaveInPNG(length, width int, sdf [][]float64, address string) error {
img := image.NewRGBA(image.Rect(0, 0, length, width))
for i := 0; i < width; i++ {
for j := 0; j < length; j++ {
//ndc空间转为像素空间
v := uint16(ndc2pixed(sdf[i][j]) * 65535)
img.Set(j, i, color.RGBA64{R: v, G: v, B: v, A: 65535})
}
}
return save(address, img)
}
func save(address string, img image.Image) error {
if address == "" {
address = "./unknow.png"
}
file, err := os.OpenFile(address, os.O_CREATE|os.O_WRONLY, 0777)
if err != nil {
return err
}
defer file.Close()
b := bufio.NewWriter(file)
err = png.Encode(b, img)
if err != nil {
return err
}
err = b.Flush()
if err != nil {
return err
}
return nil
}
// ndc空间转为像素空间 [-1,1]->[0,1]
func ndc2pixed(num float64) float64 {
return (num + 1) / 2
}
一、8SSEDT算法
二、优缺点
三、扩散原理
一、生成遮罩
二、初始化矩阵
type Point struct {
DX float64 //x偏移
DY float64 //y偏移
}
//返回距离平方,可以输入x和y的偏移量
func (p Point) DistanceSqu(addx, addy float64) float64 {
return (p.DX+addx)*(p.DX+addx) + (p.DY+addy)*(p.DY+addy)
}
var wg2 sync.WaitGroup
//异步处理
func InitMatrix(length, width int) (m [][]*Point) {
m = make([][]*Point, width)
for i := 0; i < width; i++ {
wg2.Add(1)
go func(i int) {
defer wg2.Done()
temp := make([]*Point, 0, length)
for j := 0; j < length; j++ {
temp = append(temp, &Point{})
}
m[i] = temp
}(i)
}
wg2.Wait()
return
}
一、过滤无效样本
// 检测元素是否存在
func isEsist(i, j, length, width int) bool {
if i < 0 || j < 0 {
return false
} else if i >= width || j >= length {
return false
}
return true
}
二、检测边界元素的方法
三、最短距离的计算
Min()
函数,要求返回最小值和发生变化的布尔信号
func computeShortestDistance(m [][]*init_matrix.Point, masks [][]bool, i, j, length, width int) {
var _min = math.Inf(1)
//变化信号
var ok bool
for x := i - 1; x <= i+1; x++ {
for y := j - 1; y <= j+1; y++ {
if i == x && j == y {
continue
}
//样本存在
if isEsist(x, y, length, width) {
//距离场不为0且遮罩相同
if m[x][y].DistanceSqu(0, 0) != 0 && masks[i][j] == masks[x][y] {
_min, ok = Min(_min, m[x][y].DistanceSqu(math.Abs(float64(x-i)), math.Abs(float64(y-j))))
} else if masks[i][j] != masks[x][y] {
//遮罩不相同,说明位于边界
_min, ok = Min(_min, math.Pow(math.Abs(float64(x-i))/2, 2)+math.Pow(math.Abs(float64(y-j))/2, 2))
} else {
//其余情况为false
ok = false
}
//最小值发生变化时
if ok {
if masks[x][y] == masks[i][j] {
//距离场扩散
m[i][j].DX = m[x][y].DX + math.Abs(float64(x-i))
m[i][j].DY = m[x][y].DY + math.Abs(float64(y-j))
} else if masks[x][y] != masks[i][j] {
//设置边界sdf
if !masks[i][j] {
//位于符号内部时
m[i][j].DX = -math.Abs(float64(x-i)) / 2
m[i][j].DY = -math.Abs(float64(y-j)) / 2
} else {
//位于符号外部时
m[i][j].DX = math.Abs(float64(x-i)) / 2
m[i][j].DY = math.Abs(float64(y-j)) / 2
}
}
}
}
}
}
}
func Min(_min, num float64) (float64, bool) {
//减少不必要的比较次数
if num < _min {
return num, true
}
return _min, false
}
一、有向距离场的扩散
var wg sync.WaitGroup
func GenerateSDF(length, width int, m [][]*init_matrix.Point, masks [][]bool) (res [][]float64) {
res = make([][]float64, width)
//1st pass
for i := 0; i < width; i++ {
//left to right
for j := 0; j < length; j++ {
computeShortestDistance(m, masks, i, j, length, width)
}
//right to left
for j := length - 1; j > -1; j-- {
computeShortestDistance(m, masks, i, j, length, width)
}
}
//2nd pass
for j := 0; j < length; j++ {
//top to bottom
for i := 0; i < width; i++ {
computeShortestDistance(m, masks, i, j, length, width)
}
//bottom to top
for i := width - 1; i > -1; i-- {
computeShortestDistance(m, masks, i, j, length, width)
}
}
//归一化并返回数组
normalizeParam := math.Sqrt(math.Pow(float64(length), 2) + math.Pow(float64(width), 2))
for i := 0; i < width; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
temp := make([]float64, length)
for j := 0; j < length; j++ {
//归一化
temp[j] = math.Sqrt(m[i][j].DistanceSqu(0, 0)) / normalizeParam
//设置有向距离
if !masks[i][j] {
temp[j] *= -1
}
}
res[i] = temp
}(i)
}
wg.Wait()
return
}
算法 | 测试样本 | 核数 | 耗时 |
---|---|---|---|
遍历边界法 | S500.png | 2 | 549.678694 ms/op |
遍历边界法 | S500.png | 5 | 233.342346 ms/op |
8SSEDT算法 | S500.png | 2 | 55.650370 ms/op |
8SSEDT算法 | S500.png | 5 | 51.566675 ms/op |
遍历边界法 | S1000.png | 2 | 4423.387607 ms/op |
遍历边界法 | S1000.png | 5 | 1769.491201 ms/op |
8SSEDT算法 | S1000.png | 2 | 267.859178 ms/op |
8SSEDT算法 | S1000.png | 5 | 221.036966 ms/op |
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color("_Color",COLOR)=(0,0,0)
}
SubShader
{
Tags { "RenderType"="opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed3 _Color;
fixed4 frag (v2f i) : SV_Target
{
fixed sdf = tex2D(_MainTex, i.uv).r;
if(sdf>0.5){
return fixed4(1,1,1,1);
}
return fixed4(_Color,1);
}
ENDCG
}
}
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color("_Color",COLOR)=(0,0,0)
_alpha("_alpha",Range(0,1))=0.5
}
SubShader
{
Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
float _alpha;
fixed3 _Color;
fixed4 frag (v2f i) : SV_Target
{
fixed sdf = tex2D(_MainTex, i.uv).r;
//裁剪剔除
clip(_alpha-sdf);
return fixed4(_Color,1);
}
ENDCG
}
}
一、数据存储
二、滑动窗口
三、混合原理
计算两个符号的有向距离场(A,B)
对于重叠区域和不被所有符号重叠区域不参与混合处理(只有端点值才能使用=
)
被符号A覆盖但不被符号B覆盖的区域
$$ blend =0.5-\frac{|A|}{|A|+|B|}*0.4 $$
被符号B覆盖但不被符号A覆盖的区域
$$ blend =0.9-\frac{|B|}{|A|+|B|}*0.4 $$
func BlendChar(charA [][]float64, charB [][]float64, length, width int) (res [][]float64) {
res = make([][]float64, width)
for i := 0; i < width; i++ {
temp := make([]float64, length)
for j := 0; j < length; j++ {
//c'die区域
if isChar(charA[i][j]) && isChar(charB[i][j]) {
temp[j] = 1
//非符合区域
} else if !isChar(charA[i][j]) && !isChar(charB[i][j]) {
temp[j] = 0
} else {
total := math.Abs(charB[i][j]) + math.Abs(charA[i][j])
//只在B [0.5,0.9]
if isChar(charB[i][j]) {
temp[j] = 0.9 - math.Abs(charB[i][j])/total*0.4
} else {
//只在A [0.1,0.5]
temp[j] = 0.5 - math.Abs(charA[i][j])/total*0.4
}
}
}
res[i] = temp
}
return
}
func isChar(n float64) bool {
return n <= 0
}
四、符号切换Shader实现
_Min
控制滑动窗口bias
处理:
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Min("_Min",Range(0.1,0.5))=0.1
}
SubShader
{
Tags { "Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout" }
Pass
{
CGPROGRAM
......
float _Min;
fixed4 frag (v2f i) : SV_Target
{
fixed param = tex2D(_MainTex, i.uv).r;
//裁剪参数为0的像素
clip(param-0.01);
//重合位置居中
param=param==1?0.5:param;
//滑动窗口裁剪
//下界
clip(param-_Min);
//上界,需要加bias保证符号完整输出
clip(0.4-param+_Min+0.01);
return fixed4(0,1,0,1);
}
ENDCG
}
}
FaceShadow
(一般由艺术家根据某种效果画出来)FaceShadow
,并按夹角从小到大排列(白色表示阴影面,黑色表示受光面)FaceShadow
,将两张FaceShadow
进行简单的混合平均,发现得出的图片有一块区域是灰色的,而该灰色的区域是第一张FaceShadow
过渡到第二张FaceShadow
需要变化的区域。依此类推可以得到任意一张FaceShadow
过渡到下一张FaceShadow
需要变化的区域。当有n张FaceShadow
时,这种过渡区域有$(n-1)$个facemap
,且特定的夹角要有对应的效果,则需要将$[0,1]$平均分摊到$(n-1)$个过渡区域里,即第$i$个过渡区域的取值范围为$[\frac{N-i-1}{N},\frac{N-i}{N}),N=n-1$,通过该方式确定了在某一过渡区域里的像素值的下限和上限(其中下限的值可取,上限逼近的值)FaceShadow
转化为sdf图FaceShadow
的sdf图该点的值$sdf_{pre}$(该点在FaceShadow
的黑色区域内,所以为负值,表示点到过渡区域的右边界最近的距离)FaceShadow
的sdf图该点的值$sdf_{cur}$(该点在FaceShadow
的白色区域内,所以为正值,表示点到过渡区域的左边界最近的距离)FaceShadow
的数量,$n-1$为过渡区域的数量)$$ Grayscale=\frac{n-i-2}{n-1}+\frac{abs(sdf_{cur})}{abs(sdf_{pre})+sdf_{cur}}×\frac{1}{n-1} $$
facemap
里,基本还原reference
一、生成有向距离场
facemap
通常预计算生成,使用遍历边界法生成高质量有向距离场[][]float64
)二、混合
facemap
生成原理实现逻辑,混合结果使用灰度保存alpha
通道,但会因为透明通道预处理会导致RGB
通道发生改变(//main
for i := 1; i <= 9; i++ {
BlendSDF(sdfs[i-1], sdfs[i], length, width, i, len(sdfs), resultImg)
}
//grey
func BlendSDF(pre [][]float64, cur [][]float64, length, width, idx, photoNum int, outputImg *image.RGBA) {
for i := 0; i < width; i++ {
for j := 0; j < length; j++ {
min := 1 - float64(idx)/float64(photoNum-1)
preValue := pre[i][j]
curValue := cur[i][j]
if preValue*curValue <= 0 {
temp := curValue/(math.Abs(preValue)+curValue)/float64(photoNum-1) + min
c := uint16(temp * 65535)
outputImg.SetRGBA64(j, i, color.RGBA64{R: c, G: c, B: c, A: 65535})
} else if curValue < 0 && preValue < 0 {
outputImg.SetRGBA64(j, i, color.RGBA64{R: 0, G: 0, B: 0, A: 65535})
}
}
}
}
func BlendSDFToAlpha(pre [][]float64, cur [][]float64, length, width, idx, photoNum int, outputImg *image.RGBA) {
for i := 0; i < width; i++ {
for j := 0; j < length; j++ {
_min := 1 - float64(idx)/float64(photoNum-1)
preValue := pre[i][j]
curValue := cur[i][j]
if preValue*curValue <= 0 {
alpha := curValue/(math.Abs(preValue)+curValue)/float64(photoNum-1) + _min
outputImg.SetRGBA64(j, i, *GetColor(&color.NRGBA64{R: 65535, G: 65535, B: 65535, A: uint16(alpha * 65535)}, outputImg))
} else if curValue < 0 && preValue < 0 {
outputImg.SetRGBA64(j, i, *GetColor(&color.NRGBA64{R: 65535, G: 65535, B: 65535, A: 0}, outputImg))
}
}
}
}
//非预乘像素返回RGBA
func GetColor(c *color.NRGBA64, img *image.RGBA) *color.RGBA64 {
r, g, b, a := img.ColorModel().Convert(c).RGBA()
return &color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
三、保存结果
.png
格式func SaveInPng(address string, img image.Image) error {
if address == "" {
address = "./unknow.png"
}
file, err := os.OpenFile(address, os.O_CREATE|os.O_WRONLY, 0777)
if err != nil {
return err
}
defer file.Close()
b := bufio.NewWriter(file)
err = png.Encode(b, img)
if err != nil {
return err
}
err = b.Flush()
if err != nil {
return err
}
return nil
}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。