Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

CoreMLでリアルタイムなスタイル変換

これはGalapagos Advent Calendar 20日目の記事です。

二度目まして。iOSチームの高橋です。好きな金額は二兆円です。

今回はiOS上で簡単にニューラルネットのモデルを実行させられるCoreMLを利用して、リアルタイムなスタイル変換を実装する話をします。

準備

Kerasモデルファイルの入手

さて、リアルタイムなスタイル変換を行う手法としてはarXiv:1603.08155が存在しますが、なんと!昨日の記事でまんださんがこれをKerasで実装してくれています!(しらじら)

なので、できあがったh5モデルファイルをもらうことにしました。これさえあればモネ風のスタイル変換ができる、はずです。

mlmodelへの変換

Kerasのモデルファイルをもらったので、coremltoolsを使ってmlmodelファイルに変換します。 coremltoolsは現時点ではPython2系にしか対応していないようなので、しぶしぶPython2を使います。

$ pip install -U coremltools
>>> import coremltools
WARNING:root:Keras version 2.1.2 detected. Last version known to be fully compatible of Keras is 2.0.4 .
WARNING:root:TensorFlow version 1.4.1 detected. Last version known to be fully compatible is 1.1.1 .
>>> coreml_model = coremltools.converters.keras.convert('./monet2.h5', input_names='input_1', image_input_names='input_1', output_names='transform_output')

(中略)

ValueError: Unknown layer: InputNormalize

はい、失敗しましたね。今日のblogはここまでです。ありがとうございました。

……というわけにはゆかないので、今回はこれを乗り越えてみせたいと思います。

なぜなのか

なぜ失敗するのかというと、元になったKerasのモデルがKerasにはない独自のレイヤーを定義して使用しているからです1。 するとcoremltoolsは「そんなレイヤー知りません」という顔でエラーを投げて寄越すわけです。

# たとえばこういうレイヤーがあると変換に失敗する
class InputNormalize(Layer):
    def __init__(self, **kwargs):
        super(InputNormalize, self).__init__(**kwargs)

    def build(self, input_shape):
        pass

    def compute_output_shape(self,input_shape):
        return input_shape

    def call(self, x, mask=None):
        return x/255.

いまさら純Kerasで作り直してもらうわけにもゆかないので、これは困りました。

tf-coreml

ところで話は変わりますが、TensorFlowのモデルからmlmodelへ変換するスクリプトが実は存在します(いくらかの制限つきではありますが)。

github.com

ということは、KerasのモデルをTensorFlowのモデルに変換することができれば、この問題を乗り越えることができるかもしれません。 そしてKerasのモデルがTensorFlowをバックエンドとして利用しているならば、それは原理的には可能なはずです。

Keras to TensorFlow

そう思って検索をかけてみると、先行研究を発見することができます。 だよね〜、と思いながらモデルファイルを投入します。

$ python keras_to_tensorflow.py -input_model_file ./monet2.h5 -output_model_file ./monet2.pb
('input args: ', Namespace(f=None, graph_def=False, input_fld='.', input_model_file='../monet2.h5', num_outputs=1, output_fld='.', output_graphdef_file='model.ascii', output_model_file='../monet2.pb', output_node_prefix='output_node'))

(中略)

ValueError: Unknown layer: InputNormalize

コケてしまいました。やはり「そんなレイヤー知りません」ということのようです。えっじゃあKerasは独自に定義したレイヤーを含むモデルは配布できないってことですか?と思って検索すると、こういうコメントを発見しました:

model = keras.models.load_model('temp_model.h5',
              custom_objects={'Melspectrogram':kapre.time_frequency.Melspectrogram})

なるほど、load_model時にレイヤークラスを渡してやればいいようです。なのでkeras_to_tensorflow.pyをすこし修正して再挑戦。

net_model = load_model(weight_file_path, custom_objects={'InputNormalize': InputNormalize,
                                                         'ReflectionPadding2D': ReflectionPadding2D,
                                                         'Denormalize': Denormalize,
                                                         'VGGNormalize': VGGNormalize,
                                                         'dummy_loss': dummy_loss})
Converted 122 variables to const ops.
('saved the freezed graph (ready for inference) at: ', '././monet2.pb')

