基于扫描算法获取psd图层轮廓

前言

本文主要介绍如何用 四向扫描算法 来获取 psd 图层的轮廓信息,也算是补充了上一篇文章使用psd.js将psd路径转成svg格式里面获取轮廓功能的不足之处,上一篇只能获取形状图层的轮廓,这难免有时候不太方便,所有本文通过算法解析所有像素,来读取简单的图层外轮廓

获取图层像素数据

在上一篇已经讲过了要如何导入 psd 文件并且获取具体的图层数据,所有在这里就不再多说,在我们拿到具体的某一个图层的数据之后,如果当前不是一个形状图层,那么我们就可以通过其他方式来获取轮廓,首先需要获取一下这个图层的像素数据

我们通过 pixelData 来获取图层的像素数据。在 PSD (Photoshop Document) 文件中, pixelData 通常指的是图像的像素数据,它存储了每个像素的颜色和透明度信息 。在 Photoshop 中,每个图层都包含自己的 pixelData,这些数据共同构成了最终的图像效果。

在实际的图层里面,我们可以通过 node.layer.image.pixelData 来获取当前这个图层的像素数据

这个数据在经过 psd.js 的处理之后,每个像素的各通道值在 pixelData 中是线性排列的,索引计算方式为:(y \* width + x) \* channels + channelIndex

新建图片,转化图层像素数据

通过 canvascreateImageData 方法,我们可以创建一个空的 ImageData 对象,然后经过处理,将图层的数据都渲染到这张图片上面

针对 ImageData 对象来说,最关键的就是 ImageData.data,一个 Uint8ClampedArray 格式的存储对象。

然后我们通过方法转化 node.layer.image.pixelDataImageData.data 数据

js 复制代码
const pixelData = node.layer.image.pixelData

// 将pixelData复制到imgData
// 注意:PSD格式可能是分通道存储的,需要转换为RGBA格式
let imgDataIndex = 0
for (let y = 0; y < height; y++) {
	for (let x = 0; x < width; x++) {
		// 设置RGBA值
		const pixelIndex = (y * width + x) * channels

		// 设置RGB通道
		for (let c = 0; c < Math.min(3, channels); c++) {
			imgData.data[imgDataIndex + c] = pixelData[pixelIndex + c] || 0
		}

		// 设置Alpha通道 (如果没有Alpha通道,设置为255,完全不透明)
		imgData.data[imgDataIndex + 3] =
			channels > 3 ? pixelData[pixelIndex + 3] : 255

		imgDataIndex += 4
	}
}

console.log(imgData)

通过四向扫描算法获取图片的轮廓数据

首先我们先来了解一下 ImageData.data 也就是 Uint8ClampedArray 类型,这个类型有三个特点:

  1. 数据排列方式:按照 RGBA 四个值一组排列,每个像素占用连续的 4 个元素
    • 索引 0, 4, 8... 存储红色通道 (R) 值
    • 索引 1, 5, 9... 存储绿色通道 (G) 值
    • 索引 2, 6, 10... 存储蓝色通道 (B) 值
    • 索引 3, 7, 11... 存储透明度通道 (A) 值
  2. 值范围:每个通道的值范围是 0-255
    • 对于 Alpha 通道,0 表示完全透明,255 表示完全不透明
  3. 数据类型:Uint8ClampedArray 确保所有值都被限制在 0-255 范围内

简单理解这是一个非常长的数组,里面存储了每一个点位的颜色,透明度 等信息,然后我们就可以通过图片的宽度或者长度来找到 (x,y) 这个点位在数组里面的哪个位置,然后我们只是需要判断当前的点位是否为透明,所有就有了以下函数

js 复制代码
// 判断像素是否不透明
const isOpaque = (x: number, y: number): boolean => {
	if (x < 0 || x >= width || y < 0 || y >= height) return false
	const idx = (y * width + x) * 4
	return data[idx + 3] > 0 // Alpha通道大于0表示不透明
}

