まっしろけっけ

めもてきなやーつ

食べるものを変えてみた

はじめに

在宅勤務になり基本家でご飯を食べることが多くなりました。
元々平日夜は自炊をしていました。
基本的に同じものをずっと食べ続けることができるので同じものを作っていたのですが多少作るのがめんどくさいなぁ〜などと思いつつも 1 食 300 ~ 400円くらいで作れるし節約にはなるしなぁと思いやっていたんですが元同僚の ひさいちくんつくりおき.jp について tweet してるのを見て良さそうかもと思い利用し始めたので before/after を書いておく

before

朝飯

元々朝飯は食べておらず火曜日と木曜日に朝ジムにいくようになりジム後はお腹が空くのでコンビニでおにぎりを買うなどしていたが 1.5 ヶ月ほど前からプロテインに切り替えた。
これはつくりおき.jpの件とは関係なくなんとなくプロテイン飲んでみるか?となって

昼飯

昼飯はだいたいコンビニで弁当なりを買って食べていたので、毎日昼の時間になるとコンビニに買いに行っていた。
コンビニに行くと弁当だけではなくなんか余計なものも買っちゃうよね...というのと減量したいときはサラダと低カロリーのものだったりを買うので割と 1000 円いかないくらいかかってしまっていた。

夕飯

夏場はあまり火を使いたくないのでオーブンレンジを活用していた。
何種類かの野菜と鶏肉を適当な大きさに切って袋に入れてそこにオリーブオイルとその日の気分で塩や醤油などを入れて全体になじむように揉んだ後にオーブン用の鉄板にクッキングシートをしいてそれの上に先のものを並べてオーブンで 30 分放置するだけでそれなりに美味しいものができる。

冬場はずっと鍋を食べている。スープは市販のものではなく粉末のかつおだしや鶏だしをベースに塩や醤油などで薄味にしていた。一回で 2,3 日分をまとめて作って残ったやつはそのまま冷蔵庫行きにしているので毎日料理する必要がなくて楽。


基本的には週の初めにその週使う野菜と肉は切っておくので週の途中で調理を行う時間を短縮していた

両方とも野菜が取りやすいし、塩分なども控えめにできるので健康的にもいいかなという感じ。
基本的に夕飯に関しては米は食べないようにしていたのでこれで終わり。

after

朝飯

特に変わらずジムに行った後だけプロテインを飲んでいる。

昼飯

つくりおき.jpから送られてきた料理を皿に取り分けてレンジでチンするだけ、ご飯は数年ぶりに稼働させた炊飯器で炊いている。
米に関してはロウカット米なるものを購入してそれに十六穀を混ぜて食べているが普通にうまい。

夕飯

夕飯も昼飯と同じでつくりおき.jpから送られてきた料理を取り分けてレンジでチンするだけ。
それだけだと足りないので夕飯も米を食べるようにした。

最後に

こんな感じでつくりおき.jpを利用しだしたことによって平日は完全に料理をしなくなり時間が増えた。
今までも休日にそれなりに手の込んだものだったりを作ったりしてたのでそれは変わらず。

つくりおき.jpは週 3 食プランで合計で 12 食届くので月 ~ 金と土日のどちらかの昼飯と夕飯という感じで食べている。
金額的にも一食 650 円で1日なら * 2 で 1300 円ほどになるので以前の料理をしていた時と比べてもすごく差があるという感じではない。
味はすごく美味しいし、料理の種類も自分では作らんだろうなって感じのものが多いのですごく嬉しいし毎日ほぼ同じものを食べていた以前と比べると今日はどれを食べようか?という楽しみができた。

nosh も試しはしたがメインは良いとして付け合わせの野菜などがやはり冷凍感があってそこがつくりおき.jpと比べると劣るなって感じだった。

体重的な変化だと 10 年以上ぶりに家で夕飯に米を食べ出したが体重に大きな変化はないというか緩やかに落ちている感じもある
コンビニで余計なものを買う機会が減ったからかもしれない...

炊飯器は 10 年以上前に 3000 円くらいで買ったやつなので買い換えようと思っている

