1)Prisma — 图片风格迁移的鼻祖:
照片可以记录生活的瞬间,变成一幅幅的回忆;而 Prisma 则是可以让瞬间的回忆变成永恒的名画!我们平常用手机随意拍出来的照片效果看起来都很普通,而通过 Prisma 处理之后,你一定会惊叹于它的神奇!
Pisma 是一款来自俄罗斯的照片美化应用,和「彩云天气」那样,借助人工智能技术将自身的能力提升到另一个层次。“Pisma 运用了综合人工神经网络技术(neural networks)和人工智能技术,学习模仿各种著名绘画大师和主要流派的艺术风格,然后对你的照片进行全智能的风格化处理”。
也就是说,每个滤镜最后所呈现的照片艺术效果,都是 Pisma “模仿”过去那些世界伟大艺术家们的风格,对你的照片进行 AI 智能分析之后而重绘出来的。不仅在技术上让人惊叹,实际产出的照片效果之佳,也同样让人为之惊叹!!
2)Tensorflow — 谷歌开源,实现图片风格迁移:
官方Demo地址:https://github.com/tensorflow/tensorflow/tree/master/tensorflow/examples/android
谷歌利用tensorflow一共实现了Android相关的例子有三个 —— 物体定位,行人识别,风格迁移,如图:
谷歌这里给出的实例是处理相机获得实时预览图。本篇文章的目的,是将风格迁移的效果运用到图片上。
谷歌训练的模型文件中一共有26种风格迁移的效果,下面来一一实现。
1)build.gradle中添加tensorflow的依赖:
implementation 'org.tensorflow:tensorflow-android:+'
2)assets文件夹下放置谷歌训练好的模型文件:
谷歌pb模型文件(文末有项目地址)3)对TensorFlowInferenceInterface类进行实例化:
private static final String MODEL_FILE = "stylize_quantized.pb";
... ...
inferenceInterface = new TensorFlowInferenceInterface(getAssets(), MODEL_FILE);
这里使用assets文件下的模型文件完成该类的实例化。
4)正式处理一张图片:
private void execute(final int position) {
handler = new Handler(getMainLooper());
inferenceInterface = new TensorFlowInferenceInterface(getAssets(), MODEL_FILE);
runInBackground(new Runnable() {
@Override
public void run() {
croppedBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.beauty_1);
croppedBitmap = Bitmap.createScaledBitmap(croppedBitmap, desiredSize, desiredSize, false);
imageView.setImageBitmap(croppedBitmap);
cropCopyBitmap = Bitmap.createBitmap(croppedBitmap);
final long startTime = SystemClock.uptimeMillis();
stylizeImage(croppedBitmap, position);
lastProcessingTimeMs = SystemClock.uptimeMillis() - startTime;
textureCopyBitmap = Bitmap.createBitmap(croppedBitmap);
done();
}
});
}
private void done() {
imageView.setImageBitmap(textureCopyBitmap);
}
private int desiredSize = 512;
private final float[] styleVals = new float[NUM_STYLES];
private int[] intValues = new int[desiredSize * desiredSize];
private float[] floatValues = new float[desiredSize * desiredSize * 3];
private void stylizeImage(Bitmap bitmap, int model) {
bitmap.getPixels(intValues, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
for (int i = 0; i < intValues.length; ++i) {
final int val = intValues[i];
floatValues[i * 3] = ((val >> 16) & 0xFF) / 255.0f;
floatValues[i * 3 + 1] = ((val >> 8) & 0xFF) / 255.0f;
floatValues[i * 3 + 2] = (val & 0xFF) / 255.0f;
}
for (int i = 0; i < NUM_STYLES; ++i) {
styleVals[i] = 0f;
}
styleVals[model] = 1f;
// Copy the input data into TensorFlow.
Log.d("tensor", "Width: " + bitmap.getWidth() + ", Height: " + bitmap.getHeight());
inferenceInterface.feed(
INPUT_NODE, floatValues, 1, bitmap.getWidth(), bitmap.getHeight(), 3);
inferenceInterface.feed(STYLE_NODE, styleVals, NUM_STYLES);
inferenceInterface.run(new String[]{OUTPUT_NODE}, false);
inferenceInterface.fetch(OUTPUT_NODE, floatValues);
for (int i = 0; i < intValues.length; ++i) {
intValues[i] =
0xFF000000
| (((int) (floatValues[i * 3] * 255)) << 16)
| (((int) (floatValues[i * 3 + 1] * 255)) << 8)
| ((int) (floatValues[i * 3 + 2] * 255));
}
bitmap.setPixels(intValues, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
}
① 我们获取本地的一张图片,然后将其进行裁剪缩放做成一张正方形图片bitmap(512 * 512);
② stylizeImage()方法中,接收该bitmap,同时接收一个float类型的参数,用以指定不同的风格迁移效果;
③ 注意TensorFlowInferenceInterface实例接口的三个方法(查看源码,可知):
feed
()方法有很多,根据需要传入参数即可,必须传入的为 intputName
和数据 src
。必须要的是传入数据的类型是什么,不然是不成功的。run
()方法也有好几个,是执行运行的,需要传入 outputName
数组,这里的outputName
需要和 fetch
相关函数中的一致。fetch
()方法也有很多,也是需要传出的即可,必须传入的是 outputName
和 要存储结果的数组 dst
。必须要确定传出结果的数据类型。得到的结果如下:
2016年,中国版的Prisma — “深黑”上线,效果和Prisma类似,但有一个致命的缺陷——只能处理正方形图片。
谷歌给出的示例中,由于模型文件本身的限制,也是只能处理一张正方形的图片。如果在上面代码中,去掉裁剪缩放的步骤,直接使用一张正方形图片,此时,跑出来的结果是报错:
java.nio.BufferOverflowException
解决方法是,将用于存储结果的数组人为扩大为两倍:
float[] floatValues1 = new float[floatValues.length * 2];
// inferenceInterface.fetch(OUTPUT_NODE, floatValues);
inferenceInterface.fetch(OUTPUT_NODE, floatValues1);
for (int i = 0; i < intValues.length; ++i) {
intValues[i] =
0xFF000000
| (((int) (floatValues1[i * 3] * 255)) << 16)
| (((int) (floatValues1[i * 3 + 1] * 255)) << 8)
| ((int) (floatValues1[i * 3 + 2] * 255));
}
这样处理之后,不会报错,但是得到的效果非常差:
受制于模型文件,所以不能直接使用上面的代码处理一张长方形图片,而用户大多数的图片都是长方形的,所以需要进一步的处理。
1)使用canvas——人为将长方形图片转换为正方形图片;
这里有一个非常关键的思想:谷歌的模型文件只能处理正方形图片,那我们就根据用户的图片宽高去手动构建一个正方形图片,然后在处理结果出来之后,显示之前,对图片进行裁剪,这样就可以得到一张经过正常处理的长方形图片。这一过程借助Canvas实现:
croppedBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.beauty);
croppedBitmap = reSizeBitmap(croppedBitmap);
croppedBitmap = Bitmap.createScaledBitmap(croppedBitmap, desiredSize, desiredSize, false);
imageView.setImageBitmap(croppedBitmap);
... ...
// 这个方法就是将一张长方形图片转换成一张正方形图片
private Bitmap reSizeBitmap(Bitmap bitmap) {
Bitmap b = null;
width = bitmap.getWidth();
height = bitmap.getHeight();
if (width < height) {
b = Bitmap.createBitmap(height, height, bitmap.getConfig());
Canvas c = new Canvas(b);
c.drawBitmap(bitmap, (height - width) / 2, 0, null);
} else if (height < width) {
b = Bitmap.createBitmap(width, width, bitmap.getConfig());
Canvas c = new Canvas(b);
c.drawBitmap(bitmap, 0, (width - height) / 2, null);
} else if (width == height) {
return bitmap;
}
return b;
}
我们获取图片的宽高,根据长的那一边进行构建正方形。
2)处理完后,再将图片进行裁剪,裁剪完后进行显示:
private void done() {
if (width < height) {
float scale = desiredSize * 1.0f / height * 1.0f;
textureCopyBitmap = ImageUtils.clip(textureCopyBitmap, (desiredSize - Math.round(width * scale)) / 2,
0, Math.round(width * scale), desiredSize);
imageView.setImageBitmap(textureCopyBitmap);
} else {
float scale = desiredSize * 1.0f / width * 1.0f;
textureCopyBitmap = ImageUtils.clip(textureCopyBitmap, 0,
(desiredSize - Math.round(height * scale)) / 2, desiredSize, Math.round(height * scale));
imageView.setImageBitmap(textureCopyBitmap);
}
}
得到的结果如下:
第一张图,我们可以清晰的看到,真正处理的还是一张正方形图片(我们手动转换长方形图片得到的),而后进行裁剪,将多余的图片区域裁剪掉,就可以显示了。最终我们得到了想要的结果。
本项目地址(包含模型文件):https://github.com/shenbuqingyun/stylizedImage-tensorflow
PS:谷歌训练的模型文件非常合适,大小只有550KB,却实现了26种风格滤镜效果。我在github上找到的其他个人训练的模型文件,通常都以M为单位,非常大。另外,Tensorflow在Android中的库打包时也会非常大,arm32位和64位的到有8M,这是非常惊人的大库。所以实际使用时,需要考量效果和成本。