PPTX解析:柔化边缘
PPT中可以对形状和图片进行柔化边缘操作(如图所示),其本质上可以看为对一个可视化对象的呈现进行视觉处理操作。通过本篇内容,我们将介绍柔化边缘的存储相关,并将说明我们如何实现近似相同的效果(因为使用的算法会导致最终生成的效果会有细微的差别)。在本案例中,将通过对一张图片进行处理,来理解柔化边缘的实现。
存储解析
PPT对图片进行柔化边缘这个行为,并不会对原图进行修改,而是通过将修改信息直接存入xml中,并在加载图片时通过计算将效果渲染出来。由于PPT不会存储一张经过该效果处理后的图片,所以第三方应用需要主动获取相关的存储信息,解析后将原图进行修改或通过着色器处理渲染效果。
首先,让我们通过存储节点来看一下PPTX中柔化边缘效果存放在哪一个节点中:
<p:pic>
......
<p:spPr>
......
<a:effectLst>
<a:softEdge rad="635000" />
</a:effectLst>
</p:spPr>
......
</p:pic>
节点名称 | 含义 | 值含义 |
---|---|---|
p:pic | 图片 | 此元素指定文档中的图片对象的存在 |
p:spPr | 形状属性 | 此元素指定图片对象具有形状属性(没错,PPT中图片和形状共用一部分属性) |
a:effectLst | 效果列表 | 存放特殊效果(阴影、发光、柔化边缘等)的列表 |
a:softEdge | 柔化边缘效果 | 柔化边缘效果,具有属性<a:softEdge /> |
该效果节点**<a:softEdge />
**的属性如下:
属性名称 | 属性含义 | 值含义 | 补充说明 |
---|---|---|---|
rad | 效果半径 | 柔化边缘的效果半径 | 英制公制单位(English Metric Unit)。用于对接厘米(“Cm”)和英寸(“Inch”)的虚拟单位。其特殊的数值设计,便于让你在转换百位以内的英寸和毫米、像素长度时,不会产生小数。 |
注:我们可以根据需要参考下面的代码将英制公制单位的值转为像素值。
/// <summary>
/// 将英制公制单位 <paramref name="emu"/> 转换为像素值
/// <param name="emu">英制公制单位的值</param>
/// <param name="dpi">设备DPI</param>
/// </summary>
public static double ToPixel(double emu, double dpi)
{
return emu / 914400 * dpi;
}
效果实现
下面我们通过两种方式实现柔化边缘的效果。
效果实现(OpenCv)
本方案仅提供OpenCv实现的思想,而不提供代码实现。因为在C#的项目中使用OpenCv的库代价太大了(体积50M+),让人直呼受不了!所以我们将在下面使用C#编写简单的算法代替将要使用到的OpenCv算法。
首先,柔化边缘所要用到的算法基于以下两个点:
- 图像腐蚀
- 图像模糊
图像的柔化边缘的本质是将图片的Alpha通道进行腐蚀,然后将Alpha通道进行模糊处理。需要注意的一点是需要将超出图像的位置Alpha通道视为透明,并且参与腐蚀运算(可以视为图像的最外圈像素的Alpha值为0)。
因此,我们只需要基于OpenCv的erode()
函数将图片的Alpha通道进行3次腐蚀操作,再通过blur()
函数将图片的Alpha通道进行3次模糊操作,就能获得和PPT柔化边缘的近似效果。但根据选择的模糊算法,生成的最终效果也会有区别。下面的实现中,我们将通过C#来实现erode()和blur()的效果。
效果实现(C#)
下面我们将通过自己的算法实现腐蚀
和模糊
操作,进而实现柔化边缘的效果。
需要注意的内容:
- 如果希望将PPT的效果和以下代码实现的近乎一致,记得将柔化半径进行转换(PPT中使用的是英制公制单位,而下面的案例使用的是像素单位)。
- 在进行效果处理时,建议将图片缩放至真实显示的尺寸再进行处理(例如原图是4K大小,实际显示的是720P的大小,那么我们应该对720P的尺寸进行计算提高运算效率)。
/// <summary>
/// PPTX柔化边缘效果
/// </summary>
public class ImageEffect
{
/// <summary>
/// 根据原始图片<paramref name="source"/>创建带有柔化边缘的图片
/// </summary>
/// <param name="bitmap">源图片</param>
/// <param name="radius">柔化半径(单位:像素)</param>
/// <returns></returns>
public Bitmap CreateSoftEdgeBitmap(Bitmap bitmap, float radius)
{
var cols = bitmap.Width;
var rows = bitmap.Height;
//克隆一个32位ARgb图片,用于读取Alpha通道
var image = bitmap.Clone(new Rectangle(0, 0, cols, rows), PixelFormat.Format32bppArgb);
SetSoftEdgeEffect(image, radius);
return image;
}
/// <summary>
/// 为原始图片<paramref name="source"/>设置柔化边缘的效果
/// </summary>
/// <param name="source">源图片(必须是32位带Alpha通道的图片)</param>
/// <param name="radius">柔化半径(单位:像素)</param>
/// <returns></returns>
public void SetSoftEdgeEffect(Bitmap source, float radius)
{
var pixelFormat = source.PixelFormat;
if (pixelFormat != PixelFormat.Format32bppArgb)
{
throw new NotSupportedException($"Unsupported image pixel format {nameof(pixelFormat)} is used.");
}
//锁定图片并拷贝图片像素
var cols = source.Width;
var rows = source.Height;
var rect = new Rectangle(0, 0, cols, rows);
var channels = System.Drawing.Image.GetPixelFormatSize(PixelFormat.Format32bppArgb) / 8;
var total = cols * rows * channels;
var data = new byte[total];
var bitmapData = source.LockBits(rect, ImageLockMode.ReadWrite, source.PixelFormat);
var iPtr = bitmapData.Scan0;
Marshal.Copy(iPtr, data, 0, total);
//通过算法设置柔化边缘效果
SetSoftEdgeEffect(data, cols, rows, channels, radius);
Marshal.Copy(data, 0, iPtr, total);
source.UnlockBits(bitmapData);
}
/// <summary>
/// 设置柔化边缘效果
/// </summary>
/// <param name="data"></param>
/// <param name="cols"></param>
/// <param name="rows"></param>
/// <param name="channels"></param>
/// <param name="radius"></param>
private void SetSoftEdgeEffect(byte[] data, int cols, int rows, int channels, float radius)
{
//创建并提供Alpha蒙层来进行腐蚀和模糊
var mask = CreateSoftEdgeAlphaMask(data, cols, rows, channels);
var offsetX = (int)Math.Round(radius / 4.0);
var offsetY = (int)Math.Round(radius / 4.0);
var size = new Size(offsetX, offsetY);
//腐蚀操作
mask = AlphaErode(mask, size, 3);
//模糊操作
mask = AlphaBlur(mask, size, 3);
//应用Alpha蒙层数据
ApplySoftEdgeAlphaMask(data, mask, cols, rows, channels);
}
/// <summary>
/// 创建Alpha通道蒙层数据
/// </summary>
/// <param name="data">图像原始数据</param>
/// <param name="cols">图像宽度</param>
/// <param name="rows">图像高度</param>
/// <param name="channels">图像通道数</param>
/// <returns></returns>
private byte[,] CreateSoftEdgeAlphaMask(byte[] data, int cols, int rows, int channels)
{
//根据宽高设置一个蒙层数组
var masks = new byte[cols, rows];
//需要考虑大小端
var isLittleEndian = BitConverter.IsLittleEndian;
for (var row = 0; row < rows; row++)
{
for (var col = 0; col < cols; col++)
{
var indexOffset = (row * cols + col) * channels;
var alpha = isLittleEndian ? data[indexOffset + 3] : data[indexOffset + 0];
masks[col, row] = alpha == 0 ? (byte)0 : byte.MaxValue;
}
}
return masks;
}
/// <summary>
/// 应用Alpha通道蒙层数据
/// </summary>
/// <param name="data">图像原始数据</param>
/// <param name="mask">图像Alpha蒙层</param>
/// <param name="cols">图像宽度</param>
/// <param name="rows">图像高度</param>
/// <param name="channels">图像通道数</param>
/// <returns></returns>
private void ApplySoftEdgeAlphaMask(byte[] data, byte[,] mask, int cols, int rows, int channels)
{
//需要考虑大小端
var isLittleEndian = BitConverter.IsLittleEndian;
for (var row = 0; row < rows; row++)
{
for (var col = 0; col < cols; col++)
{
var indexOffset = (row * cols + col) * channels;
var index = isLittleEndian ? indexOffset + 3 : indexOffset;
//根据蒙层设置Alpha
var alpha = (byte)(mask[col, row] / 255.0d * data[index]);
data[index] = alpha;
}
}
}
/// <summary>
/// 对Alpha蒙层进行腐蚀操作
/// </summary>
/// <param name="sourceMask">输入蒙层数据</param>
/// <param name="size">腐蚀操作卷积核大小</param>
/// <param name="iteration">连续腐蚀次数</param>
/// <returns>输出蒙层数据</returns>
private byte[,] AlphaErode(byte[,] sourceMask, Size size, uint iteration)
{
var offsetX = size.Width;
var offsetY = size.Height;
var cols = sourceMask.GetLength(0);
var rows = sourceMask.GetLength(1);
var erodeMask = new byte[cols, rows];
for (var i = 0; i < iteration; i++)
{
var target = new byte[cols, rows];
var mask = sourceMask;
//下面的卷积操作会尽可能减少不必要的运算过程
Parallel.For(offsetY, rows - offsetY, row =>
{
var isNeedInitialize = true;
var blackPointCols = new List<int>();
for (var col = offsetX; col < cols - offsetX; col++)
{
var minCol = col - offsetX;
var maxCol = col + offsetX;
var minRow = row - offsetY;
var maxRow = row + offsetY;
if (isNeedInitialize)
{
for (var x = minCol; x <= maxCol; x++)
{
for (var y = minRow; y < maxRow; y++)
{
if (mask[x, y] == 0)
{
blackPointCols.Add(x);
break;
}
}
}
isNeedInitialize = false;
}
else
{
blackPointCols.Remove(minCol - 1);
for (var y = minRow; y <= maxRow; y++)
{
if (mask[maxCol, y] == 0)
{
blackPointCols.Add(maxCol);
break;
}
}
}
if (blackPointCols.Count == 0)
{
target[col, row] = byte.MaxValue;
}
}
});
sourceMask = target;
erodeMask = target;
}
return erodeMask;
}
/// <summary>
/// 对Alpha蒙层进行模糊操作(使用归一化框过滤器模糊图像,是一种简单的模糊函数,是计算每个像素中对应核的平均值)
/// </summary>
/// <param name="sourceMask">输入蒙层数据</param>
/// <param name="size">模糊操作卷积核大小</param>
/// <param name="iteration">连续腐蚀次数</param>
/// <returns>输出蒙层数据</returns>
private byte[,] AlphaBlur(byte[,] sourceMask, Size size, uint iteration)
{
var offsetX = size.Width;
var offsetY = size.Height;
var cols = sourceMask.GetLength(0);
var rows = sourceMask.GetLength(1);
var blurMask = new byte[cols, rows];
for (var i = 0; i < iteration; i++)
{
var target = new byte[cols, rows];
var mask = sourceMask;
//下面的卷积操作会尽可能减少不必要的运算过程
Parallel.For(0, rows, row =>
{
var isNeedInitialize = true;
var valueCache = new Dictionary<int, int>();
for (var col = 0; col < cols; col++)
{
var minCol = col - offsetX;
var maxCol = col + offsetX;
var minRow = row - offsetY;
var maxRow = row + offsetY;
var count = (offsetX * 2 + 1) * (offsetY * 2 + 1);
if (count == 0) count = 1;
if (isNeedInitialize)
{
for (var x = minCol; x <= maxCol; x++)
{
var value = 0;
if (x > 0 && x < cols)
{
for (var y = minRow; y < maxRow; y++)
{
if (y > 0 && y < rows)
{
value += mask[x, y];
}
}
}
valueCache.Add(x, value);
}
isNeedInitialize = false;
}
else
{
var value = 0;
valueCache.Remove(minCol - 1);
if (maxCol > 0 && maxCol < cols)
{
for (var y = minRow; y < maxRow; y++)
{
if (y > 0 && y < rows)
{
value += mask[maxCol, y];
}
}
}
valueCache.Add(maxCol, value);
}
var targetValue = valueCache.Values.Sum() / (double)count;
target[col, row] = (byte)Math.Round(targetValue);
}
});
sourceMask = target;
blurMask = target;
}
return blurMask;
}
}
实现的效果
GitHub项目仓库
如果希望参考完整案例,请参考下面的项目:
柔化边缘案例
附加
如果您有更好的方案欢迎留言分享!
我的博客会首发于 个人博客-仙尘阁,而 CSDN 会从其中精选发布,但是一旦发布了就很少更新。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 晓嗔戈 (包含链接: https://imxcg.blog.csdn.net/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 。