EKS の node を managed node groups 管理に移行する

はじめに

aws.amazon.com

EKS の managed node groups にカスタム AMI と EC2 起動テンプレートのサポートが追加されました。
元々 managed node groups が出る以前から EKS を使用しておりかつカスタム AMI も使用していたので managed node groups が登場した当初に試した時はカスタム AMI 使えないならダメじゃんとなり移行を諦めましたが、サポートされたことにより移行するぞとなりました。

managed node groups を使わない場合の問題

問題というほどではありませんが、managed node groups を使わない場合 CloudFormation で node を定義して cluster にアタッチするような手法だったので EKS のコンソールを見ても node との繋がりがわかりにくいという問題がありました。
`k get nodes` で node 一覧を取得するか EC2 インスタンスの特定のタグで絞込みをすることで EKS の node なのだと把握できるくらいでしょうか?(それでも大きな問題はないんですけど)

手順

EKS cluster の作成

https://console.aws.amazon.com/eks/home#/clusters こちらからクラスター作成を選択

f:id:shiro-16:20200828115940p:plain

cluster 名とかは適当に入れます。今回は shiro16-test としています。 kubernetes の version はお好きなものを選びます。今回は最新が最高ということで選べる最新の 1.17 にしています。
クラスターサービスロールはこちらを参考に IAM ロールを作成しそれを選択します。

f:id:shiro-16:20200828115945p:plain

続いてのネットワーク周りの設定は自身の運用環境に合わせて設定してください。

f:id:shiro-16:20200828115954p:plain

ログ周りも運用に合わせて設定してください。本番稼働なのであれば全て取得しておくのが良いかもしれません。
これで入力することはほぼ終わりなので確認画面で作成をポチッとすると cluster が作成されます。(10min ほど完了まで時間がかかるかと思われます)

managed node groups 作成

EKS cluster の作成が完了し cluster の詳細画面にいくと下記のような画面が表示されていると思います。

f:id:shiro-16:20200828120005p:plain

ここでノードグループの追加を選択します。

f:id:shiro-16:20200828120020p:plain

ノードグループ名は好きな名前を入力し、ノード IAM ロールはこちらを参考に IAM ロールを作成しそれを選択します。

更に今回はカスタム AMI を使用するので起動テンプレートを使用するを選択します。
起動テンプレートの設定に関しては後述しますが、強力にキャッシュしているのかここの選択に新規で作成したテンプレートやバージョンがリロードしても表示されないことがありますが、再読み込みマークを選択すると表示されるはずです。

f:id:shiro-16:20200828120027p:plain

最後に node 数を決めて作成を開始すれば EC2 インスタンスが立ち上がり node が cluster に join してくれます。

EC2 起動テンプレート作成

https://console.aws.amazon.com/ec2/home#LaunchTemplates こちらのページで起動テンプレート作成を選択します。
起動テンプレート名や説明を入力します。次に下記のような項目があるので使用したい AMI とインスタンスタイプを選択します。

f:id:shiro-16:20200828120024p:plain

ちなみに仕事では packer + ansible でこちらの AMI に wazuh agent や stns 等をインストールした AMI を作成しています。

最後に高度な詳細 という箇所があるのでそこを開いて一番下のユーザーデータの部分にこのようなスクリプトを書きます。

MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="//"

--//
Content-Type: text/x-shellscript; charset="us-ascii"
#!/bin/bash
set -ex
B64_CLUSTER_CA=xxxxxxxxxxxxxxx
API_SERVER_URL=https://xxxxxxxxxxxx.xx.region.eks.amazonaws.com
/etc/eks/bootstrap.sh shiro16-test --kubelet-extra-args '--node-labels=eks.amazonaws.com/nodegroup=shiro16-test-nodegroup,eks.amazonaws.com/nodegroup-image=ami-xxxx' --b64-cluster-ca $B64_CLUSTER_CA --apiserver-endpoint $API_SERVER_URL

--//--

shiro16-test は自身の cluster 名
shiro16-test-nodegroup は node group 名
ami-xxxx は使用する AMI 名

