使用Tensorflow实现图片风格迁移,圆梦名画

一,前期基础知识储备

1)Prisma — 图片风格迁移的鼻祖

照片可以记录生活的瞬间,变成一幅幅的回忆;而 Prisma 则是可以让瞬间的回忆变成永恒的名画!我们平常用手机随意拍出来的照片效果看起来都很普通,而通过 Prisma 处理之后,你一定会惊叹于它的神奇!

使用Tensorflow实现图片风格迁移,圆梦名画_第1张图片

 

Pisma 是一款来自俄罗斯的照片美化应用,和「彩云天气」那样,借助人工智能技术将自身的能力提升到另一个层次。“Pisma 运用了综合人工神经网络技术(neural networks)和人工智能技术,学习模仿各种著名绘画大师和主要流派的艺术风格,然后对你的照片进行全智能的风格化处理”。

也就是说,每个滤镜最后所呈现的照片艺术效果,都是 Pisma “模仿”过去那些世界伟大艺术家们的风格,对你的照片进行 AI 智能分析之后而重绘出来的。不仅在技术上让人惊叹,实际产出的照片效果之佳,也同样让人为之惊叹!!

2)Tensorflow — 谷歌开源,实现图片风格迁移

官方Demo地址:https://github.com/tensorflow/tensorflow/tree/master/tensorflow/examples/android

谷歌利用tensorflow一共实现了Android相关的例子有三个 —— 物体定位,行人识别,风格迁移,如图:

使用Tensorflow实现图片风格迁移,圆梦名画_第2张图片

谷歌这里给出的实例是处理相机获得实时预览图。本篇文章的目的,是将风格迁移的效果运用到图片上。

谷歌训练的模型文件中一共有26种风格迁移的效果,下面来一一实现。

二,上代码,具体实现

1)build.gradle中添加tensorflow的依赖

implementation 'org.tensorflow:tensorflow-android:+'

2)assets文件夹下放置谷歌训练好的模型文件

使用Tensorflow实现图片风格迁移,圆梦名画_第3张图片 谷歌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。必须要确定传出结果的数据类型。

得到的结果如下:

使用Tensorflow实现图片风格迁移,圆梦名画_第4张图片

三,延伸,可运用于正式项目中

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));
        }

 

 

这样处理之后,不会报错,但是得到的效果非常差

使用Tensorflow实现图片风格迁移,圆梦名画_第5张图片

受制于模型文件,所以不能直接使用上面的代码处理一张长方形图片,而用户大多数的图片都是长方形的,所以需要进一步的处理。

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);
        }
    }

得到的结果如下:

使用Tensorflow实现图片风格迁移,圆梦名画_第6张图片 使用Tensorflow实现图片风格迁移,圆梦名画_第7张图片

第一张图,我们可以清晰的看到,真正处理的还是一张正方形图片(我们手动转换长方形图片得到的),而后进行裁剪,将多余的图片区域裁剪掉,就可以显示了。最终我们得到了想要的结果。

 

本项目地址(包含模型文件):https://github.com/shenbuqingyun/stylizedImage-tensorflow

PS:谷歌训练的模型文件非常合适,大小只有550KB,却实现了26种风格滤镜效果。我在github上找到的其他个人训练的模型文件,通常都以M为单位,非常大。另外,Tensorflow在Android中的库打包时也会非常大,arm32位和64位的到有8M,这是非常惊人的大库。所以实际使用时,需要考量效果和成本。

 

你可能感兴趣的:(高级要求)