やった!これでTensorFlowのモデルファイルが手に入りました。

TensorFlow to CoreML

手に入ったTensorFlowモデルをtf-coremlに投入してやります。

>>> import tfcoreml
WARNING:root:Keras version 2.1.2 detected. Last version known to be fully compatible of Keras is 2.0.6 .
WARNING:root:TensorFlow version 1.4.1 detected. Last version known to be fully compatible is 1.2.1 .
>>> coreml_model = tfcoreml.convert(tf_model_path='monet2.pb', mlmodel_path='monet2.mlmodel', input_name_shape_dict={'input_1:0': [1, 256, 256, 3]}, output_feature_names=['transform_output/mul:0'], image_input_names=['input_1:0'])

(中略)

 Core ML model generated. Saved at location: monet2.mlmodel

Core ML input(s):
 [name: "input_1__0"
type {
  imageType {
    width: 256
    height: 256
    colorSpace: RGB
  }
}
]
Core ML output(s):
 [name: "transform_output__mul__0"
type {
  multiArrayType {
    shape: 3
    shape: 256
    shape: 256
    dataType: DOUBLE
  }
}
]

これでmlmodelファイルが手に入った……ように見えますが、よく見ると出力の型がmultiArrayTypeになっています。これだと画像として出力されないので、Swift側で変換コードを書いてやる必要があるのですが、それはイケてないなあと思っていたところ、AppleのDeveloper Forumに解決方法が書かれていました。

forums.developer.apple.com

こういう関数を定義して噛ませればよいようです:

def convert_multiarray_output_to_image(spec, feature_name, is_bgr=False):
    """
    Convert an output multiarray to be represented as an image
    This will modify the Model_pb spec passed in.
    Example:
        model = coremltools.models.MLModel('MyNeuralNetwork.mlmodel')
        spec = model.get_spec()
        convert_multiarray_output_to_image(spec,'imageOutput',is_bgr=False)
        newModel = coremltools.models.MLModel(spec)
        newModel.save('MyNeuralNetworkWithImageOutput.mlmodel')
    Parameters
    ----------
    spec: Model_pb
        The specification containing the output feature to convert
    feature_name: str
        The name of the multiarray output feature you want to convert
    is_bgr: boolean
        If multiarray has 3 channels, set to True for RGB pixel order or false for BGR
    """
    for output in spec.description.output:
        if output.name != feature_name:
            continue
        if output.type.WhichOneof('Type') != 'multiArrayType':
            raise ValueError("%s is not a multiarray type" % output.name)
        array_shape = tuple(output.type.multiArrayType.shape)
        channels, height, width = array_shape
        from coremltools.proto import FeatureTypes_pb2 as ft
        if channels == 1:
            output.type.imageType.colorSpace = ft.ImageFeatureType.ColorSpace.Value('GRAYSCALE')
        elif channels == 3:
            if is_bgr:
                output.type.imageType.colorSpace = ft.ImageFeatureType.ColorSpace.Value('BGR')
            else:
                output.type.imageType.colorSpace = ft.ImageFeatureType.ColorSpace.Value('RGB')
        else:
            raise ValueError("Channel Value %d not supported for image inputs" % channels)
        output.type.imageType.width = width
        output.type.imageType.height = height

実際に試してみます。

>>> spec = coreml_model.get_spec()
>>> convert_multiarray_output_to_image(spec, 'transform_output__mul__0')
>>> newModel = coremltools.models.MLModel(spec)
>>> newModel.save('monet2.mlmodel')

これで……

f:id:glpgsinc:20171218155353p:plain

できました!!!

あとはこのmlmodelファイルをスッとXcodeに投入すれば、画像の変換を行うクラスが生成されます。長かった…… 便利な世の中ですね。

実装

メインロジック

実装のメイン部分はCoreMLのおかげで非常に簡潔です。こんな風に:

import AVFoundation
import CoreImage
import UIKit

class ViewController: UIViewController {

    @IBOutlet private weak var imageView: UIImageView!

    let model = monet2()
    let imageSize = CGSize(width: 256, height: 256)

