画像生成でポーズを指定するとき、ControlNet の OpenPose を使っている人は多いと思います。棒人間の骨格を渡すと、その姿勢どおりの人物が出てくるあれです。
私も毎回お世話になっているのですが、しばらく使ううちに、手元の作業でだけ都合が悪い場面が2つ出てきました。
- 写真から骨格を起こすと、下半身が無いことがある。上半身しか写っていない写真だと当然そうなるのですが、生成したいのは全身だったりします
- 抽出した骨格のサイズが、生成したい画像のサイズと合わない。横長の写真から起こした骨格を縦長のキャンバスへ持っていくと、人物が横に伸びたり見切れたりします
どちらも「毎回手で直せば済む」話ではあります。ただ、私はポーズを貯めて使い回したいタイプで、1枚ごとに手で直していると、そのうち直すのが面倒になってポーズのストックが増えなくなります。
そこで、写真を1枚渡すと骨格 PNG が出てくる小さな道具を作りました。この記事はその作り方です。明日から自分の手元で同じものを作れるように、実際に動いているコードをそのまま載せます。
記事の中では要点だけを抜き出して説明していくので、そのまま動かせる形は GitHub に置きました。手を動かしながら読みたい方はこちらをどうぞ。
先に一番大事なことを書いておきます。この記事で使う OpenPose の検出器と学習済みモデルは、商用利用ができません。 個人利用や研究目的なら問題ありませんが、仕事で使う・成果物を売る場合は別の話になります。詳しくは後半の「ライセンスの落とし穴」で書きます。私はこれを作り終えたあとに気づきました。
作ったもの
こういう骨格 PNG が出てきます。写真を1枚渡すだけで、全身が生成サイズの中央に収まった状態で保存されます。
見た目はただの棒人間ですが、この PNG の中には18点の座標がそのまま入っています。あとで関節を動かしたくなったときに、絵から位置を読み取り直す必要がありません。
やっていることは4段階です。
| 段階 | 中身 |
|---|---|
| 1 | 写真から OpenPose の18点キーポイントを取る |
| 2 | 脚が検出されていなければ、胴の長さから膝と足首を作って全身にする |
| 3 | 全身が収まるように、生成サイズ(私の場合は768×1024)へ拡大縮小して配置する |
| 4 | 描いた骨格 PNG に、18点の座標そのものを埋め込んで保存する |
4がこの記事のいちばん言いたいところです。骨格画像は見た目としては棒人間の絵でしかないので、あとから「この関節をちょっと動かしたい」と思っても、絵から関節の位置を読み取り直すしかありません。座標を画像の中に一緒に持たせておけば、その画像は編集できる素材になります。
準備
必要なのは2つだけです。
pip install controlnet_aux Pillow
controlnet_aux は ControlNet の前処理器を集めたパッケージで、OpenPose もこの中に入っています。
学習済みモデルは初回実行時に Hugging Face から自動でダウンロードされます。取りに行く先はこのリポジトリです。
姿勢の検出に使われるのは annotator/ckpts/body_pose_model.pth で、約200MBあります。保存先は Hugging Face の既定のキャッシュで、macOS・Linux なら次の場所です。
~/.cache/huggingface/hub/models--lllyasviel--ControlNet/snapshots/<ハッシュ>/annotator/ckpts/body_pose_model.pth
最初の1回だけネットにつながっている必要があります。2回目以降はこのキャッシュを使うので、オフラインでも動きます。
1. 写真から18点を取る
ここは controlnet_aux に任せます。
import numpy as np
from PIL import Image
from controlnet_aux import OpenposeDetector
det = OpenposeDetector.from_pretrained("lllyasviel/ControlNet")
src = Image.open("input.jpg").convert("RGB")
poses = det.detect_poses(np.array(src))
poses には検出された人物ぶんの結果が入ります。各人物の body.keypoints が18個のリストで、検出できなかった関節は None が入っています。ここが後で効いてきます。
18点の並びは決まっていて、この記事で扱うのは次の7点です。
| 番号 | 部位 |
|---|---|
| 1 | 首 |
| 8 / 11 | 右腰 / 左腰 |
| 9 / 12 | 右膝 / 左膝 |
| 10 / 13 | 右足首 / 左足首 |
座標は0〜1に正規化された値で返ってきます。画像の幅・高さに対する割合ですね。これは後の拡大縮小でありがたい仕様でした。
2. 無い脚を作る
上半身しか写っていない写真だと、膝(9・12)と足首(10・13)が None のままです。ここを胴の長さから推測して埋めます。
理屈はシンプルで、首から腰までの長さを1つの物差しにするというものです。人体の比率はおおむね安定しているので、胴の長さが分かれば太ももとすねの長さもだいたい決まります。
from controlnet_aux.open_pose.body import Keypoint
THIGH_RATIO, SHIN_RATIO, FOOT_INWARD = 0.72, 0.70, 0.18
def synth_legs(kps):
"""膝(9,12)・足首(10,13)が無ければ首(1)・腰(8,11)から合成する。"""
kps = list(kps)
neck, rhip, lhip = kps[1], kps[8], kps[11]
if neck is None or rhip is None or lhip is None:
return kps
hip_y = (rhip.y + lhip.y) / 2.0
T = hip_y - neck.y # 首から腰までを物差しにする
thigh, shin = THIGH_RATIO * T, SHIN_RATIO * T
cx = (rhip.x + lhip.x) / 2.0 # 腰の中心(内股ぎみに寄せるため)
for knee_i, ank_i, hip in [(9, 10, rhip), (12, 13, lhip)]:
if kps[knee_i] is None:
kps[knee_i] = Keypoint(x=hip.x, y=hip.y + thigh)
if kps[ank_i] is None:
kps[ank_i] = Keypoint(
x=hip.x + FOOT_INWARD * (cx - hip.x),
y=hip.y + thigh + shin,
)
return kps
比率の 0.72 と 0.70 は、手元の写真で何度か試して落ち着いた値です。厳密な人体計測から出したものではないので、好みで動かしてください。
FOOT_INWARD だけ少し補足します。腰から真下へ足首を下ろすと、両脚がまっすぐ平行に伸びた立ち姿になります。これが妙にロボットっぽくて、生成結果も硬くなりました。足首を腰の中心へ18%だけ寄せると、ほんのり内股になって自然に見えます。棒人間なのに、こういうところで印象が変わるのは面白いところでした。
この関数は「無いものを作る」ので、元の写真に写っていない情報を自動で追加しています。座っている人の上半身だけを渡すと、立っている脚が生えます。あくまで「全身の骨格が欲しいときの叩き台」として使ってください。
3. 生成サイズへ収める
ここが地味に効いた部分です。
抽出した骨格をそのまま使うと、元の写真の縦横比に引きずられます。私は 768×1024 の縦長で生成しているので、横長の写真から起こした骨格は左右が余って人物が小さくなります。
やることは、全キーポイントの外接範囲を求めて、縦横比を保ったままキャンバスへ収めるだけです。
FRAME_W, FRAME_H = 768, 1024
TOP_MARGIN, SIDE_MARGIN = 24, 24
all_x = [k.x for kps in new_poses for k in kps if k is not None]
all_y = [k.y for kps in new_poses for k in kps if k is not None]
xmin, xmax, ytop, ybot = min(all_x), max(all_x), min(all_y), max(all_y)
W0, H0 = src.size
Wc, Hc = (xmax - xmin) * W0, (ybot - ytop) * H0 # 元画像上での人物の大きさ
scale = min((FRAME_W - 2 * SIDE_MARGIN) / Wc,
(FRAME_H - 2 * TOP_MARGIN) / Hc) # はみ出さないほうに合わせる
x_off = (FRAME_W - Wc * scale) / 2.0 # 横は中央
y_off = TOP_MARGIN # 縦は上に寄せる
def to_frame(k):
if k is None:
return None
fx = x_off + (k.x - xmin) * W0 * scale
fy = y_off + (k.y - ytop) * H0 * scale
return Keypoint(x=fx / FRAME_W, y=fy / FRAME_H) # 再び0〜1へ戻す
scale を2つの比の小さいほうにするのがポイントで、こうすると縦にも横にもはみ出しません。よくある「アスペクト比を保ったまま枠に収める」計算です。
縦位置だけ中央ではなく上に寄せています。人物を縦中央に置くと、頭の上と足元に同じだけ余白ができて、記念写真のような間延びした構図になりました。頭上を24pxだけ空けて上に詰めると、全身がしっかり入った感じになります。
あとは描くだけです。controlnet_aux に描画関数がそのまま入っています。
from controlnet_aux.open_pose import util
canvas = np.zeros((FRAME_H, FRAME_W, 3), dtype=np.uint8)
for kps in framed:
util.draw_bodypose(canvas, kps)
黒い背景に色付きの棒人間が描かれます。ControlNet が期待している見た目そのものです。
4. 座標をPNGに埋め込む
そしてここが本題です。
PNG には tEXt チャンクという、テキストを好きに入れておける場所があります。生成画像のメタデータにプロンプトが入っているのを見たことがある方も多いと思いますが、あれと同じ仕組みです。ここに18点の座標を JSON で入れておきます。
import json
from PIL import Image, PngImagePlugin
META_KEY = "openpose"
def _kp_to_pair(kp):
if kp is None:
return None
if hasattr(kp, "x"):
return [float(kp.x), float(kp.y)]
return [float(kp[0]), float(kp[1])]
def build_openpose_meta(people, frame=(768, 1024)):
out = [{"keypoints": [_kp_to_pair(kp) for kp in pose]} for pose in people]
return {"version": 1, "frame": [int(frame[0]), int(frame[1])], "people": out}
def save_with_meta(img, path, meta):
info = PngImagePlugin.PngInfo()
info.add_text(META_KEY, json.dumps(meta, separators=(",", ":")))
img.save(path, pnginfo=info)
def read_meta(path):
with Image.open(path) as im:
raw = im.info.get(META_KEY)
return None if raw is None else json.loads(raw)
version を入れてあるのは、あとで形式を変えたくなったときに古い画像を見分けるためです。frame を持たせているのは、座標が0〜1の正規化値なので、どのサイズを前提に描いたかが分からないと復元できないからです。
これで、できあがった PNG は「見れば棒人間、開ければ座標データ」という二重の顔を持ちます。
>>> read_meta("ske_ve_000.png")["people"][0]["keypoints"][1]
[0.5013020833333334, 0.15234375]
私はこの座標を読み込むための小さな HTML ツールも作りました。骨格 PNG を放り込むと、18点が丸で表示され、関節をドラッグで動かして保存し直せます。
面白いのは、座標が入っていない普通の骨格画像を放り込んだときです。そのときツールは絵を1枚のスプライトとして扱い、全体の移動と拡大縮小しかできません。関節を個別に動かすには、どの丸がどの関節なのかを知っている必要があるからです。
同じツール・同じ見た目の画像なのに、中に座標があるかどうかだけで、できることが変わります。 これが「埋め込んでおく」ことの実利でした。
貯めた骨格 PNG は、そのまま ControlNet に投げてもいいし、少し直してから投げてもいい。ポーズが「使い捨ての前処理結果」ではなく「手元の素材」になったのが、この仕組みでいちばん変わったところでした。
ライセンスの落とし穴
さて、冒頭で予告した話です。
この道具がひととおり動いたあと、私は「これ、売れるんじゃないか」と思いました。似たようなものが有料で出ていないか調べ、ついでに使っているパッケージのライセンスも見ておくかと軽い気持ちでファイルを開いたら、こう書いてありました。
# openpose
# Original from CMU https://github.com/CMU-Perceptual-Computing-Lab/openpose
# 2nd Edited by https://github.com/Hzzone/pytorch-openpose
# 3rd Edited by ControlNet
# 4th Edited by ControlNet (added face and hand pose estimation)
# 5th Edited by ControlNet (added yaml and remove extra requirements)
# This preprocessor is licensed by CMU for non-commercial use only.
最後の1行です。「この前処理器は CMU により非商用利用に限ってライセンスされています」。
ここが分かりにくいところなのですが、controlnet_aux というパッケージ全体のライセンスは Apache License 2.0 で、これは商用利用できます。私も最初にそれを見て安心していました。ところがその中の OpenPose の部分だけが、上流の CMU のライセンスを引き継いで非商用限定になっています。
学習済みモデル(body_pose_model.pth)のほうも同じです。ダウンロード元のリポジトリ全体には openrail というライセンスが表示されていますが、モデルカードを読むと、このファイルは「サードパーティのモデル」として扱われていて、個別の条件は示されていません。つまり上流の CMU の条件がそのまま生きていると読むのが自然です。
CMU の原文にはこう書かれています。
a personal, non-exclusive, non-transferable license to use the Software for noncommercial research purposes
「非営利の研究目的」に限る、と明記されています。対象も「あなたの所属する学術機関か非営利組織、または個人」です。
何がだめで、何が問題ないのか
私が整理した結論はこうです。
| やること | 可否 |
|---|---|
| 個人で使う・研究に使う | ✅ 問題なし |
| この記事のように無料で作り方を公開する | ✅ 問題なし |
| ツール一式を売る | 🔴 だめ。買った人が非商用限定の検出器を使うことになる |
| 学習済みモデルを自分のリポジトリに置いて配る | 🔴 だめ。再配布にあたる |
| GitHub にコードを置き、モデルは各自でダウンロードしてもらう | ✅ 問題なし |
自分で書いた部分(脚の合成・サイズへのフィット・座標の埋め込み)は自分の著作物なので、この制約を受けません。 制約がかかるのは、あくまで CMU 由来の検出器と学習済みモデルを使う部分です。
もし商用でどうしても使いたい場合、検出器を差し替えるという道は残っています。OpenPose の18点という形式は ControlNet 側が期待している入力の仕様であって、「その18点をどうやって求めるか」は縛られていません。Apache 2.0 で公開されている姿勢推定(MediaPipe Pose や ViTPose、RTMPose あたり)で点を取り、18点の並びに変換すれば、理屈のうえでは同じことができます。
ただし、キーポイントの定義がぴったり1対1で対応しているわけではないので(MediaPipe は33点、COCO 系は17点)、変換の設計と精度の検証がそれなりに要ります。私はまだ試していません。ここは「できるはず」までの話として読んでください。
教訓
パッケージ全体のライセンスと、その中の一部分のライセンスは別物です。
pip show で出てくるライセンス表記や、リポジトリのトップに貼られたバッジだけを見て安心すると、私のように作り終えてから気づきます。使っているモジュールのソースを開いて、ファイル冒頭のコメントを読むのがいちばん確実でした。数秒で済みます。
学習済みモデルも同じで、配布リポジトリのライセンス表記が、その中の全ファイルに一律で適用されているとは限りません。「これは第三者のモデルです」と書いてあったら、その第三者まで辿る必要があります。
標準のエディタとの棲み分け
もうひとつ正直に書いておきます。
調べているうちに、ControlNet の拡張機能には関節を編集するエディタが最初から組み込まれていることを知りました(sd-webui-openpose-editor)。前処理を実行してから編集ボタンを押すと、キーポイントを動かして送り返せます。手や顔にも対応しています。無料です。
つまり「関節をドラッグして直す」という部分については、ControlNet を使っている人はすでに持っているわけです。私が作った HTML ツールは、この点では車輪の再発明でした。
では今回の道具に残る意味は何かというと、次の3つだと思っています。
- 写っていない脚を補って全身にする(標準のエディタは検出結果を編集する道具なので、無いものは無いまま)
- 生成サイズへ自動で合わせる(縦長・横長の行き来を毎回手で直さなくてよくなる)
- 座標を画像に埋めて素材として貯める(「その場で直して送る」ではなく「ストックして使い回す」ための形)
標準のエディタはその場の1枚を直すための道具で、今回作ったものはポーズを資産として貯めるための道具、という違いです。同じ土俵にいるようで、目的が違いました。
車輪の再発明だと分かったときは少しがっかりしましたが、作ったあとだったからこそ「何が違うのか」を具体的に言えるようになったとも思っています。作る前に調べていたら「もうあるのか」で終わっていた気がします。
まとめ
-
controlnet_auxの OpenPose で18点を取り、Noneの関節は胴の長さから作れば全身にできる - 外接範囲を求めて縦横比を保ったまま収めれば、生成サイズへの合わせ込みは自動化できる
- PNG の tEXt チャンクに座標を入れておくと、骨格画像が「編集できる素材」になる
-
controlnet_aux全体は Apache 2.0 だが、OpenPose の部分と学習済みモデルは CMU の非商用限定。商用で使うなら検出器の差し替えが要る
明日からできるのは、まず4つ目の確認です。手元のプロジェクトで、パッケージ全体ではなく実際に import しているモジュールのソースを開いてみてください。 冒頭のコメントに、思っていたのと違う条件が書いてあるかもしれません。
そして骨格のほうを試したくなったら、通しで動くコードと編集用の HTML はここに置いてあります(MIT ライセンス。学習済みモデルは含めていません——上に書いた理由で、再配布はしていません)。
