1 Star 1 Fork 0

咸鱼程序员 / 有向距离场的生成与CG相关应用

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README

有向距离场的生成

遍历边界法

一、遍历边界法

  • 对输入图片进行遮罩处理
  • 根据遮罩图寻找边界
  • 计算各个像素的有向距离场
  • 保存有向距离场

二、优缺点

  • 优点:可异步并行处理,实现简单,使用GPU并行运算的情况下速度非常快(力大飞砖)
  • 缺点:单核情况下速度非常慢,计算时间不稳定(与边界元素个数和计算单元个数有关)

生成遮罩

  • 输入符号的灰度图,取像素r通道的值
  • 使用weight控制比较值compare的大小,当像素r通道的值小于该值时则认为像素处于符号遮罩里
  • 使用truefalse存储遮罩信息
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
}

寻找边界

  • 根据遮罩信息判断像素是否在边界,需要从横纵两方向进行遍历

    • 情况一:$白\rightarrow 黑$
    • 情况二:$黑\rightarrow 白$
  • 遍历时需要在二维数组的周围虚拟1个true元素,用于判断数组边缘元素是否属于边界

    • 遍历第一个元素时,将pre设置为true
    • 遍历第idx+1个元素时,将cur设为true
  • 执行两次遍历时会得到重复的元素需要对元素去重,golang里常用哈希map实现

  • 坐标过滤公式:$key=x*width+y$(对于不同的坐标其标记是不同的,对于相同坐标拥有相同的标记)

  • 为方便后续计算定义结构体存储预计算结果,计算前需要对xy归一化处理(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
	}
}

计算有向距离场

  • 使用坐标的两点间距离公式计算距离,需要对x和y归一化处理
    • $distance=\sqrt{(x_1-x)^2+(y_1-y)^2}=x_1^2+y_1^2-2x_1x-2y_1y+x^2+y^2$
  • 对于处于遮罩内的像素,距离值需要取反
  • 输出时需要将ndc空间的值([-1,1])转为图像空间的值([0,1]),最后输出灰度像素用于存储在图像里
  • 数组索引(i,j)映射到坐标(x,y)的公式:$(i,j)→(x,y)=(j,i)$
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
}

保存有向距离场

  • 将有向距离场存储在png图像里
  • 数组索引(i,j)映射到坐标(x,y)的公式:$(i,j)→(x,y)=(j,i)$
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算法

一、8SSEDT算法

  • 对输入图片进行遮罩处理,同时初始化矩阵存储距离信息
  • 需要2次Pass遍历处理,共遍历4次矩阵
    • 第一次Pass(纵轴从上到下遍历)
      • 横轴从左到右遍历,此次遍历主要检测像素是否在边界位置,并小范围扩散sdf
      • 横轴从右到左遍历,完成较大范围的sdf扩散
    • 第二次Pass(纵轴从下到上遍历)
      • 横轴从左到右遍历,完成更大范围的sdf扩散
      • 横轴从右到左遍历,完成全范围的sdf扩散
  • 保存有向距离场

二、优缺点

  • 优点:速度快,稳定,适用于图形中间件
  • 缺点:单核瓶颈(速度快但由于不能异步并行处理不能实时生成有向距离场)

三、扩散原理

  • 在像素的周围取3×3矩阵,并去除中间元素(8个样本)
  • 在8个样本距离场的基础上加上到中间像素的距离,选取最小值并赋值给中间像素

矩阵初始化

一、生成遮罩

  • 方法和遍历边界法的生成遮罩相同

二、初始化矩阵

  • 每个元素的结构体及其方法
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
}

计算有向距离场

一、过滤无效样本

  • 处于边界的元素不能取够8个样本,当取到不存在的样本时会导致程序出现异常,因此要过滤
// 检测元素是否存在
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
}

二、检测边界元素的方法

  • 取像素周围的8个样本,检测样本与中间像素的遮罩是否相同,不相同则像素处于边界处
  • 若像素处于边界处,基于样本位置设置不同的距离场,其中东西南北优先级比四角高
    • 东西:$(0.5,0)$
    • 南北:$(0,0.5)$
    • 四角:$(0.5,0.5)$
  • 设置边界参数x和y都不能同时为0,要求生成的距离场不能存在0值,否则往后的步骤会出现拆东墙补西墙的麻烦