B64_CLUSTER_CA と API_SERVER_URL に関しては下記のコマンドで取得可能です。

$ aws eks describe-cluster --name shiro16-test --query 'cluster.[endpoint,certificateAuthority]'
[
    "https://xxxxxxxxxxxx.xx.region.eks.amazonaws.com",
    {
        "data": "xxxxxxxxxxxxxxx"
    }
]

これで作成を行えば起動テンプレートの作成も完了です。

さいごに

AutoScale に関しては下記を見れば全てわかる

docs.aws.amazon.com


なぜ移行したではなくするなのかというと来週(8/31)から夏休みなのでその間に万が一何か起こるとだるいという理由だけです。
なので休み明けに移行します。

mackerel-agent を使って異常を検知した際に自動で復旧させる

はじめに

普段会社では監視ツールとして mackerel などを使ったりしているのですが、mackerel を使っている場合 nginx が動いている server のメモリ使用量が一定割合を超えたらエスカレが来るみたいなことをしていると思います。

こういう場合の対応は基本的に nginx を reload させてあげて終わりとなることがほとんどなのでエスカレの電話きてわざわざ人が対応するよりは自動化できた方がいいよね?という気持ちになるわけです。
でそれが mackerel-agent を使えばできますという話。

mackerel-agent の設定

まずは mackerel-agent の conf を用意します。

mackerel-plugin-memory-checker.conf

[plugin.checks.memory]
command = "/usr/local/bin/memory_checker"
action = { command = "bash -c '[ \"$MACKEREL_STATUS\" != \"OK\" ]' && systemctl reload nginx", user = "root" }

これは command が正常に終了しなかった際に action を実行するという意味です。

memory_checker の中身はこんな感じ

#!/usr/bin/env ruby

metrics = {}
File.foreach("/proc/meminfo") do |line|
  if md = line.match(/(MemTotal|MemAvailable):[\s]+([0-9]+)/)
    metrics[md[1]] = md[2].to_i
  end
end

available_percentage = metrics["MemAvailable"] / metrics["MemTotal"].to_f * 100

if available_percentage < 20
  puts "Memory WARNING: #{available_percentage}%"
  exit 1
else
  puts "Memory OK: #{available_percentage}%"
end

メモリの残り容量が 20% をきっていたら exit 1 で異常終了させるというコードになります。
これによって mackerel-agent は conf で記述した action 内のコマンドを実行してくれます。

さいごに

対応方法がほぼ固定化しているエスカレであれば mackerel-agent の機能などを使ってどんどん楽をしていくのが良いと思います。

深夜も平和に寝れるしね

高速に機能開発を行う際の思考

はじめに

僕自身は普段の開発において手が早いと言われることが多く、現職も含めて 3 社全てでそう言われてきたので実際にそうなのだろうなという気がしている。
見積もりをする際も「 n 時間(またはポイント)ですね」と答えると「じゃ他の人ならそれの 2 〜 3 倍くらいかな」みたいな会話をされたこともある。

何故自分は手が早いんだろうか?他の人との違いは何なのだろうか?というのを考えはじめたのだが、他人の頭の中を覗くのは不可能なので
一般的な機能開発に絞って自身がどのような思考で開発を行なっているのか?というのを考えてみた。(他の部分でもこういう思考をしているかもしれないが今回は機能開発の際にどういう思考になっているかしかまとめません)

要件定義

新しい機能を作る際はだいたい誰かしらが◯◯という課題を解決するために◯◯という機能を作りたいというのから始まり、本当にユーザが求めてるのはその機能なのか?という話だったりその機能で課題を解決できるのか?みたいな話をすると思うんですよ。この段階では特にそれについて考える以上のことはないので省略しますが、大事なのはこの後の機能を作る際の仕様を固めていく要件定義の段階。

結論から言うと要件定義が終わった段階で自身の頭の中には動くコードが既に出来上がっています。(自分の中ではコードが降って来たと表現している)

何故そのようなことになるかと言うと大事なのはパターンなのかなと。

