- 投稿日:2020-07-12T22:02:20+09:00
【SwiftUI】BlendMode で View を合成する
はじめに
この記事では、Adobe Photoshop などのデザインツールでおなじみのブレンドモードについて扱います。
コードと実行結果は SwiftUI のものですが、ブレンドモードは Core Graphics が提供しているので、SwiftUI を使わない開発でも知識としては使えると思います。さて、本題です。
SwiftUI で2つのビューをブレンドしてみます。こんな感じのイメージです。
緑が透過なビューの場合は単純に上に載せるだけなので、非透過な緑色のビューとの合成を考えます。
この場合のサンプルコードはこちら。struct ContentView: View { var body: some View { ZStack { Image("lenna") .resizable() .scaledToFit() Rectangle() .fill(Color.green) .blendMode(.multiply) // ← ココ! } .edgesIgnoringSafeArea(.all) } }簡単な説明
ZStack
でImage
の上にRectangle
を載せる
Image
を配置
- アスペクト比を維持してビューにフィット
Rectangle
を配置
green
で塗りつぶし- 乗算ブレンド
- セーフエリアを無視して配置
実行結果
BlendMode
を指定して配置するだけなので、とても簡単ですが、次のことが気になりました。
- 乗算ブレンド
.multiply
とはどのような合成方法なのか?- 他の
BlendMode
を指定した場合はどのような結果が得られるのか?ということで、本記事では各種ブレンドモードについての情報と実際の表示結果を載せました。
表示結果は SwiftUI のプレビュー機能を使用しています。環境
- Xcode Version 11.5 (11E608c)
BlendMode
SwiftUI の API リファレンスには概要が記載されていなかったので、Core Graphics の API リファレンスも参照しました。次のように記載されていました。
You can find more information on blend modes, including examples of images produced using them, and many mathematical descriptions of the modes, in PDF Reference, Fourth Edition, Version 1.5, Adobe Systems, Inc. If you are a former QuickDraw developer, it may be helpful for you to think of blend modes as an alternative to transfer modes1
Adobe Systems の PDF Reference, Fourth Edition に正確な情報が記載されているとのことです。
この資料には、全てではないですが、各ブレンドモードの数式も記載がありました。PDF Reference に記載のあるブレンドモード
BlendMode CGBlendMode 名称 normal kCGBlendModeNormal 通常 multiply kCGBlendModeMultiply 乗算 screen kCGBlendModeScreen スクリーン overlay kCGBlendModeOverlay オーバーレイ darken kCGBlendModeDarken 比較(暗) lighten kCGBlendModeLighten 比較(明) colorDodge kCGBlendModeColorDodge 覆い焼きカラー colorBurn kCGBlendModeColorBurn 焼き込みカラー softLight kCGBlendModeSoftLight ソフトライト hardLight kCGBlendModeHardLight ハードライト difference kCGBlendModeDifference 差の絶対値 exclusion kCGBlendModeExclusion 除外 hue kCGBlendModeHue 色相 saturation kCGBlendModeSaturation 彩度 color kCGBlendModeColor カラー luminosity kCGBlendModeLuminosity 輝度 以下、数学的な説明が必要な場合は、PDF Reference, Fourth Edition で記載されていた次の変数を使います。
変数 意味 $C_b$ 背景の色(基本色) $C_s$ 前面の色(合成色) $C(C_b, C_s)$ ブレンド関数 上記変数は各色成分をもつベクトルです。また、今回扱う関数は、各色成分ごとに分離可能な関数です。
normal
通常
各ピクセルを編集またはペイントして結果色を作成します。これは、初期設定のモードです(通常モードは、モノクロ 2 階調画像やインデックスカラー画像で作業するときには、2 階調化と呼ばれます)。2
ブレンド関数は次の通りです。前面をそのまま反映します。
B(c_b,c_s) = c_smultiply
乗算
各チャンネル内のカラー情報に基づき、基本色と合成色を乗算します。結果色は暗いカラーになります。どのカラーも、ブラックで乗算すると結果はブラックになります。どのカラーも、ホワイトで乗算した場合は変更されません。ブラックまたはホワイト以外のカラーでペイントしている場合、ペイントツールで繰り返しストロークを描くとカラーは徐々に暗くなります。この効果は、複数のマーカーペンで描画したような効果が得られます。3
ブレンド関数は次の通りです。背景カラーと前面カラーの RGB の構成要素をそれぞれ乗算します。
B(c_b,c_s) = c_b × c_s背景ビューと前面ビューを入替えても同じ結果を得ることができます。
色の各成分は、0 以上 1 以下の小数です。特に ブラック(0, 0, 0), ホワイト(1, 1, 1) なので、
ブラックで乗算すると結果はブラック、ホワイトで乗算した場合は変更されないことがわかります。また、0 以上 1 未満の数は乗算を繰り返すほど、0 に近づくので、
multiply ブレンドモードを繰り返すほど徐々に暗くなります。
screen
スクリーン
各チャンネル内のカラー情報に基づき、合成色と基本色を反転したカラーを乗算します。結果色は明るいカラーになります。ブラックでスクリーニングすると、カラーは変更されません。ホワイトでスクリーニングすると、ホワイトになります。この効果は、複数の写真スライドを重ね合わせて投影したような効果が得られます。4
ブレンド関数は次の通りです。
B(c_b,c_s) = 1 – (1–c_b)×(1–c_s)背景カラーと前面カラーをそれぞれ反転させたものを乗算して、その結果の値を反転させます。
この式から、multiply と逆の性質を得ることができます。
- ブラックとスクリーンをすると変更されない
- ホワイトとスクリーンをすると結果はホワイト
- 繰り返すほど徐々に明るくなる
overlay
オーバーレイ
基本色に応じて、カラーを乗算またはスクリーンします。基本色のハイライトおよびシャドウを保持しながら、パターンまたはカラーを既存のピクセルに重ねます。基本色は、置き換えられませんが、合成色と混合されて基本色の明るさまたは暗さを反映します。5
darken
比較(暗)
各チャンネル内のカラー情報に基づき、基本色または合成色のいずれか暗い方を結果色として選択します。合成色よりも明るいピクセルが置き換えられ、合成色よりも暗いピクセルは変更されません。6
ブレンド関数は次の通りです。
B(c_b, c_s) = min(c_b, c_s)lighten
比較(明)
各チャンネル内のカラー情報に基づき、基本色または合成色のいずれか明るい方を結果色として選択します。合成色よりも暗いピクセルが置き換えられ、合成色よりも明るいピクセルは変更されません。7
ブレンド関数は次の通りです。
B(c_b, c_s) = max(c_b, c_s)colorDodge
覆い焼きカラー
各チャンネルのカラー情報に基づき、基本色を明るくして基本色と合成色のコントラストを落とし、合成色を反映します。ブラックと合成しても変化はありません。8
colorBurn
焼き込みカラー
各チャンネルのカラー情報に基づき、基本色を暗くして基本色と合成色のコントラストを強くし、合成色を反映します。ホワイトで合成した場合は、何も変更されません。9
softLight
ソフトライト
合成色に応じて、カラーを暗くまたは明るくします。画像上でスポットライトを照らしたような効果が得られます。合成色(光源)が 50 %グレーよりも明るい場合、画像は覆い焼きされたかのように明るくなります。合成色が 50 %グレーよりも暗い場合、画像は焼き込んだように暗くなります。純粋な黒または白でペイントすると、その部分の明暗ははっきりしますが、純粋な黒または白にはなりません。10
hardLight
ハードライト
合成色に応じて、カラーを乗算またはスクリーンします。画像上で直接スポットライトを照らしたような効果が得られます。合成色(光源)が 50 %グレーよりも明るい場合、画像はスクリーンされたかのように明るくなります。これは、画像にハイライトを追加するときに役立ちます。合成色が 50 %グレーよりも暗い場合、画像は乗算されたかのように暗くなります。これは、画像にシャドウを追加するときに役立ちます。純粋なホワイトまたはブラックでペイントすると、純粋なホワイトまたはブラックになります。11
difference
差の絶対値
各チャンネル内のカラー情報に基づいて、合成色を基本色から取り除くか、基本色を合成色から取り除きます。明るさの値の大きい方のカラーから小さい方のカラーを取り除きます。ホワイトと合成すると基本色の値が反転しますが、ブラックと合成しても変化はありません。12
ブレンド関数は次の通りです。
B(c_b, c_s) = | c_b – c_s |ホワイトとブレンドすると色反転ができます。
exclusion
除外
差の絶対値モードと似ていますが、効果のコントラストはより低くなります。ホワイトと合成すると、基本色の値が反転しますが、ブラックと合成しても変化はありません。13
hue
色相
ベースカラーの輝度と彩度、およびブレンドカラーの色相を持つ最終カラーが作成されます。14
saturation
彩度
基本色の輝度と色相および合成色の彩度を使用して、結果色を作成します。このモードで彩度ゼロ(グレー)の領域をペイントした場合は、何も変更されません。15
color
カラー
基本色の輝度と、合成色の色相および彩度を使用して、結果色を作成します。これにより、画像内のグレーレベルが保持され、モノクロ画像のカラー化およびカラー画像の階調化に役立ちます。16
luminosity
輝度
基本色の色相および彩度と、合成色の輝度を使用して、結果色を作成します。このモードでは、カラーモードの反対の効果が作成されます。17
サンプルコード
今回のスクリーンショットで使ったコードはこちら。
表示データ
Item.swiftimport SwiftUI struct Item: Hashable { let mode: BlendMode, name: String static var items: [Item] = [ Item(mode: .normal, name: ".normal"), Item(mode: .multiply, name: ".multiply"), Item(mode: .screen, name: ".screen"), Item(mode: .overlay, name: ".overlay"), Item(mode: .darken, name: ".darken"), Item(mode: .lighten, name: ".lighten"), Item(mode: .colorDodge, name: ".colorDodge"), Item(mode: .colorBurn, name: ".colorBurn"), Item(mode: .softLight, name: ".softLight"), Item(mode: .hardLight, name: ".hardLight"), Item(mode: .difference, name: ".difference"), Item(mode: .exclusion, name: ".exclusion"), ] }画像と色のブレンド
ImageBlendView.swiftimport SwiftUI struct ImageBlendView: View { var color: Color var mode: BlendMode var body: some View { ZStack { Image("lenna").resizable().scaledToFit() Rectangle().fill(color).blendMode(mode) } } } struct ImageBlendViewSamples: View { var mode: BlendMode var colors: [Color] = [.clear, .red, .green, .blue, .black, .white] var body: some View { HStack(spacing: 0) { [colors, mode] in ForEach(0..<colors.count) { i in ImageBlendView(color: colors[i], mode: mode) } } } } // MARK: - PreviewProvider struct ImageBlendViewSamples_Previews: PreviewProvider { static var previews: some View { Group { [items = Item.items] in ForEach(0..<items.count) { i in ImageBlendViewSamples(mode: items[i].mode) .previewDisplayName(items[i].name) } } .previewLayout(.fixed(width: 100 * 6, height: 100)) } }円のブレンド
import SwiftUI struct ColorBlendView: View { var mode: BlendMode var background: Color var colors: [Color] var body: some View { GeometryReader<AnyView> { [mode, colors] geometry in let edge = min(geometry.size.width, geometry.size.height)/2 let offset = edge/3 let arg: (Int) -> CGFloat = { 2*(.pi)*CGFloat($0)/CGFloat(colors.count) - (.pi)/2 } return AnyView( ZStack { ForEach(0..<colors.count) { index in Circle() .fill(colors[index]) .frame(width: edge) .offset(x: offset*cos(arg(index)), y: offset*sin(arg(index))) .blendMode(mode) } } .frame(width: geometry.size.width, height: geometry.size.height) ) } .background(background) .edgesIgnoringSafeArea(.all) } } struct ColorBlendViewSamples: View { var mode: BlendMode var foregrounds: [[Color]] = [ [.red, .green, .blue], .init(repeating: .green, count: 7) ] var backgrounds: [Color] = [ Color(red: 0, green: 0, blue: 0), Color(red: 1, green: 1, blue: 1), ] var body: some View { HStack(spacing: .zero) { [mode, foregrounds, backgrounds] in ForEach(0..<backgrounds.count) { i in ForEach(0..<foregrounds.count) { j in ColorBlendView(mode: mode, background: backgrounds[i], colors: foregrounds[j]) } } } } } // MARK: - PreviewProvider struct ColorBlendViewSamples_Previews: PreviewProvider { static var previews: some View { Group { [items = Item.items] in ForEach(0..<items.count) { i in ColorBlendViewSamples(mode: items[i].mode) .previewDisplayName(items[i].name) } } .previewLayout(.fixed(width: 120 * 4, height: 120)) } }参考
- CGBlendMode | Apple Developer Documentation
- BlendMode | Apple Developer Documentation
- Adobe PDF Reference Archives
- Adobe Photoshop での描画モード
- 標準画像データベース[神奈川工大 信号処理応用研究室]
- 投稿日:2020-07-12T19:38:09+09:00
個人的にFlutterで何回も調べがちなことまとめ
個人的な備忘としてまとめておきます
最近弊社で開発してるAndroidアプリがFlutter製ということもあり、
開発する中で何度も調べることがもっと増えてくると思うので逐次更新していきます。ふたりのみ Android for Flutter(会員向け)
クラフトビール飲み比べ定期配送サービス ふたりのみ とは環境
- Flutter 1.17.4
- Dart 2.8.4
Container の装飾周り
角丸にする
Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(4.0), ), );枠線を描く
Container( decoration: BoxDecoration( border: Border.all(color: Colors.blueAccent) ), );Navigator での画面遷移周り
画面遷移
Flutter 画面遷移
でいつも調べてます
Navigator は iOS と同じならスタック(後のせ先取り)なので push はお皿を積むイメージNavigator.push( context, MaterialPageRoute<Null>( settings: RouteSettings(name: "/detail"), builder: (BuildContext context) => Container(); ) );前の画面に戻る
pop はお皿を取るイメージ
一枚だけ上からとりますNavigator.pop(context);rootまでpopする
積んだお皿全部退けます
Navigator.popUntil(context, ModalRoute.withName('/'));TextField の装飾周り
枠線を描く
iOS ちっくなTextFieldにしたいときよく使う
TextField( decoration: InputDecoration( border: OutlineInputBorder(), ), );ラベルとヒントをつける
プレースホルダー的なやつを設定したいときあるけどよく忘れる
TextField( decoration: InputDecoration( labelText: 'ラベル', hintText: 'ヒント', ), );
- 投稿日:2020-07-12T16:15:53+09:00
[iOS13]キャッシュの設定について
どうやって設定するか
URLRequestを使っている場合
URLRequestクラスの
cachePolicy
プロパティより編集する。参考:cachePolicy
Alamofireを使っている場合
やはりURLRequestにキャッシュポリシーを設定し、Alamofireに渡すことになる。
// URLRequestを作成する際に、cachePolicyでキャッシュを無効化に指定する. let request = URLRequest(url: url!, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 5) // URLRequestをAlamofire.requestに渡す。 Alamofire.request(request).responseString(encoding: .utf8) { data in // なんらかの処理 }参考: [Swift] Alamofireでキャッシュを無効化してリクエストを発行する
Moyaを使っている場合
やはりMoyaの機能としてキャッシュポリシーを変える機能はないため、URLRequestを使うことになる。Moyaの
PluginType
を利用すると便利。MoyaCacheable.swiftprotocol MoyaCacheable { typealias MoyaCacheablePolicy = URLRequest.CachePolicy var cachePolicy: MoyaCacheablePolicy { get } }MoyaCacheablePlugin.swiftfinal class MoyaCacheablePlugin: PluginType { ///リクエスト送信前に呼ばれるメソッド。リクエストを編集したい時に使える func prepare(_ request: URLRequest, target: TargetType) -> URLRequest { if let moyaCachableProtocol = target as? MoyaCacheable { var cachableRequest = request ///カスタムのキャッシュポリシーを設定 cachableRequest.cachePolicy = moyaCachableProtocol.cachePolicy return cachableRequest } return request } }Hoge.swift//Hogeクラスをプロバイダに設定 let hogeProvider = MoyaProvider<Hoge>(plugins: [MoyaCacheablePlugin()]) extension Hoge: MoyaCacheable { var cachePolicy: MoyaCacheablePolicy { //採用したいキャッシュポリシーの定義 return .reloadIgnoringLocalAndRemoteCacheData } } }あとは、
hogeProvider
を用いてリクエストを送れば、Hoge
クラス内で設定したキャッシュポリシーが使われる。設定の中身
cachePolicy 説明 useProtocolCachePolicy (デフォルト)特定のURLロード要求に対して、プロトコル実装で定義されているキャッシング・ロジックがあればそれを使用します。 HTTPまたはHTTPSの場合、RFC2616というロジックを使います。 reloadIgnoringLocalCacheData ローカルに保存されたキャッシュデータを無視して取得します。 reloadIgnoringLocalAndRemoteCacheData ローカルのキャッシュデータに加え、ネットワーク途中のプロキシなども含めてキャッシュを無視して取得します。iOS13以降より有効 returnCacheDataElseLoad キャッシュデータがあれば、その有効期限などは考慮せずキャッシュを使います。なければ、取得します。 returnCacheDataDontLoad キャッシュデータがあれば、その有効期限などは考慮せずキャッシュを使います。なければ、失敗になります。 reloadRevalidatingCacheData HEADリクエストを発行し更新を確認します、更新があればリクエストを発行します。
- 投稿日:2020-07-12T13:43:58+09:00
Kerasで作成した回帰モデルをCoreML上で使用するまで
はじめに
iOSの機械学習フレームワークCoreMLではKerasで訓練済みの機械学習モデルを利用することができます。
本記事ではKerasによる回帰モデルをCoreML上で動くように変換し、実際に予測を行うまでの流れについて解説します。大まかな流れとしては以下のようになります。
- 1: Kerasで回帰モデルを作成/学習し、.h5ファイルとして出力する
- 2: coremltoolsを用いて出力した.h5ファイルを.mlmodelファイルに変換する
- 3: iOSのCoreML上で回帰モデルとして使用する
本記事における実行環境
- Python: 3.7.7
- Keras: 2.3.1
- Swift: 5
- iOS: 13.5
- Xcode: 11.5
1: 回帰モデルを.h5ファイルとして出力
https://www.tensorflow.org/tutorials/keras/regression
今回は上記チュートリアルに基づいて自動車の燃費を予測する回帰モデルを作成し、.h5ファイルとして出力します。ただし、チュートリアルのコードをそのまま実行してできたモデルをCoreMLで使用できる形式に変換しようとするとエラーが発生します。KerasをTensorflow経由でimportしていることが関係しているようです。
そのためKerasを直接importする形に書き直して実行します。下記コードをご参照ください。from __future__ import absolute_import, division, print_function, unicode_literals import pathlib import pandas as pd import keras from keras.models import Sequential from keras.layers import Dense from keras import optimizers dataset_path = keras.utils.get_file("auto-mpg.data", "https://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data") column_names = ['MPG','Cylinders','Displacement','Horsepower','Weight', 'Acceleration', 'Model Year', 'Origin'] raw_dataset = pd.read_csv(dataset_path, names=column_names, na_values = "?", comment='\t', sep=" ", skipinitialspace=True) dataset = raw_dataset.copy() dataset = dataset.dropna() origin = dataset.pop('Origin') dataset['USA'] = (origin == 1)*1.0 dataset['Europe'] = (origin == 2)*1.0 dataset['Japan'] = (origin == 3)*1.0 train_dataset = dataset.sample(frac=0.8,random_state=0) test_dataset = dataset.drop(train_dataset.index) train_stats = train_dataset.describe() train_stats.pop("MPG") train_stats = train_stats.transpose() train_labels = train_dataset.pop('MPG') test_labels = test_dataset.pop('MPG') def norm(x): return (x - train_stats['mean']) / train_stats['std'] normed_train_data = norm(train_dataset) normed_test_data = norm(test_dataset) def build_model(): model = Sequential([ Dense(64, activation='relu', input_shape=[len(train_dataset.keys())]), Dense(64, activation='relu'), Dense(1) ]) optimizer = optimizers.RMSprop(0.001) model.compile(loss='mse', optimizer=optimizer, metrics=['mae', 'mse']) return model model = build_model() class PrintDot(keras.callbacks.Callback): def on_epoch_end(self, epoch, logs): if epoch % 100 == 0: print('') print('.', end='') EPOCHS = 1000 history = model.fit( normed_train_data, train_labels, epochs=EPOCHS, validation_split = 0.2, verbose=0, callbacks=[PrintDot()]) # これでモデルを.h5ファイルに出力します。 model.save("SampleReg.h5")2: .h5ファイルを.mlmodelファイルに変換
先ほど作成した.h5ファイルをCoreMLで扱える.mlmodelファイルに変換します。
まずは変換に必要なcoremltoolsをPythonにインストールします。
$ pip install -U coremltools
また、TensorflowやKerasが手元のPython環境にインストールされていない場合は改めてインストールしておいてください。インストールが完了したら、下記のコードを実行することで.h5ファイルを.mlmodelファイルに変換することができます。
import coremltools coreml_model = coremltools.converters.keras.convert('SampleReg.h5') coreml_model.save('SampleRegonCoreML.mlmodel')3: CoreMLでモデルを使う
Xcodeを起動しiOSプロジェクトを新規に作成し、ファイル一覧に先ほど作成した.mlmodelファイルを追加します。
この.mlmodelファイルをクリックすると、以下の図のようにモデルの概要が表示されます。入力(Inputs)として9つの変数を取り、出力(Outputs)として1つの変数を出すモデルであることが確認できます。
MultiArrayというのはMLMultiArrayというCoreMLのクラスを指すようで、これはモデルの入出力を扱うための多次元配列のクラスになります。形状(Shape)やデータ型を指定して配列を作ることができます。それではこのモデルを使って実際に予測を行います。下記のコードをご参照ください。
import CoreML // .mlmodelファイルを追加した時点でモデルと同じ名前のクラスが自動的に生成され、 // さらにそれらの入力データ/出力データを扱うクラスも作られます(名前は「モデル名+Input/Output」)。 let model = SampleRegonCoreML() // MLMultiArrayはこのように配列形状とデータ型を指定して初期化することができます。 // let inputArray = try! MLMultiArray(shape: [9], dataType: MLMultiArrayDataType.double) // 一方で従来の配列から直接作ることもできます。今回は元のデータセットから抜き出した1データについて直打ちしてみました。 let inputArray = try! MLMultiArray([1.483887,1.578444,2.890853,1.925289,-0.559020,-1.604642,0.774676,-0.465148,-0.495225]) // 上の配列を用意したモデルの入力としてあてがいます。 let inputToModel: SampleRegonCoreMLInput = try! SampleRegonCoreMLInput(input1: inputArray) // 予測を行い、その結果を出力します。 if let prediction = try? model.prediction(input: inputToModel) { print(prediction.output1) }Keras側で同データについてmodel.predict()した結果と、上記コードによって出力される結果が一致することを確認します。