然后四向扫面,就是通过上下左右四个方向逐行扫面,碰到不透明的元素就退出,记录下点位,当然这种方法就只能判断一些简单图形,如果需要判断复杂图形的轮廓,那就需要修改这个算法

js 复制代码
// 1. 从上到下扫描(获取上边界)
for (let x = 0; x < width; x++) {
	for (let y = 0; y < height; y++) {
		if (isOpaque(x, y)) {
			contourPoints.push({ x, y })
			break // 找到第一个不透明像素后停止向下扫描
		}
	}
}

// 2. 从右到左扫描(获取右边界)
for (let y = 0; y < height; y++) {
	for (let x = width - 1; x >= 0; x--) {
		if (isOpaque(x, y)) {
			// 避免重复添加已有的点
			if (!contourPoints.some((p) => p.x === x && p.y === y)) {
				contourPoints.push({ x, y })
			}
			break // 找到第一个不透明像素后停止向左扫描
		}
	}
}

// 3. 从下到上扫描(获取下边界)
for (let x = width - 1; x >= 0; x--) {
	for (let y = height - 1; y >= 0; y--) {
		if (isOpaque(x, y)) {
			// 避免重复添加已有的点
			if (!contourPoints.some((p) => p.x === x && p.y === y)) {
				contourPoints.push({ x, y })
			}
			break // 找到第一个不透明像素后停止向上扫描
		}
	}
}

// 4. 从左到右扫描(获取左边界)
for (let y = height - 1; y >= 0; y--) {
	for (let x = 0; x < width; x++) {
		if (isOpaque(x, y)) {
			// 避免重复添加已有的点
			if (!contourPoints.some((p) => p.x === x && p.y === y)) {
				contourPoints.push({ x, y })
			}
			break // 找到第一个不透明像素后停止向右扫描
		}
	}
}

console.log('四向扫描获取的轮廓点数量:', contourPoints.length)

到这里为止我们就获得了所有不透明的外轮廓点位,但是我们还可以进一步的优化这些点位数据,通过 Ramer-Douglas-Peucker 算法 对所有的点位进行简化:

js 复制代码
// 对轮廓点进行排序,确保它们按照顺时针或逆时针顺序排列
const sortContourPoints = (points: ContourPoint[]): ContourPoint[] => {
	if (points.length <= 2) return points

	// 计算轮廓的中心点
	let sumX = 0,
		sumY = 0
	for (const point of points) {
		sumX += point.x
		sumY += point.y
	}
	const centerX = sumX / points.length
	const centerY = sumY / points.length

	// 按照相对于中心点的角度排序
	return [...points].sort((a, b) => {
		const angleA = Math.atan2(a.y - centerY, a.x - centerX)
		const angleB = Math.atan2(b.y - centerY, b.x - centerX)
		return angleA - angleB
	})
}