何かの機能を作るとなった場合、ほとんどの場合(極)一部だけ同じような仕様や似たような仕様の場合がほとんどだと僕は思っていて、そういう過去に経験した似たパターンを組み合わせることによって新しい 1 つの機能を作ることが可能になると考えています。

例えば数値を入力させる場合上限と下限は幾つが良いのか?というのも要件定義に含まれると思いますが、このような入力に上限/下限を設けるのも一つのパターンとして捉えています。

このように細かいパターンを組み合わせることによって一つの機能が出来上がると捉えているので要件定義が終わった段階で僕の頭の中にはパターンが積み重なって出来た 1 つの機能が出来上がっているわけです。

さらにこのパターンは頭の中で実際のコードと紐づいているんですよね。先の数値の上限/下限であれば Validation が必要で Rails だと model に greater_than_or_equal_to などを使って記述するのような感じで。
なのでこのパターンが積み重なって 1 つの機能が出来上がった段階でそのパターンに紐付く実際のコードも出来上がるということになります。

全てが自身の頭の中にあるパターンに当てはまらないかもしれませんが、細かく分解すればほぼ当てはまるしもし当てはまらなくても当てはまらないということが発生することがまだ知らないことがあってそれを解決できるという楽しさに繋がっていると感じています。当てはまらない場合も機能全体が当てはまらないのではなくその機能の極一部なので他の部分のコードは既に頭の中では出来上がっているので当てはまらない部分を集中して考えるだけになります。

実際にコードを書く

コードを書く際は頭の中にあるコードをタイピングするだけなので、その際にも他のことを考えています。
例えば何かユーザにフォームから入力される場合は上記でまとめたパターンに漏れがないか?を頭の中で様々な入力に対してコードを走らせてみてみたり、仕様の漏れを探すということをすることが多い気がします。でこの仕様の漏れなどがあった場合はそれを test として書きどのような結果が適切なのかをコードに落とし込んでおく。

1 日で全てのコードを書き切れる程度の機能ならいいのですが、大きい機能だとそういう訳にもいかないので基本的に必ず機能単位でのキリが良いところで終わりにします。
これは翌日などに続きをするとなった際にどこまでやったっけ?と思い出すのを少なくするためです。
また、1 日で終わらない場合は翌日のコードを書き始める前に仕様をもう一度確認して頭の中にパターンを思い出させるということをやります。

コードを書く際にこれも大事なことだと思っているのですが、帰宅中や犬の散歩,お風呂などで書いたコードに対してリファクタリングできないか?をよく考える気がします。
タイピングしている際もここは DRY に出来るから module にまとめておくかみたいなことはよくあるのですが、一旦時間を置いて自身のコードと向き合うというのが重要かと思います。この際はビジネスロジックに関して考えるとこはせずあくまでリファクタリングに関してのみ考えています。(ビジネスロジックまで考えると仕事感が出すぎるので好きなリファクタだけを考えている)
これによってレビュー時の指摘なども減らせます。

その後

そのあとは実際に動作検証とかをしてリリースになると思うので特別考えていることはないですが、 test に書いてあることを何回も動作検証を行わないとか決済系以外はリリース後に問題(バグ)があってもまぁなんとか出来るだろうという考えでいるのでガンガンリリースしようという考えだという程度かなと思います。

さいごに

ということで機能開発を行う際にどのような思考で開発を行なっているか?をまとめてみた。
一文で表すと

パターンを蓄積しパターンを適切に配置、パターンと実際のコード(処理)を紐付けておくことによって高速に機能開発が出来ている

というのが僕が手が早いと言われる理由の一端かなともちろんこれが全てではないと思うので機能開発に関わらず書くかもしれない。


ちなみにこの記事は 7/15 に書いていたのですが同僚ライバルの pyama が同じようなテーマ(彼の場合は高速に開発するための自身の行動をどうするか?みたいな話)でブログを更新してたので公開を先延ばしにしました。

エンジニアとしての境界を超えることについて

はじめに

