前言
最近网上看了一个叫BadApple的字符串动画,很有意思。作者是用python+OpenCv实现的转换流程。而OpenCv又是一个开源项目,我们为什么不能把这个效果移植到iOS手机上呢?
正文
首先先放一下效果图:
本次文章提及的相关内容demo地址
视频网盘地址:
我们想实现这么一个效果,自然不能只是纯粹地去翻写代码,我们首先得知道原理,才能知道为什么要这么做?
原理
ps: 这里的原理不一定那么专业,其中结合了本人的理解,如有错误,还望指出
首先,视频的本质是声音+图像,这里因为声音相关内容并非我们的研究方向,所以暂时省略。
所以,我们要将视频转换成字符串动画,首先就需要知道一点,图像是如何转成字符串的
图像转成字符串
我们在研究如何转化前,还需要大概了解下图像的存储。
图像的存储
一个图像是由许许多多的小方格组成的,这些小方格都有明确的位置和颜色,这些小方格的位置与颜色最终决定了图像的样子,这些小方格也被称作像素
图像的显示
图像在屏幕上的显示,便是将图像的每一个像素点的颜色渲染在屏幕上的对应位置处,我们就看到对应图片了。
计算机中一般采用Bitmap(位图),来显示图像
Bitmap一般采用二维数组进行存取,二维数组中储存的便是该像素的颜色。
颜色
计算机中颜色一般是采用RGB通道(在iOS中,我没有用过其他),也就是说一个颜色是由三种基础颜色(红Red、绿Green、蓝Blue)通道叠加生成的。所有颜色均可由三种颜色通道的不同强度组成。
我们通常用到图像一般是8位图像,在这里也可以说是三种颜色的强度可以从0 ~ (2^8 - 1 = 255)一共256个强度等级,而可以显示的总颜色数量就是256^3。
至此,图像就由一个具象的画面,变成了抽象的数据了。
灰度图像
因为彩色图像使用三个颜色通道描述一个像素点的颜色,我们也可以认为一张彩色图像拥有三个维度。
但是,如果我们希望以字符串显示的话,我们只能展示其中的一个维度(这里不考虑使用富文本时候字符串可以展示多个颜色的情况)。
所以我们需要对彩色图像进行降维处理,也就是通过某种算法使得我们可以使用一个颜色通道便可以描述出图像的特点。
灰度图像正是为了满足我们的这种需求而出现的。
定义
转换公式
- 浮点算法:Gray = R * 0.3 + G * 0.59+B * 0.11
- 整数方法:Gray = (R * 30 + G * 59 + B * 11)/100
- 移位方法:Gray = (R * 76 + G * 151 + B * 28)>>8;
- 平均值法:Gray =(R + G + B)/ 3;
- 仅取绿色:Gray = G;
为什么要选取灰度图像?
字符串图像,意味着图像的每一个像素均是使用一个字符进行表示的。而字符是一个一维的变量(在这种条件下,我们忽略了每一个字符的字体、颜色、字号差别)。所以我们需要一个同样是一维的图像——灰度图像
如何将灰度像素转换成字符串
这里提供一个灰度值对应的字符串表示数组:
@[@"$", @"@", @"B", @"%", @"8", @"&", @"W", @"M", @"#", @"*", @"o", @"a", @"h", @"k", @"b", @"d", @"p", @"q", @"w", @"m", @"Z", @"0", @"o", @"Q", @"L", @"C", @"J", @"U", @"Y", @"X", @"z", @"c", @"v", @"u", @"n", @"x", @"r", @"j", @"f", @"t", @"/", @"\\", @"|", @"(", @")", @"1", @"{", @"}", @"[", @"]", @"?", @"-", @"_", @"+", @"~", @"<", @">", @"i", @"!", @"l", @"I", @";", @":", @",", @"\"", @"^", @"
", @"'", @".", @" "]`
我们根据对应灰度图像的每一个像素的Gray值,获取对应Gray值的百分比(即 Gray * 1.f / 255.0
)。然后在根据百分比,获得对应的字符,拼接成一个完整的字符串。这个字符串,便是字符串图像了
流程图解
过程实现
我们已经分析过基础原理了,接下来开始编写应用吧
首先当然是视频了,如果想直接观看原视频,请点击这里
OpenCV 配置
什么是OpenCV?
iOS 配置OpenCV
一共有三种方法可以配置OpenCV
- 直接从源码编译(不过多介绍,因为我也不是很了解)
- 从CocoaPod导入(因为折腾起来感觉很费劲,因此也不过多介绍)
- 下载打包好的iOS Framework
这里我只介绍第3种
我们创建好对应工程后,将解压过的Framework拖入到项目后我们需要添加如下依赖库:
AVFoundation.framework
AssetsLibrary.framework
CoreMedia.framework
CoreVideo.framework
复制代码
这里是我目前为止需要的依赖库,用以保证我们的项目编译不报错
处理图像
首先,因为OpenCV是c++ 代码,所以我们所有希望和OpenCV进行交互的类,后缀均需要改成.mm
文件以使编译器识别c++ 语法
接下来导入头文件
#import <opencv2/opencv.hpp>
#import <opencv2/imgproc/types_c.h>
#import <opencv2/imgcodecs/ios.h>
#import <opencv2/videoio/cap_ios.h>
#import <opencv2/core/core.hpp>
#import <opencv2/highgui/highgui.hpp>
复制代码
这些头文件需要在我们导入其他文件之前导入,也就是说需要写在最上面,否则会报错。
接下来我按照上面的步骤依次写出图像转字符串操作:
// 先声明一个结构体用以标明长和宽
typedef struct{
int width, height;
}SizeT;
复制代码
- 图片缩放
/// 根据给定大小缩放图片
- (UIImage *)resizeImage:(UIImage *)image withSize:(SizeT)size {
/// cv::Mat 对象为对应的图片的二维数组对象,我们可以很方便使用角标进行数组操作
cv::Mat cvImage;
/// 将UIImage对象转换为对应cv::Mat对象
UIImageToMat(image, cvImage);
cv::Mat reSizeImage;
/// 重新赋值大小
cv::resize(cvImage, reSizeImage, cv::Size(size.width, size.height));
/// 释放
cvImage.release();
/// 生成新的UIImage
UIImage *nImage = MatToUIImage(reSizeImage);
/// 释放
reSizeImage.release();
return nImage;
}
复制代码
- 彩色图转换为灰度图片
- (UIImage *)grayImage:(UIImage *)image {
cv::Mat cvImage;
UIImageToMat(image, cvImage);
cv::Mat gray;
// 将图像转换为灰度显示
cv::cvtColor(cvImage, gray, CV_RGB2GRAY);
cvImage.release();
// 将灰度图片转成UIImage
UIImage *nImage = MatToUIImage(gray);
gray.release();
return nImage;
}
复制代码
- 将灰度图片转换为字符串
- (NSString *)convertImage:(UIImage *)image {
cv::Mat gray;
UIImageToMat(image, gray);
// 获取一共多少列
int row = gray.rows;
// 获取一共多少行
int col = gray.cols;
// 初始化字符串数组 用来存储图片的每一行
NSMutableArray <NSString *>* array = [NSMutableArray arrayWithCapacity:row];
// 给定字符串灰度对应值
NSArray *pixels = @[@"$", @"@", @"B", @"%", @"8", @"&", @"W", @"M", @"#", @"*", @"o", @"a", @"h", @"k", @"b", @"d", @"p", @"q", @"w", @"m", @"Z", @"0", @"o", @"Q", @"L", @"C", @"J", @"U", @"Y", @"X", @"z", @"c", @"v", @"u", @"n", @"x", @"r", @"j", @"f", @"t", @"/", @"\\", @"|", @"(", @")", @"1", @"{", @"}", @"[", @"]", @"?", @"-", @"_", @"+", @"~", @"<", @">", @"i", @"!", @"l", @"I", @";", @":", @",", @"\"", @"^", @"`", @"'", @".", @" "];;
for (int i = 0 ; i < row; i ++) {
NSMutableArray <NSString *>*item = [NSMutableArray arrayWithCapacity:col];
for (int j = 0; j < col; j ++) {
// 取出对应灰度值
int temp = gray.at<uchar>(i, j);
// 计算灰度百分比
CGFloat percent = temp / 255.f;
// 根据百分比取出对应的字符
int totalCount = (pixels.count - 1) * percent;
// 加入到字符串数组里
[item addObject:pixels[totalCount]];
}
// 将数组转成字符串
[array addObject:[item componentsJoinedByString:@" "]];
}
gray.release();
// 返回分好行后的字符串
return [array componentsJoinedByString:@"\n"];
}
复制代码
这些步骤操作完毕后,我们剩下的最后一个步骤,就是将字符串显示在Label上。
一些iOS上的坑
- 字符串图像首先得需要保证是等宽字体(因为非等宽字体将会产生图像移位,显示错乱),但是系统标准字体又是非等宽字体。
解决方案: 采用等宽字体Courier
- 在iOS上,UILabel如果采用居中设置的话,字符串渲染过程中可能出现撕裂
解决方案: UILabel请全部采用默认设置(左对齐),如果需要居中,最好采用AutoLayout将Label设置居中
处理视频
视频的处理其实就是将视频的每一帧转换成图像,然后将图像转换成字符串。播放就是按照视频原有的每帧间隔将字符串显示在屏幕上,于是就变成动画了。
具体细节就不过多赘述了,可以看对应demo
结尾
图像处理其实是很高深的学科,这里我只是很简单的将图像转换为字符串。