    /// メインロジック。CVPixelBufferを受け取って変換して表示する
    ///
    private func pixelBufferDidUpdate(pixelBuffer: CVPixelBuffer) {
        let output = try! model.prediction(input_1__0: pixelBuffer)
        let image = UIImage(ciImage: CIImage(cvPixelBuffer: output.transform_output__mul__0))
        DispatchQueue.main.async {
            self.imageView.image = image
        }
    }

    let session: AVCaptureSession = AVCaptureSession()
    let videoQueue: DispatchQueue = DispatchQueue(label: "videoqueue")
    var pixelBuffer: CVPixelBuffer? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        heavyLifting()
    }

}

で、残る問題は動画のキャプチャとピクセルバッファのクロップなのですが、それについては以下で簡単に説明したいと思います。

AVFoundationによる動画のキャプチャ

今回はリアルタイムなスタイル変換を行いたいので、カメラからの入力をリアルタイムに受け取る必要があります。 こういう場合にはAVFoundationを使えばよいらしいので、そのためのセットアップを行います2

extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    func heavyLifting() {
        CVPixelBufferCreate(kCFAllocatorDefault,
                            Int(imageSize.width), Int(imageSize.height),
                            kCVPixelFormatType_32BGRA, nil, &pixelBuffer)

        let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)!
        device.activeVideoMinFrameDuration = CMTime(value: 1, timescale: 30)

        let videoInput = try! AVCaptureDeviceInput(device: device)
        session.addInput(videoInput)

        session.sessionPreset = .hd1280x720

        let videoDataOutput = AVCaptureVideoDataOutput()
        videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
        videoDataOutput.setSampleBufferDelegate(self, queue: videoQueue)
        videoDataOutput.alwaysDiscardsLateVideoFrames = true
        session.addOutput(videoDataOutput)

        let connection = videoDataOutput.connection(with: .video)
        connection?.videoOrientation = .portrait

        session.startRunning()
    }

冒頭のCVPixelBufferCreateは、CoreMLとのやりとりに使うピクセルバッファを作成するためのもので、AVFoundationとは関係ありません。 その下では背面カメラから動画の入力を受ける設定をしており、動画サイズは1280×720だという指定が続きます。最後に出力の受け先をselfに向けて撮影を開始しています。

CoreImageによるフレームの切り抜き

あとはキャプチャしたフレームを適切な大きさにクロップしてCVPixelBufferに書き込む必要がありますが、これはCoreImageを経由することで実現できるようです。筆者はCoreImageに明るくないのでけっこう苦労しましたが、Stack Overflowに助けられてなんとか画像の中央を切り出すことに成功しました。

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        let ciImage = CIImage(cvImageBuffer: CMSampleBufferGetImageBuffer(sampleBuffer)!)
        let originalRect = ciImage.extent
        let cropRect = CGRect(x: (originalRect.width - imageSize.width) / 2,
                              y: (originalRect.height - imageSize.height) / 2,
                              width: imageSize.width, height: imageSize.height)
        let cropped = ciImage.cropped(to: cropRect)
        let transformed = cropped.transformed(by: CGAffineTransform(translationX: -cropRect.origin.x, 
                                                                    y: -cropRect.origin.y))
        let context = CIContext()
        context.render(transformed, to: pixelBuffer!)
        pixelBufferDidUpdate(pixelBuffer: pixelBuffer!)
    }

}

こうしてできたpixelBufferを上で書いたメインロジックに渡してやれば、CoreMLの力で画像が変換されるので、CIImageを経由してUIImageにしてUIImageViewに表示してやれば完成です。

結果

弊社エントランスにかざってあるクリスマスツリーをモネ風にしてみました。端末はiPhone Xを使用していますが、iPhone 7でも似たような速度で動きます。

f:id:glpgsinc:20171218185207g:plain

いかがでしょうか。モネ風かどうかはともかく、なにかしらスタイルが変換されていることがおわかりいただけるかと思います3。 関係ないですが、端末がまあまあ熱くなるので、無茶をしているのだなあという気持ちになります。

おわりに

ということで、iOS端末上でリアルタイムにスタイル変換をするアプリを簡単に作ることができました。次はこっちの論文なんかも実装してみたら楽しいかもしれませんね。