web service というものを開発するエンジニアには サーバサイド/インフラ/フロントエンド/iOS/Android などのそれぞれの専門(強み)を持ったエンジニアが存在していると思います。その専門性を境界として見た際に越境する/しないエンジニアではどのような違いがあるのかというのを自身の経験などから僕自身が考えている事をまとめていきます。

自身について

10 年以上お金を貰ってソフトウェアエンジニアをやっていて、その過程でサーバサイド/インフラ/フロントエンド/Android に関しての実務を一定期間行ってきたという経歴があります。現在は主にサーバサイド/インフラを中心として minne というサービスのシニアエンジニアリングリードというものをやっている。

書いたような領域以外でも DevOps みたいな領域の違いみたいなものもあると思いますが、僕自身 Dev と Ops に違いがあるというのは全然感じたことがなく Effective DevOps という書籍を読んでもふむ〜?となったし弊社の VPoE の @hsbt に 「DevOps 上手い」というような事を言われた記憶があるのだが(うろ覚え)なるほど(よくわかっていない)となった記憶がある。

越境する/しないの違い

上記で説明した通り僕自身は越境した側の人間でしてなかった時とした後で実際にどういう変化があったのか?というと細かくあげるといっぱいあるのだが大きいものを 2 点ほどあげる。他にも

1. それぞれの苦労が理解できる

それぞれの領域のエンジニアが、(特に)領域が繋がる部分でどのような事で苦労するか?やどのような状態なら嬉しいか?がわかるようになった。

例えば API のレスポンスで json を返す際に特定の値が基本的には文字列だが数字のみだったら int 型のような API だった場合にアプリエンジニアとしてはとても困るという事が発生するんですけど、iOSAndroid 側に越境した事がある開発者であればその事を理解できているはずなのでそもそもそんなレスポンスになる API は作らないという事になるはず。

この事によってリリース後に気づいて慌てて修正するだったり、リリース前の検証で発覚して修正をするというような手間が発生せず効率的に開発ができるという事になります。越境した事があるエンジニアがチームに 1 人いるだけでも仕様検討やレビューの段階でこのような事に指摘をしてくれるのでチームとしての生産性も向上すると思われます。

2. 各領域の話が理解できる

これは自分の現在の立場的によかったと感じるというのが大きいかもしれないのですが、シニアエンジニアリングリードというのは事業部のリードエンジニア兼エンジニアリングマネージャーなのでシステム全体に関して色々な事を考えたり時には各エンジニアに技術的な仕様について相談されたりという事があったり、部署のエンジニアを評価するという事が必要になってくる。

上記のような事があり、越境しているおかげでどういう事がしたいのか?だったり技術的にどんな高度な事をしているのか?を判断できるようになったという部分がある。

やってみたと実務の違い

それぞれの領域でとりあえずチュートリアルやって動くもの作りましたというのだけで越境したと捉えられる人もいるかもしれませんが、それだけでは継続的に開発をして得る事のできる知識とは雲泥の差がある為、実務にこだわらずともそれぞれの領域と関わる形(Android アプリなら API を利用する等)で継続的に開発を行ったことがあるという事を越境した事があると捉えています。

越境について

それじゃみんな越境すればいいのか?とか全ての技術を平均的にできたらいいのか?という話になると思うんですが、これに対する僕の考えを書いておくと

みんな越境すればいいのか?

これに対する僕の考えは(経験年数が浅い人ほど)興味があるならして欲しい。なぜかというと先に述べた 1 のメリットが大きいという考えからです。
また、越境した結果その分野が実はその人が一番楽しいと思える分野かもしれないからです。僕自身はサーバサイド/インフラから始まり一通り終えてサーバサイド/インフラがやっぱり楽しいと感じましたが、どこかでサーバサイド/インフラより面白いと感じるものがあったのならそこに留まっていたと思うし、仕事をするなら楽しいと思える事をやりたいじゃないですか。経験年数が浅い人ほどというのをつけたのは歳を取ると越境する体力みたいなのが徐々になくなってくるのかなぁと思うからです(個人の感想)

全ての技術を平均的にできたらいいの?

