[カラーサイエンス][Python] 正しく変換したアニメカラーデータ
ねこまたやさんのアニメカラー測定データがどうにも怪しい (実測データと目視データが異なる) ので調べてみた。
同サイトのD65 XYZ値は100倍になっているようで、まずここでハマる。測定機器の凸版 CS CM1000は調べても詳細が出てこないが、下記を見る限りD50光源らしい?
このツールでの測色はD50にしないと正常なプロファイルが取れません。
光源の切替えはCS-CM1000付属のソフト上で行います。
ピンと来たので、測定データのD65 XYZ値をXYZ Scaling変換でD50 XYZ値に一旦戻してから、より高精度なBradford変換でD65 XYZ値に変換してみたら、どうにも其れっぽい色が出てきた。値のズレはXYZ Scaling変換にも一因があるようだ。
import numpy as np def fix_color(xyz): xyz_mat = np.matrix(xyz).T # http://technorgb.blogspot.jp/2015/08/blog-post_22.html # http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html xyz_to_lms_mat = np.matrix([ [ 0.8951, 0.2664, -0.1614], [-0.7502, 1.7135, 0.0367], [ 0.0389, -0.0685, 1.0296] ]) d65_mat = np.matrix([0.95047, 1.0, 1.08883]).T d50_mat = np.matrix([0.96422, 1.0, 0.82521]).T d65_xyz_to_d50_xyz_mat = np.diagflat(d50_mat.A / d65_mat.A) # XYZ Scaling method d50_xyz_to_d65_xyz_mat = xyz_to_lms_mat.I * np.diagflat((xyz_to_lms_mat * d65_mat) / (xyz_to_lms_mat * d50_mat)) * xyz_to_lms_mat # Bradford method xyz_mat = d50_xyz_to_d65_xyz_mat * (d65_xyz_to_d50_xyz_mat * xyz_mat) return xyz_mat.A1
sRGBへの変換 (Numpy使いきれてないのがバレる)
import numpy as np # https://www.w3.org/Graphics/Color/srgb def linearsrgb_to_srgb(lin): # return [0 if c < 0 else 12.92 * c if c <= 0.0031308 else 1.055 * np.power(c, (1/2.4)) - 0.055 for c in lin] return [12.92 * c if abs(c) <= 0.0031308 else math.copysign(1.055 * np.power(abs(c), (1/2.4)) - 0.055, c) for c in lin] def d65xyz_to_srgb(xyz): d65xyz_to_linearsrgb_mat = np.matrix([ [ 3.2406255, -1.537208 , -0.4986286], [-0.9689307, 1.8757561, 0.0415175], [ 0.0557101, -0.2040211, 1.0569959] ]) xyz_mat = np.matrix(xyz).T lin_srgb = d65xyz_to_linearsrgb_mat * xyz_mat srgb = linearsrgb_to_srgb(lin_srgb) return [i*255 for i in srgb]
書き出し
import numpy as np import csv with open("test.html", "w") as htmlfile: htmlfile.write("<html><head></head><body>") # http://www.nekomataya.info/teck_info/taiyo_color/TaiyoChart.txt with open("TaiyoChart.txt", "rb") as cvsfile: cl = 0 chartreader = csv.reader(cvsfile, delimiter='\t') for row in chartreader: if cl < 2: cl = cl + 1 continue if len(row) < 9: continue cname = row[2] xyz = map(float,[row[7], row[8], row[9]]) xyz_str = ",".join(map(str,xyz)) srgb = d65xyz_to_srgb(fix_color(np.array(xyz)/100)) srgb_str = ",".join([str(int(round(c))) for c in srgb]) o_srgb = [row[12], row[13], row[14]] o_srgb_str = ",".join(o_srgb) m_srgb = [row[4], row[5], row[6]] m_srgb_str = ",".join(m_srgb) print("%s = XYZ(%s), sRGB(%s), o_sRGB(%s)"%(cname, xyz_str, srgb_str, o_srgb_str)) htmlfile.write("<div style='display: inline-block; height 5em; width: 10em;'>\ <p style='margin: 0'>%s</p>\ <p style='background-color: rgb(%s); margin: 0'>XYZ=%s<br>sRGB=%s</p>\ <p style='background-color: rgb(%s); margin: 0'>o_sRGB=%s</p>\ <p style='background-color: rgb(%s); margin: 0'>m_sRPG=%s</p></div>"%\ (cname, srgb_str, xyz_str, srgb_str, o_srgb_str, o_srgb_str, m_srgb_str, m_srgb_str)) htmlfile.write("</body></html>")
若干表現変更。
暗い色はsRGB変換よりもgamma 2.2変換の方が、アニメカラー測定データにある目視カラーデータに近くなるように見える (X7より後はsRGB変換の方が近い)。うーむ、目視カラーデータの方も怪しい気がするような。
AbemaTVの仕様とHLSの暗号化の弱さ
AbemaTVの仕様について気になったので調べてみた (研究目的です念の為)。
AbemaTVはPCへの動画配信において、配信プロトコルにHLSを使用しているようだ。HLSはMPEG-DASHと異なりDRMが使えず (厳密にはMac環境のFairplayなどの例外もあるが) 、AbemaTVでは鍵の生成に若干工夫を行ってるのみのようだ。
まず、APIを使ってチャンネル一覧をダウンロード。
$ curl https://api.abema.io/v1/channels {"channels":[{"id":"abema-news","name":"AbemaNewsチャンネル","playback":{"hls":"https://linear-abematv.akamaized.net/channel/abema-news/playlist.m3u8"}},{"id":"abema-special","name":"AbemaSPECIALチャンネル","playback":{"hls":"https://linear-abematv.akamaized.net/channel/abema-special/playlist.m3u8"}},(後略)
次に画質一覧をダウンロード。
$ curl https://linear-abematv.akamaized.net/channel/abema-news/playlist.m3u8 #EXTM3U #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=184000 180/playlist.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000 240/playlist.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=900000 360/playlist.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1400000 480/playlist.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2200000 720/playlist.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=4200000 1080/playlist.m3u8
映像のフラグメント一覧をダウンロード。
$ curl https://linear-abematv.akamaized.net/channel/abema-news/1080/playlist.m3u8 #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:6 #EXT-X-MEDIA-SEQUENCE:951004 #EXT-X-DISCONTINUITY-SEQUENCE:16994 #EXT-X-KEY:METHOD=AES-128,URI="abematv-license://XXXXXXXXXXXXXXXXXXXXXX",IV=0x000000000000000000000000000000 #EXTINF:5.005000, https://linear-abematv.akamaized.net/tsnews/abema-news/h264/1080/xxxxxxxxxxxxxxxxxxxxxx.ts #EXTINF:5.005000, https://linear-abematv.akamaized.net/tsnews/abema-news/h264/1080/xxxxxxxxxxxxxxxxxxxxxx.ts #EXTINF:5.005000, https://linear-abematv.akamaized.net/tsnews/abema-news/h264/1080/xxxxxxxxxxxxxxxxxxxxxx.ts #EXTINF:5.005000, https://linear-abematv.akamaized.net/tsnews/abema-news/h264/1080/xxxxxxxxxxxxxxxxxxxxxx.ts
さて、映像はAES-128方式で暗号化されているようだ。暗号の鍵には初期化ベクトル(IV)とURIが指定されているが、URIに使われているabematv-licenseスキーマとは何だろう。仕組みは良く分からないが、Chromeの通信ログを見ると、スキーマの後ろの部分 (XXXXXXXXXXXXXXXXXXXXXX) と何処かにあるトークンを使って、とあるURLにアクセスしているようだ。
トークンはローカルストレージにあるものと同じようなので、Chromeのコンソールからwindow.localStorage["abm_mediaToken"]と打つと手に入る。このトークンとスキーマの後ろの部分を使って、ライセンスキーの種を手に入れる。
$ curl https://license.abema.io/abematv-hls?t=トークン --data '{"lt":"スキーマの後ろの部分","kv":"wd","kg":166}' {"cid":"abema-news","k":"XXXXXXXXXXXXXXXXXXXXXXX"}
さて、どうやってライセンスキーの種 (k) をキーに変換するのだろう? 調べた所、遅延ロードされた以下のJavascriptがこの変換を処理しているようだ。
https://abema.tv/xhrp.js
若干難読化されているけれども、肝心の部分はそのままだし、コードインジェクションもし放題なので割と何とかなる。
キー計算の表面部分のロジックはこんな感じ。
function _0x569113(cid, uid, k){ var _k = k.substring(0,k.length-1); var c = k.charAt(k.length-1); return c=='5'?_0x1e2ccc(cid, uid, _k): c=='4'?_0xa25b8f(cid, uid, _k): _0x2782e2(cid, uid, _k); } var _0x5ee3af=_0x569113(cid, window.localStorage["abm_userId"], k)
キー計算の中心部分は解読していないけれど、alert(_0x5ee3af);をインジェクションしてコードを実行するだけでキーが手に入る。
手に入ったキーは、バイナリ化してkey.binとして保存しておく。あとはそのキーを使って再生するだけ。
$ wget -N https://linear-abematv.akamaized.net/channel/abema-news/1080/playlist.m3u8 \ && sed -i 's/URI=.*\,/URI=\"key.bin\",/g' playlist.m3u8 \ && ffplay playlist.m3u8 -protocol_whitelist file,http,https,tcp,tls,crypto
フラグメント毎にしか再生できないので実用性には欠けるけれども、何にせよHLSが弱いことは証明できたので良いかな。
今後、AbemaTVでも強固なDRM付きのMPEG-DASHが導入されていくらしいので期待。
追記。何となく「簡単さ」が伝わってないようで残念なので、Python + Selenium WebDriverで自動化した方法を書いておきます。といっても大したものでもないですが。
from selenium import webdriver from time import sleep import requests import re import os if __name__ == '__main__': browser = webdriver.Chrome(executable_path = "/usr/lib/chromium-browser/chromedriver") browser.get("https://abema.tv/now-on-air/abema-news") sleep(1) js = requests.get("https://abema.tv/xhrp.js").text mod_js = re.sub("(_0x31a687=.*?);", "\\1;window.key=_0x31a687;", js) browser.execute_script(mod_js) sleep(1) key = browser.execute_script("return window.key;") print(key) browser.close() f = open("key.bin", "wb") f.write(bytearray(key)) f.close() pl = requests.get("https://linear-abematv.akamaized.net/channel/abema-news/1080/playlist.m3u8").text mod_pl = re.sub('URI=.*?\,', 'URI=\"key.bin\",', pl) f = open("playlist.m3u8", "w") f.write(mod_pl) f.close() os.system("ffplay playlist.m3u8 -protocol_whitelist file,http,https,tcp,tls,crypto")
なお、これはHLSの弱さを伝えるための単なる技術デモであり、フラグメント毎にしか再生できないため実用的ではなく、研究目的以外での利用は想定していません。
再追記。普通に独自スキーマへXMLHttpRequestするだけで良いと聞いたのでテストコード。
from selenium import webdriver from time import sleep import requests import re import os if __name__ == '__main__': browser = webdriver.Chrome(executable_path = "/usr/lib/chromium-browser/chromedriver") browser.get("https://abema.tv/now-on-air/abema-news") sleep(2) pl = requests.get("https://linear-abematv.akamaized.net/channel/abema-news/1080/playlist.m3u8").text key_url = re.search(u'URI=\"(.*?)\"\,',pl).group(1) browser.execute_script(''' var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) window.key = new Uint8Array(xhr.response) } xhr.open("GET", "%s"); xhr.send(); '''%key_url) sleep(1) key = browser.execute_script("return window.key;") browser.close() f = open("key.bin", "wb") f.write(bytearray(key)) f.close() mod_pl = re.sub('URI=.*?\,', 'URI=\"key.bin\",', pl) f = open("playlist.m3u8", "w") f.write(mod_pl) f.close() os.system("ffplay playlist.m3u8 -protocol_whitelist file,http,https,tcp,tls,crypto")
ありゃ、本当だ。色々難しく考えすぎてたようです。Javascriptへのcode injectionは不要だし、他のサイトにも使えそう。
ついでに複数フラグメントについても調べてみたら、単にプレーヤー側がリロードを繰り返すだけとのこと。ちょっと信じられないので、とりあえずPython3でプロキシを書いてみた。
from http.server import HTTPServer, SimpleHTTPRequestHandler import requests import re class MyHandler(SimpleHTTPRequestHandler): def do_GET(self): if self.path == "/key.bin": f = open("key.bin", "rb") body = f.read() f.close() else: pl = requests.get("https://linear-abematv.akamaized.net/channel/abema-news/1080/playlist.m3u8").content body = re.sub(b'URI=.*?\,', b'URI=\"key.bin\",', pl) self.send_response(200) self.send_header('Content-type', 'application/x-mpegURL') self.send_header('Content-length', len(body)) self.end_headers() self.wfile.write(body) httpd = HTTPServer(('localhost', 8000), MyHandler) httpd.serve_forever()
…本当ですね。思った以上にザルだった。MPEG-DASHへの一本化が待たれます。
なお、これらコードはエラーチェックが適当ですし、何故かたまに途切れりします。研究目的以外での利用は想定していません。また、暗号化においてHLSを使用することも推奨しません。
ディスプレイのオーバークロック
ついでだから、片方のディスプレイをオーバークロックしてみた。
$ cvt 1280 1024 76 # 1280x1024 75.98 Hz (CVT) hsync: 81.45 kHz; pclk: 140.75 MHz Modeline "1280x1024_76.00" 140.75 1280 1368 1504 1728 1024 1027 1034 1072 -hsync +vsync $ xrandr --newmode "1280x1024_76.00" 140.75 1280 1368 1504 1728 1024 1027 1034 1072 -hsync +vsync $ xrandr --addmode HDMI-1-2 1280x1024_76.00 $ xrandr --output HDMI-1-2 --mode 1280x1024_76.00
削除は以下で。
$ xrandr --delmode HDMI-1-2 1280x1024_76.00 $ xrandr --rmmode 1280x1024_76.00
75Hz→76Hzにしかならなかったけど、しないよりはマシか。
NVIDIAとIntelでマルチディスプレイ
ハマったので書いておく。
BusIDを調べる
$ lspci | grep NVIDIA | grep -v Audio | sed -e "s/^0*\(.\+\):0*\(.\+\)\.\(.\)\ .*$/PCI:\1:\2:\3/"
xorg.confを設定
Section "ServerLayout" Identifier "layout" Screen 0 "nvidia" Inactive "intel" EndSection Section "Device" Identifier "nvidia" Driver "nvidia" BusID "<BusID>" EndSection Section "Screen" Identifier "nvidia" Device "nvidia" # Uncomment this line if your computer has no display devices connected to # the NVIDIA GPU. Leave it commented if you have display devices # connected to the NVIDIA GPU that you would like to use. #Option "UseDisplayDevice" "none" EndSection Section "Device" Identifier "intel" Driver "modesetting" EndSection Section "Screen" Identifier "intel" Device "intel" EndSection
ディスプレイIDを調べる
$ xrandr --setprovideroutputsource 1 0 $ xrandr | grep \ connected | sed -e "s/\ .*$//"
.xsessionrcを設定
xrandr --setprovideroutputsource 1 0 xrandr --auto xrandr --output <ディスプレイID 1> --right-of <ディスプレイID 2>
補足。nvidia-settingsではIntel側のモニタをPRIME Displayとして表示しているものの、そのレンダリングはNVIDIA GPU側で行っているようだ。それは良いとしても、二つのモニタの周波数が異なる場合に、セカンダリ側のアプリケーションのfpsもNVIDIA側モニタの周波数に制限されてしまっているように見える (アプリケーションの問題かも?)。
GPU毎にディスプレイを立ち上げて、DISPLAY=:0.1 xfwm4&のようにすればどうかなと思ったけど、何故か上手くいかないので保留。
簡略化 (simplify)
Octaveはsymbolicで対応しているらしい?が、Ubuntu 14.04のでは未対応みたい。Sympyは対応している。
これで楽になるかな…と思ったけど、結果を見るとうーむ。
補足すると、以下のが、
>>> t = Symbol('t') >>> f = 0.3635819 + (0.4891775 + 0.0106411) * t * (4*t*t + 3) + 0.1365995 * (2*t*t-1); >>> simplify(f) 1.99927439999666*t**3 + 0.273199*t**2 + 1.49945579999949*t + 0.2269824 >>> factor(f) 1.0*(1.99927439999666*t**3 + 0.273199*t**2 + 1.49945579999949*t + 0.2269824)
こうなる。そして、欲しかったのはこれ。
Wolfram|Alphaだと、候補の一つとして出してくれるようだ。
t*(t*(1.9992744*t + 0.273199) + 1.4994558) + 0.2269824
http://www.wolframalpha.com/input/?i=simplify%280.3635819+%2B+%280.4891775+%2B+0.0106411%29+*+t+*+%284*t*t+%2B+3%29+%2B+0.1365995+*+%282*t*t-1%29%29
1.99927*t**3 + 0.273199*t**2 + 1.49946*t + 0.226982
t(t(1.99927*t + 0.273199) + 1.49946) + 0.226982