ところで

弊社では機械学習に興味があったりなかったりするエンジニアを募集しています。ご応募お待ちしております。

www.glpgs.com

以上です。明日はUIデザイナーのまあのんさんがProtoPieについて書いてくれるそうですね。お楽しみに。


  1. という記事を書いている間に判明したのですが、iOS 11.2以降は独自のレイヤーに対応させることが(がんばれば)可能なようですね(参考)。

  2. あっ、Info.plistのNSCameraUsageDescriptionを設定しておくのを忘れないでくださいね。

  3. もっとも、元からわりと派手なクリスマスツリーだという話はあるのですが。

Kerasのお勉強のついでにStyleTransfer

これはGalapagos Advent Calendar 19日目の記事です。

こんにちは、AIチームの中の人まんだです。 アドベントカレンダー二度目の登場です。前回は合同勉強会の参加レポートを投稿しましたが、 今回はせめてAIチームらしく、機械学習の話題をしたいと思います。

今回の内容は、「Neural-Style-Transferを題材にKerasのお勉強を始めよう」です。

背景

ガラパゴスAIチームでは長らく機械学習モデルはTensorFlowで書いていたのですが、 最近「Kerasって便利そうだよね〜。」「次に新しく組むモデルからはKerasを採用しても良いのでは?」 という話がちらほらと上がり始めていました。 これは、勉強せないかんなぁ〜と考えていたところで、12月のアドベントカレンダーの話が来たので、 「そうだ!アドベントカレンダーねたに抱き合わせでKerasのお勉強をしてしまおう」と思い立ってこの記事が作られています。 ただKerasのお勉強をしてもつまらないので、何かモデルをいじってみようということで、今回はNeural-Style-Transferを題材に選んでみました。

Neural-Style-Transferとは

NeuralStyleTransferとは、論文1で2016年に発表された、画像(絵画とか)のスタイル(画風・雰囲気)を別の画像(写真など)に転写できるようにするネットワークのことです。

このモデルでは、1回の画像のスタイル変換ごとにノイズ画像を出発地点として、ForwardとBackwardの計算が必要になるために出力されるまでそこそこ時間がかかります。 なので、今回ターゲットにするのは、リアルタイムでスタイル変換することを可能にしたモデル2を使います。

StyleTransferとKeras

というわけで、Style-TranferをKerasで...と思ったら、当然のようにすでに実装はされているわけでして。 今回はこの実装コードを参考にしながらKerasのお勉強を進めていきます。

モデルの中身

高速スタイル変換の中身自体の解説は、この記事などで詳しく紹介されています。 ここでは、kerasでの実装コードを眺めてどのようにモデルが記述されているか見てみましょう。

Kerasでのモデルの定義

Kerasで利用出来るモデルは次の二つがあります。

  1. Sequential Model
  2. functional APIを用いたモデル

Sequential Model

以下はドキュメントから持ってきた例です。

model = Sequential()
model.add(Dense(32, input_shape=(500,)))
model.add(Dense(10, activation='softmax'))
model.compile(optimizer='rmsprop',
              loss='categorical_crossentropy',
              metrics=['accuracy'])

addを使ってレイヤーをただただ追加していくシンプルな作りですね。

functional API

シンプルなモデルだったらSequentialモデルで良いのですが、少し洒落たことがしたいとなったら functional APIを使いましょう。 またまたドキュメントから例を持ってくると

from keras.models import Model
from keras.layers import Input, Dense

a = Input(shape=(32,))
b = Dense(32)(a)
model = Model(inputs=a, outputs=b)

と書けます。

Neural-Style-TransferでのモデルをKerasで書くと?

というわけで、さっそくNeural-Style-TransferのモデルをKerasで定義してみましょう。 Githubにあるtrain.pyの中身を見てみると

net = nets.image_transform_net(img_width, img_height, tv_weight)
model = nets.loss_net(net.output, net.input, img_width, img_height, style_image_path, content_weight, style_weight)
model.summary()
optimizer = Adam()  # Adam(lr=learning_rate,beta_1=0.99)
model.compile(optimizer,  dummy_loss)  # Dummy loss since we are learning from regularizes