これ今回一番書きたかったことかもしれないんですが、僕自身としてはこの答えは no です。
時間は有限ですし、僕自身は全ての技術を高レベルで習得するというのは無理だと考えています。なので特定の技術領域に軸足を置きつつ他の領域のことも理解しているというのが良いと思います。(よく T 型と呼ばれるやつ)
一点だけを突き詰めていくんだという強い気持ちがある人はその限りではなくその一点を突き詰めていって欲しいですし。

なぜ上記のように考えるかというと僕のチームで働いている人には自身の市場価値を意識して欲しいなという考えがあって、僕のチームはそこまで多くないですがこの業界は転職する人も割と多くいる業界なので(勿論そうなって欲しくはないし、一緒に働けなくなるのは悲しいという前提はあるものの)チームメンバーが転職するとなる自体はある程度覚悟しているんですよ。

でその転職するとなったメンバーが一番入りたいと思った会社から採用されるエンジニアになって欲しいという思いがあるからで恐らく平均的にアプリからバックエンドまで出来るエンジニアと iOS アプリに関して深い理解があり技術的にも優れているエンジニアが iOS エンジニアのポジションに応募した場合に後者が採用される率の方が高いわけですよ。更にいうと iOS エンジニアとしての技量が同じくらいだった場合はその他の領域の事が少し出来るという人の方がチームとして働く上でプラスに捉えられる事が多いと思うのでそういう意味もあります。

全ての技術を平均的に出来るエンジニアというのはチームとしてはとても扱いやすいというのがあると思います。どこかの領域のリソースが足りなかったら今週はそこのタスクをやってもらって〜と動けるので...ただそういう便利屋さん的な人がいざ外に出るとなった際に便利屋さん的ポジションを大々的に求めている会社というのは少ないと思います。(スタートアップを除く、スタートアップでも T 型とかの方がいいのかなぁ?働いた事ないので知らない)
自ら望んでそういうエンジニアになるというのは良いと思いますが、強制的にそういうエンジニアにするというのは僕個人としてはその人のエンジニアとしての未来を潰しているという感じがしてしまうので行なっていません。

まとめ

長々と書きましたが、うちのチームでは基本的に「好きな(自分がやりたい)技術をやればいいよ」と言っており実際にどういうエンジニアになりたいか?は各々に任せています。

僕はメンバーが転職した際は転職先の人に強いエンジニアが来たって思って欲しいんですよね(勿論そうなって欲しくはないし、一緒に働けなくなるのは悲しいという前提はあるものの大事な事なので 2 回)

エンジニアとしての境界について書きましたが似たような事は他の職種間でも当てはまるのでは?と思います。

長文の日本語を書くのはまだまだ下手...

Golang で ImageMagick を使わずに画像をいじる②

はじめに

shiro-16.hatenablog.com

前回は上記の記事で変換/切り抜き/合成あたりをやりました。
今回は画像のリサイズを行いたいと思います。

画像のリサイズ

まずは画像の縦横のサイズを半分にしてみます。

package main

import (
	"flag"
	"fmt"
	"image"
	"image/jpeg"
	"os"
	"golang.org/x/image/draw"
)

func main() {
	flag.Parse()
	args := flag.Args()

	f, err := os.Open(args[0])
	if err != nil {
		fmt.Println("open:", err)
		return
	}
	defer f.Close()

	img, _, err := image.Decode(f)
	if err != nil {
		fmt.Println("decode:", err)
		return
	}

	fso, err := os.Create(args[1])
	if err != nil {
		fmt.Println("create:", err)
		return
	}
	defer fso.Close()

	rct := img.Bounds()
	
	dst := image.NewRGBA(image.Rect(0, 0, rct.Dx() / 2, rct.Dy() / 2)) // ここで 1/2 のサイズ指定をしている
	draw.CatmullRom.Scale(dst, dst.Bounds(), img, rct, draw.Over, nil)

	jpeg.Encode(fso, dst, &jpeg.Options{Quality: 100})
}

