笔者之前发表的音视频文章,有图像的处理,音频的重采样等等,都属于入门级别。通过阅读它们,读者能对音视频有了了解。可在G itee上面回顾。
2023 年,笔者将整理下 关于 OpenGLES 的实验室系列 并进行发表。首先为读者带来2D篇的系列,它大多是 x y 坐标,不涉及 z 坐标,所以用 2D篇。内容上,它不对 OpenGLES 的基础知识进行细说与讨论。但如果对 OpenGLES 不了解或者了解一点,仍可通过本实验室系列了解 OpenGLES。它旨在激起读者的兴趣,扩展到实际的应用上。总的来说,这些实验 & Demo 将是额外的,即对基础学习的补充,通过这些它们的实践和运用,能让读者进一步了解 OpenGLES 。
本次实验室带来的是《OpenGLES 实验室之2D篇 第二弹 の 瘦脸修图》。
如果读者还记得之前其他作者发过的一篇文章《如何实现图片的扭曲效果,窗帘效果及仿真水波纹效果,修图技术之瘦身瘦脸效果的实现(android-drawBitmapMesh)》,是介绍 Android 的 drawBitmapMesh,可以快速实现图像扭曲效果的API。那时笔者看完后,想想 iOS 也可以有,基于 OpenGLES 封装出类似的 API。因此有了本次实验 & Demo。
通过手势从脸部边缘向内滑动,Demo 效果比较一般,因为网格的细粒度不够,所以拉拽影响区域大,导致变形夸张。Demo 使用的人物图片是从《Android:修图技术之瘦脸效果的实现(drawBitmapMesh) 》 里面的,如有侵权可以告知笔者删除。
Git 地址:QHDrawBitmapMeshMan: iOS:OpenGLES 实验室之2D篇 第二弹 の 瘦脸修图
效果
这是瘦脸修图前后效果(不得不说修图真的是个技术活😂):
修图前 | 修图后 |
// 计算网格的顶点在 view 上的实际坐标
for (int i = 0; i < h + 1; i++) {
float fy = bmHeight * i / h;
for (int j = 0; j < w + 1; j++) {
float fx = bmWidth * j / w;
[verts addObject:@(fx)];
[verts addObject:@(fy)];
}
}
// CG 绘制网格
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
}
// idx 为 100 个网格的编号
// 由于是 10 * 10,所以下面指定格子进行测试
if (idx == 97) {// 将 97 号网格的纹理都绘制成 蓝色
gl_FragColor = vec4(0, 0, 1, 1);
}
else if (idx == 1) {// 将 1 号网格的纹理都绘制成 绿色
gl_FragColor = vec4(0, 1, 0, 1);
}
else {// 其网格的绘制成原始纹素
gl_FragColor = vec4(texture2D(inputImageTexture, v_texcoord).rgb, 1);
}
- (void)warp2Start:(CGPoint)sp end:(CGPoint)ep {
NSMutableArray *v = self.verts;
CGFloat width = self.frame.size.width;
CGFloat height = self.frame.size.height;
float w = width/R;
float h = height/R2;
// 满足条件的区域半径
CGFloat r = MAX(w, h) + 0.5/*敏感值*/;
float dse_x = ep.x - sp.x;
float des_y = ep.y - sp.y;
// 求出滑动的直线距离
float des = sqrtf(dse_x*dse_x + des_y*des_y);
for (int i = 0; i < v.count; i += 2) {
float x = [v[i] floatValue];
float y = [v[i + 1] floatValue];
// 边角区域不修改,保证图像依然是矩形
if (x == 0 || y == 0 || x == width || y == height) {
continue;
}
else {
float xx = x - sp.x;
float yy = y - sp.y;
// 该点是否在满足的区域,先判断是否在矩形区域,再判断圆形区域
if (fabsf(xx) <= r && fabsf(yy) <= r) {
float dxy = sqrtf(xx*xx + yy*yy);
if (dxy <= r) {
// 满足条件的计算出偏移值,并控制偏移值的比例(最大值为半个区域距离)
float dr = (r * r - dxy);
float e = dr * dr / ((dr + des * des) * (dr + des * des));
float tx = MIN(e * dse_x, w/2);
float ty = MIN(e * des_y, h/2);
v[i] = @(x + tx);
v[i + 1] = @(y + ty);
}
}
}
}
// ... ...
}
mediump float a;
mediump float b;
mediump float c;
mediump float d;//分别存四个向量的计算结果;
a = (x[1] - x[0])*(fy - y[0]) - (y[1] - y[0])*(fx - x[0]);
b = (x[2] - x[1])*(fy - y[1]) - (y[2] - y[1])*(fx - x[1]);
c = (x[3] - x[2])*(fy - y[2]) - (y[3] - y[2])*(fx - x[2]);
d = (x[0] - x[3])*(fy - y[3]) - (y[0] - y[3])*(fx - x[3]);
if ((a >= 0.0 && b >= 0.0 && c >= 0.0 && d >= 0.0) || (a <= 0.0 && b <= 0.0 && c <= 0.0 && d <= 0.0)) {
idx = i + j * w;
break;
}
std::tuple<float, float> PerspectiveTransform::transform(float x, float y) {
float denominator = a13 * x + a23 * y + a33;
float xx = (a11 * x + a21 * y + a31) / denominator;
float yy = (a12 * x + a22 * y + a32) / denominator;
return std::make_tuple(xx, yy);
}
mediump mat3 pt = h_pt[idx];
mediump float x = v_texcoord.x;
mediump float y = v_texcoord.y;
mediump float denominator = pt[0][2] * x + pt[1][2] * y + pt[2][2];
mediump float xx = (pt[0][0] * x + pt[1][0] * y + pt[2][0]) / denominator;
mediump float yy = (pt[0][1] * x + pt[1][1] * y + pt[2][1]) / denominator;
gl_FragColor = vec4(texture2D(inputImageTexture, vec2(xx, yy)).rgb, 1);
ffmpeg -i test00.png -vcodec rawvideo -pix_fmt rgb24 head.rgb
或
ffmpeg -i test00.png -vcodec rawvideo -pix_fmt rgba head_rgba.rgb