どうやらnets.pyにスタイル変換ネットワークとロスネットワークが記述されている様子です。 ともあれmodelが定義されたらoptimizerを指定して、model.compileするだけ。お手軽ですね。 今回別口でlossを定義しているので、compile時点ではdummy_lossを指定してます。

スタイル変換ネットワーク

では、nets.image_transform_net()の中身を確認してみましょう。そっとnets.pyを開いてみます。

from keras.layers import Input
from keras.layers.merge import concatenate
from keras.models import Model, Sequential
from layers import InputNormalize, VGGNormalize, ReflectionPadding2D, Denormalize, conv_bn_relu, res_conv, dconv_bn_nolinear
from loss import StyleReconstructionRegularizer, FeatureReconstructionRegularizer, TVRegularizer
from keras import backend as K
from VGG16 import VGG16
import img_util


def image_transform_net(img_width, img_height, tv_weight=1):
    x = Input(shape=(img_width, img_height, 3))
    a = InputNormalize()(x)
    a = conv_bn_relu(32, 9, 9, stride=(1, 1))(a)
    a = conv_bn_relu(64, 3, 3, stride=(2, 2))(a)
    a = conv_bn_relu(128, 3, 3, stride=(2, 2))(a)
    for i in range(5):
        a = res_conv(128, 3, 3)(a)
    a = dconv_bn_nolinear(64, 3, 3)(a)
    a = dconv_bn_nolinear(32, 3, 3)(a)
    a = dconv_bn_nolinear(3, 9, 9, stride=(1, 1), activation="tanh")(a)
    # Scale output to range [0, 255] via custom Denormalize layer
    y = Denormalize(name='transform_output')(a)

    model = Model(inputs=x, outputs=y)

    if tv_weight > 0:
        add_total_variation_loss(model.layers[-1], tv_weight)

    return model

ふむふむ。keras.layersからインポートされているInputで入力を規定している様子。その後InputNormalizeは自作しているようですが、[0, 255]→[0, 1]に正規化しているご様子。 その後、conv_bn_reluが3回繰り返されています。