三、最短距离的计算

  • 取像素周围8个样本,检测样本是否存在,不存在则跳过
  • 自定义一个Min()函数,要求返回最小值和发生变化的布尔信号
    • $(num<min)return\\ true$:可以减少切换次数,提高性能
  • 计算距离场可能的情况:
    • 像素遮罩与样本遮罩相同:添加像素到样本的偏移值并计算距离场,取最小的距离场作为像素的距离场
    • 像素遮罩与样本遮罩不相同:说明像素位于边界处,取最小的边界距离场
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
}

有向距离场的扩散

一、有向距离场的扩散

  • 使用2Pass扩散距离场
  • 输出距离场到图像时需要归一化处理
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

有向距离场在图形学的应用

符号相关

重建符号

  • 实验内容:生成一张符号的有向距离场,并使用Unity重建符号
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
    }
}

镂空符号

  • 实验内容:生成一张符号的有向距离场,并使用Unity裁剪掉非符号部分
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
    }
}

符号平滑切换

一、数据存储

  • 假设有两个符号,获取符号的有向距离场
  • 两符号的重叠区域设置为1,1默认为与所有符号重叠的参数
  • 不与两符号重叠的区域设置为0,0默认为不与所有符号重叠的参数
  • 将符号A的数据存储在$[0.1,0.5]$
  • 将符号B的数据存储在$[0.5,0.9]$
  • 产生bias的原因:shader使用$[0,1]$的范围表示一个像素的范围,而图像单通道的范围为$[0,255]$,缩放成[0,1]时会存在微小偏移

二、滑动窗口

  • 假设有一个0.5长度的滑动窗口,在该窗口范围内的参数可以允许输出像素,窗口最大参数$\alpha$
    • 当参数$\alpha$从0到0.5滑动时永远是参数小的部分先出现,参数大的部分后出现
    • 当参数$\alpha$从0.5到1滑动时永远是参数小的部分先消失,参数大的部分后消失

三、混合原理

  • 计算两个符号的有向距离场(A,B)

  • 对于重叠区域和不被所有符号重叠区域不参与混合处理(只有端点值才能使用=

    • 重叠区域设置参数1
    • 不与所有符号重叠的区域设置参数0
  • 被符号A覆盖但不被符号B覆盖的区域

    • 数据存储在$[0.1,0.5]$
    • 混合公式:(根据效果只选一种公式)

    $$ blend =0.5-\frac{|A|}{|A|+|B|}*0.4 $$

  • 被符号B覆盖但不被符号A覆盖的区域

    • 数据存储在$[0.5,0.9]$
    • 混合公式:

    $$ 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实现

    • 初始化时将滑动窗口放在A上,使用_Min控制滑动窗口
    • 裁剪顺序
      • 裁剪参数为0的像素(剔除不被符号覆盖的区域)
      • 将参数1改为0.5(还原重叠的区域)
      • 剔除低于滑动窗口下界的区域
      • 剔除高于滑动窗口上界的区域
    • bias处理:
      • 上界剔除处加长$0.01$
      • 下界没多大影响不建议修改frag或调整_Min的值$(+0.01)$,否则拆东墙补西墙
    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
        }
    }

卡通风格脸部阴影

材料准备

  • $[0,180°]$区间内不同角度下的FaceShadow(一般由艺术家根据某种效果画出来)
  • 用于渲染时白色表示阴影面,黑色表示受光面

facemap生成原理

  • 准备若干张FaceShadow,并按夹角从小到大排列(白色表示阴影面,黑色表示受光面)
  • 取出前两张FaceShadow,将两张FaceShadow进行简单的混合平均,发现得出的图片有一块区域是灰色的,而该灰色的区域是第一张FaceShadow过渡到第二张FaceShadow需要变化的区域。依此类推可以得到任意一张FaceShadow过渡到下一张FaceShadow需要变化的区域。当有n张FaceShadow时,这种过渡区域有$(n-1)$个
  • 为了得到一张从0到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的白色区域内,所以为正值,表示点到过渡区域的左边界最近的距离)
    • 过渡区域值的下限:$\frac{n-i-2}{n-1}$($n$为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通道发生改变(使用PS把颜色填回去
//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
}

空文件

简介

有向距离场的生成与CG相关应用的探索 展开 收起
Go 等 2 种语言
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
Go
1
https://gitee.com/fish_keqing/generation-and-CG-application-of-sdf.git
git@gitee.com:fish_keqing/generation-and-CG-application-of-sdf.git
fish_keqing
generation-and-CG-application-of-sdf
有向距离场的生成与CG相关应用
master

搜索帮助