元の画像が 150x150 なので 1/2 することで 75x75 の画像が出来上がります。
それが下記の画像

元画像 切り取り後
f:id:shiro-16:20200529113555j:plain f:id:shiro-16:20200605113615j:plain

今回は width と height 両方を 1/2 しましたが image.Rect(0, 0, rct.Dx() / 2, rct.Dy() / 2) の部分を image.Rect(0, 0, 100, 50) などに変更すると下記のような画像が出来上がります

元画像 切り取り後
f:id:shiro-16:20200529113555j:plain f:id:shiro-16:20200605114055j:plain

アスペクト比を無視して縮小が行われるので潰れた画像が出来上がってしまう。

そこでアスペクト比を維持したまま長辺のサイズを指定するというのを実現するのが下記のコード

// 上記の defer fso.Close() まで略
	arg, _ := strconv.ParseFloat(args[2], 64)

	rct := img.Bounds()

	var w, h int
	if rct.Dx() > rct.Dy() { // 横長画像
		m := arg / float64(rct.Dx())
		w = int(arg)
		h = int(float64(rct.Dy()) * m)
	} else { // 縦長画像
		m := arg / float64(rct.Dy())
		w = int(float64(rct.Dx()) * m)
		h = int(arg)
	}

	dst := image.NewRGBA(image.Rect(0, 0, w, h))
	draw.CatmullRom.Scale(dst, dst.Bounds(), img, rct, draw.Over, nil)

	jpeg.Encode(fso, dst, &jpeg.Options{Quality: 100})
}

わかりやすいように横長の画像を使用

150 × 84 を使用して下記のように長辺(今回は横)を 100 で指定する

$ go run main.go test.jpg out.jpg 100

すると下記のような 100 × 56 が出来上がる

元画像 切り取り後
f:id:shiro-16:20200605114910j:plain f:id:shiro-16:20200605115217j:plain

終わりに

前回の記事の内容と組み合わせると golang だけで様々な画像加工ができるのではないでしょうか?
計算とかめんどくさいという人は https://github.com/nfnt/resize を使うのも良いかもしれません。

Golang で ImageMagick を使わずに画像をいじる

はじめに

最近動的に画像を変換するみたいなことをやっていて ImageMagick を使えば簡単にできるんですが
Golang の場合 ImageMagick を使わなくても様々な画像の加工が可能なので ImageMagick を使わずにやった時のメモ

画像形式の変換

最初は画像形式の変換の説明

jpg を png にしたり webp にしたりなどです。

ちなみに Golang では webp の Encode は標準でサポートされていないので今回は https://github.com/chai2010/webp を使用します。

package main

import (
	"flag"
	"fmt"
	"github.com/chai2010/webp"
	"image"
	"image/gif"
	"image/jpeg"
	"image/png"
	"os"
	"strings"
)

func main() {
	flag.Parse()
	args := flag.Args()

	f, err := os.Open(args[0]) // 元画像読み込み
	if err != nil {
		fmt.Println("open:", err)
		return
	}
	defer f.Close()

	img, _, err := image.Decode(f) // 元画像デコード
	if err != nil {
		fmt.Println("decode:", err)
		return
	}

	fso, err := os.Create(args[1]) // 変換後画像作成
	if err != nil {
		fmt.Println("create:", err)
		return
	}
	defer fso.Close()

	slice := strings.Split(args[1], ".")

	switch slice[len(slice)-1] { // 出力画像の拡張子によってエンコードを変える
	case "jpeg", "jpg":
		jpeg.Encode(fso, img, &jpeg.Options{})
	case "png":
		png.Encode(fso, img)
	case "gif":
		gif.Encode(fso, img, nil)
	case "webp":
		webp.Encode(fso, img, &webp.Options{Lossless: true})
	default:
	}
}

こんなコードを用意して下記のように実行すると変換されます。

$ go run main.go test.jpg hoge.png
$ go run main.go test.jpg hoge.webp
元画像 変換後(gif)
f:id:shiro-16:20200529113555j:plain f:id:shiro-16:20200529113617g:plain

画像の切り抜き