from keras.layers.core import Activation
from keras.layers.normalization import BatchNormalization
from keras.layers.convolutional import Conv2D
def conv_bn_relu(nb_filter, nb_row, nb_col, stride):   
    def conv_func(x):
        x = ReflectionPadding2D(padding=(nb_row // 2, nb_col // 2))(x)
        x = Conv2D(nb_filter, (nb_row, nb_col), strides=stride, padding='valid')(x)
        x = BatchNormalization()(x)
        x = Activation("relu")(x)
        return x
    return conv_func

これはConvでたたみ込み→バッチ正規化→Reluの処理をしています。 その後はResidual Blockを5層用意しています。さらにdconv_bn_nolinearでDeconvolutionしていますね。この辺りは中身はconv_bn_reluと大差ないので割愛します。 deconv_bn_nolinear()ではactivation関数はデフォルトがReluなのですが最後の層だけはtanhを指定しています。引数で渡すだけで切り替えられて便利ですね。 最後にDenormalize()で[0, 255]の範囲に戻しています。 そしてinputとoutputをModel()に渡してあげて完了です。

Lossネットワーク

VGG16の重みをセットして、Style LossとContent Lossをそれぞれ計算します。

from VGG16 import VGG16
def loss_net(x_in, trux_x_in, width, height, style_image_path, content_weight, style_weight):
    # Append the initial input to the FastNet input to the VGG inputs
    x = concatenate([x_in, trux_x_in], axis=0)
    
    # Normalize the inputs via custom VGG Normalization layer
    x = VGGNormalize(name="vgg_normalize")(x)

    vgg = VGG16(include_top=False, input_tensor=x)

    vgg_output_dict = dict([(layer.name, layer.output) for layer in vgg.layers[-18:]])
    vgg_layers = dict([(layer.name, layer) for layer in vgg.layers[-18:]])

    if style_weight > 0:
        add_style_loss(vgg, style_image_path, vgg_layers, vgg_output_dict, width, height, style_weight)

    if content_weight > 0:
        add_content_loss(vgg_layers, vgg_output_dict, content_weight)

    # Freeze all VGG layers
    for layer in vgg.layers[-19:]:
        layer.trainable = False

    return vgg

Style LossとContent Lossに関しては、元論文に準じています。詳細は元論文を参照してください。

学習

それではいよいよ学習してみましょう。 用意するものは、

  1. 学習したいスタイル画像1枚
  2. 学習用コンテンツ画像 たくさん(今回はMS COCOを使用)
  3. GPUマシン

今回は学習に社内のGPUマシン(GeForce GTX 1080搭載)を使いました。(社内マシンはDockerで運用しているので、このモデルも諸々Dockerに載せる作業がありましたが、詳細は割愛します。) 学習させるスタイルは、モネの睡蓮にしてみました。

f:id:glpgsinc:20171218140434j:plain (http://www.mam-e.it/wp-content/uploads/2017/01/NINFEE-claude-monet-992x538.jpg)

いざ学習へ

from keras.preprocessing.image import ImageDataGenerator
datagen = ImageDataGenerator()
dummy_y = np.zeros((train_batchsize, img_width, img_height, 3))  # Dummy output, not used since we use regularizers to train
skip_to = 0

i = 0
t1 = time.time()
for x in datagen.flow_from_directory(train_image_path, class_mode=None, batch_size=train_batchsize, target_size=(img_width, img_height), shuffle=False):

    if i > nb_epoch:
        break

    if i < skip_to:
        i += train_batchsize
        if i % 1000 == 0:
            print("skip to: %d" % i)
        continue

    hist = model.train_on_batch(x, dummy_y)
    if i % 50 == 0:
        print(hist, (time.time() - t1))
        t1 = time.time()

    if i % 500 == 0:
        print("epoc: ", i)
        val_x = net.predict(x)

        display_img(i, x[0], style)
        display_img(i, val_x[0], style, True)
        model.save_weights(os.path.join(model_dump_dir, style) + '_weights.h5')
        model.save(os.path.join(model_dump_dir, style) + '.h5', include_optimizer=False)
    i += train_batchsize

学習自体はいたってシンプルです。 ImageDataGenerator().flow_from_directoryでバッチサイズ毎の画像読み出しを行って、model.train_on_batch()を呼ぶだけです。シンプル! 一応500ステップ毎にモデルのダンプをしています。

学習途中の画像はこんな感じになっています。

f:id:glpgsinc:20171218141551p:plainf:id:glpgsinc:20171218141555p:plainf:id:glpgsinc:20171218141602p:plainf:id:glpgsinc:20171218141559p:plainf:id:glpgsinc:20171218141611p:plainf:id:glpgsinc:20171218141607p:plain

最後の段は、約16万ステップでの出力になります。しっかり草原がモネの睡蓮っぽく(?)なってますね! きちんとスタイルが学習されています。よかったよかった。

まとめ

Kerasのお勉強を兼ねて、Neural-Style-Transferのモデルを学習させてみました。 実際のところキチンとKerasを使いこなすには、もう少し自分でちゃんと実装してみないとわかりませんね。 でもかなり便利に使えそうなので、じわじわ使っていこうと思います。

おわりに

明日はiosチームの高橋さんが、アプリ開発会社ガラパゴスらしく、このStyleTransferモデルを スマートフォンに載せてリアルタイム画風変換をやってくれるようです。お楽しみに。

さらにおわりに

弊社では、Kerasでバリバリ機械学習したい方、TensorFlowでガシガシ実装したい方、 機械学習興味ある方を絶賛募集中です。ご応募お待ちしております。

www.glpgs.com

以上になります。明日以降もアドベントカレンダーお楽しみに!


  1. Gatys, Leon A., Alexander S. Ecker, and Matthias Bethge. “Image style transfer using convolutional neural networks.” Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (CVPR). 2016.

  2. Justin Johnson, Alexandre Alahi, Li Fei-Fei. “Perceptual Losses for Real-Time Style Transfer and Super-Resolution.“ arXiv. 2016

ProtoPieでAppStore動かしてみた! 第2回

こんにちは。UIデザイナーのまあのんです。本格的な冬到来でちょっぴり風邪気味です。ずび


ProtoPie動かしてみたレポート第二回目になります。
一回目の記事はこちら↓

gtech.hatenablog.com

前回はsketchで作成したデザインをProtopieにinportしましたね。


-動かす流れおさらい-

  • スクロールする

  • イメージをタップすると詳細ページに遷移する

  • 詳細ページに遷移する時にイメージが拡がる

  • 一覧に戻る時イメージが縮む



今回はスクロールとページ遷移する途中までつくっていきます!

Step2.画面全体をスクロールさせる



① まずはスクロール可能な範囲をまるっとグループ化(⌘G)してレイヤー最下層に。画面に固定表示したいレイヤーは上にしておきましょう。

固定で見せたいステータスバーとタブバーを残して他はグループ化。 f:id:glpgsinc:20171215164940p:plain:w400

②そしてグループレイヤーを選択しつつ、メニューからScroll Containerを選択するとグループの上に表示されます。
f:id:glpgsinc:20171215173146p:plain:w400
Scroll Containerは、レイヤーパネルからコンテナ内にレイヤーを配置するとコンテナのスクロールやページングが可能になります。
プロパティパネルでスクロール方向やオーバースクロールした時のリアクションが設定できます。

!注意!
Scroll Containerを選択してもあくまでレイヤーパネルに追加されただけです。グループをScroll Containerにひゅっと移動させることを忘れずに!
(これよくわからず地味に苦戦してました、、)


f:id:glpgsinc:20171215173507p:plain:w400
レイヤー名の階層が変わり、グループが左上の正方形にマスクされたら成功です。

③Scroll Containerを選択した状態で、マスクされてる範囲をバイスサイズに合わせましょう。レイヤーサイズではないので気をつけて。


それでは、
Previewを見てみましょう


f:id:glpgsinc:20171215175315g:plain:w160

第一段階成功です!!!
作ってる側はこれだけで謎の感動があったりします



Step3.イメージをタップして遷移する


f:id:glpgsinc:20171215192530g:plain:w160
次にこの動きをProtopieのできる範囲で再現していきます。
最大の難関の予感、、





複雑なインタラクションなので、まずは対象のトリガーとそれに対するレスポンスを整理してみましょう。


【トリガー】
f:id:glpgsinc:20171215193916p:plain:w200
『今日のAPP』エリアをタップ


【レスポンス】(ざっくり)

  • 画像やタイトル:上部に移動しながら画面の横幅全体に広がる

  • アプリ詳細画面のテキストエリア:画像から下ににゅっと伸びながら出現

  • closeボタン:上部に移動しながら出現


f:id:glpgsinc:20171215194115p:plain:w200
遷移完了。


このレスポンスをさらに分解して一つずつ動作を追加していきます。


それではProtopieで実際に動きをつけてみましょう。

f:id:glpgsinc:20171215182908p:plain:w700

① レイヤーパネルからタップする対象のレイヤーを選択します。
②次に、Add Triggerを選択して、Tapを選択してください。

f:id:glpgsinc:20171215191235p:plain:w700
Tapの下に➕が現れましたね。
➕はトリガーに対するレスポンスを選択できます。

f:id:glpgsinc:20171218013940p:plain:w300
③虹の画像エリアを画面上部に動かすため、レイヤーで虹画像を選択しながら➕ではmoveを選択します。

f:id:glpgsinc:20171218014055p:plain:w500
右のインスペクターパネルではその時の大きさの変化量などを定義することができます。
今回はパーツを画面最上部に配置したいので、move項目のXYの値を調節していきます。 アニメーションのタイミングやイージングなどもここで細かく設定できます。

f:id:glpgsinc:20171218015632g:plain:w160
プレビューで確認。位置はこんな感じかな。


レスポンスの選択は直感的で簡単にできますね。これをうまく組み合わせていけば複雑なインタラクションも難なくできます!
この要領で他のレスポンスも追加していきます。

、、、といったところで今日はここまで!

次回で完成までもってきます!宣言!
12月21日更新予定です。


参考記事
https://www.protopie.io/learn/
さよなら Pixate, よろしくProtoPie – heru – Medium
【Protopieのススメ】UIデザイナーのためのインタラクションモックアップツール - Qiita