// 简化轮廓点,移除过于密集的点
const simplifyContour = (
	points: ContourPoint[],
	tolerance: number,
): ContourPoint[] => {
	if (points.length <= 2) return points

	// 先对点进行排序
	const orderedPoints = sortContourPoints(points)

	// 使用Ramer-Douglas-Peucker算法简化轮廓
	const result: ContourPoint[] = []

	// 计算点到线段的距离
	const pointToLineDistance = (
		p: ContourPoint,
		start: ContourPoint,
		end: ContourPoint,
	): number => {
		const { x, y } = p
		const { x: x1, y: y1 } = start
		const { x: x2, y: y2 } = end

		// 如果起点和终点重合,则计算点到点的距离
		if (x1 === x2 && y1 === y2) {
			return Math.sqrt((x - x1) ** 2 + (y - y1) ** 2)
		}

		// 计算点到线段的距离
		const A = x - x1
		const B = y - y1
		const C = x2 - x1
		const D = y2 - y1

		const dot = A * C + B * D
		const lenSq = C * C + D * D
		let param = -1

		if (lenSq !== 0) param = dot / lenSq

		let xx, yy

		if (param < 0) {
			xx = x1
			yy = y1
		} else if (param > 1) {
			xx = x2
			yy = y2
		} else {
			xx = x1 + param * C
			yy = y1 + param * D
		}

		const dx = x - xx
		const dy = y - yy

		return Math.sqrt(dx * dx + dy * dy)
	}

	// 递归简化
	const simplifyRDP = (start: number, end: number) => {
		if (end - start <= 1) return

		let maxDistance = 0
		let maxIndex = start

		for (let i = start + 1; i < end; i++) {
			const distance = pointToLineDistance(
				orderedPoints[i],
				orderedPoints[start],
				orderedPoints[end],
			)

			if (distance > maxDistance) {
				maxDistance = distance
				maxIndex = i
			}
		}

		if (maxDistance > tolerance) {
			simplifyRDP(start, maxIndex)
			result.push(orderedPoints[maxIndex])
			simplifyRDP(maxIndex, end)
		}
	}

	// 添加第一个点
	result.push(orderedPoints[0])

	// 简化中间点
	simplifyRDP(0, orderedPoints.length - 1)

	// 添加最后一个点
	result.push(orderedPoints[orderedPoints.length - 1])

	// 确保轮廓是闭合的
	if (
		result.length > 2 &&
		(result[0].x !== result[result.length - 1].x ||
			result[0].y !== result[result.length - 1].y)
	) {
		result.push({ ...result[0] })
	}

	return result
}

对比一下简化前后的点位数据

然后接下去的流程就比较简单了,已经获取到了每一个点位的坐标,只需要通过位置来生成对应的 path 数据,就能够得到 svg 了。

js 复制代码
/ 将轮廓点转换为SVG路径
const contourToPath = (points: ContourPoint[]): string => {
	if (points.length < 3) return ''

	// 使用简单的折线路径
	let pathData = `M ${points[0].x},${points[0].y}`

	for (let i = 1; i < points.length; i++) {
		pathData += ` L ${points[i].x},${points[i].y}`
	}

	// 闭合路径
	if (
		points[0].x !== points[points.length - 1].x ||
		points[0].y !== points[points.length - 1].y
	) {
		pathData += ' Z'
	}

	return `<path d="${pathData}" fill="none" stroke="black" stroke-width="1" />`
}

总结

本文通过像素点分析的方式解析图层的轮廓,但是使用的扫描方式相对简单,只能用来解析一些简单的图形,如果需要扫描复杂图形可以考虑上一篇文章中的创建形状路径或者重写扫面算法。

引用

使用psd.js将psd路径转成svg格式

相关推荐
军训猫猫头几秒前
100.Complex[]同时储存实数和虚数两组double的数组 C#例子
算法·c#·信号处理
int型码农39 分钟前
数据结构第八章(五)-外部排序和败者树
c语言·数据结构·算法·排序算法
好易学·数据结构1 小时前
可视化图解算法52:数据流中的中位数
数据结构·算法·leetcode·面试·力扣·笔试·牛客
qq_2786672861 小时前
ros中相机话题在web页面上的显示,尝试js解析sensor_msgs/Image数据
前端·javascript·ros
烛阴1 小时前
JavaScript并发控制:从Promise到队列系统
前端·javascript
dying_man1 小时前
LeetCode--35.搜索插入位置
算法·leetcode
zhangxingchao1 小时前
关于《黑马鸿蒙5.0零基础入门》课程的总结
前端
zhangxingchao1 小时前
Flutter的Widget世界
前端
&活在当下&2 小时前
element plus 的树形控件,如何根据后台返回的节点key数组,获取节点key对应的node节点
javascript·vue.js·element plus
$程2 小时前
Vue3 项目国际化实践
前端·vue.js