続いて画像の一部を切り抜く方法を説明

package main

import (
	"fmt"
	"image"
	"image/jpeg"
	"os"
)

type SubImager interface {
	SubImage(r image.Rectangle) image.Image
}

func main() {
	f, err := os.Open("test.jpg")
	if err != nil {
		fmt.Println("open:", err)
		return
	}
	defer f.Close()

	img, _, err := image.Decode(f)
	if err != nil {
		fmt.Println("decode:", err)
		return
	}

	fso, err := os.Create("out.jpg")
	if err != nil {
		fmt.Println("create:", err)
		return
	}
	defer fso.Close()

	cimg := img.(SubImager).SubImage(image.Rect(50, 0, 150, 100))

	jpeg.Encode(fso, cimg, &jpeg.Options{Quality: 100}) // Quality を指定しないと荒すぎる画像が出来上がるよ
}

image.Rect で横は 50px ~ 150px 縦は 0px ~ 100px まで切り取るという指定をしています。
こうして出来上がったのが下記の画像

元画像 切り取り後
f:id:shiro-16:20200529113555j:plain f:id:shiro-16:20200529115148j:plain

画像の合成

最後に画像の合成

2 つの画像を重ね合わせるなどして合成する方法を説明します。

package main

import (
	"fmt"
	"golang.org/x/image/draw"
	"image"
	"image/color"
	"image/jpeg"
	"os"
)

type SubImager interface {
	SubImage(r image.Rectangle) image.Image
}

func main() {
	f, err := os.Open("test.jpg")
	if err != nil {
		fmt.Println("open:", err)
		return
	}
	defer f.Close()

	img, _, err := image.Decode(f)
	if err != nil {
		fmt.Println("decode:", err)
		return
	}

	fso, err := os.Create("out.jpg")
	if err != nil {
		fmt.Println("create:", err)
		return
	}
	defer fso.Close()

	m := image.NewRGBA(image.Rect(0, 0, 200, 200)) // 200x200 の画像に test.jpg をのせる
	c := color.RGBA{0, 0, 255, 255} // RGBA で色を指定(B が 255 なので青)

	draw.Draw(m, m.Bounds(), &image.Uniform{c}, image.ZP, draw.Src) // 青い画像を描画

	rct := image.Rectangle{image.Point{25, 25}, m.Bounds().Size()} // test.jpg をのせる位置を指定する(中央に配置する為に横:25 縦:25 の位置を指定)

	draw.Draw(m, rct, img, image.Point{0, 0}, draw.Src) // 合成する画像を描画

	jpeg.Encode(fso, m, &jpeg.Options{Quality: 100})
}

こうして出来上がったのが下記の画像

元画像 合成後
f:id:shiro-16:20200529113555j:plain f:id:shiro-16:20200529122847j:plain

上記では青い画像を作成して合成を行なったがもちろん既存の画像を使うことも可能で下記のように書く

// 上記の defer fso.Close() まで略
        f2, _ := os.Open("hoge.jpg") // 元になる画像
        img2, _, _ := image.Decode(f2)

        rgba := image.NewRGBA(image.Rectangle{image.Point{0, 0}, image.Point{200, 200}}) // RGB形式の画像を用意する

        draw.Draw(rgba, image.Rectangle{image.Point{0, 0}, img2.Bounds().Size()}, img2, image.Point{0, 0}, draw.Src) // 元になる画像を描画する

        rct := image.Rectangle{image.Point{25, 25}, img2.Bounds().Size()} // 元画像への描画位置を決める

        draw.Draw(rgba, rct, img, image.Point{0, 0}, draw.Src) // 乗せる画像を描画

        jpeg.Encode(fso, rgba, &jpeg.Options{Quality: 100})
}

これで出来上がるのが下記の画像

元画像 合成する画像 合成後
f:id:shiro-16:20200529125808j:plain f:id:shiro-16:20200529113555j:plain f:id:shiro-16:20200529125837j:plain

終わりに

ちょっと長くなってしまったので今日はここまで
続きは次週とかに書くかもしれない