謎's キッチン

謎のひとりごと。Amazon欲しい物リストはこちら: https://www.amazon.co.jp/hz/wishlist/ls/CCPOV7C6JTD2

[カラーサイエンス][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側で行っているようだ。それは良いとしても、二つのモニタの周波数が異なる場合に、セカンダリ側のアプリケーションのfpsNVIDIA側モニタの周波数に制限されてしまっているように見える (アプリケーションの問題かも?)。

GPU毎にディスプレイを立ち上げて、DISPLAY=:0.1 xfwm4&のようにすればどうかなと思ったけど、何故か上手くいかないので保留。

PR-S300SEからPR-400KIに

PR-S300SEのSPIによるパケット破棄問題に困っていたので、光回線終端装置を交換してもらった。もっと早くやっとくべきだったなぁ。
デバッグコマンド叩けなくなってしまったけれども、そもそもデバッグコマンド叩ける方がセキュリティ的に問題だから仕方ないか。

U2211Hのファクトリーモードに入る

電源OFF→一番目と二番目のボタンを押しながら電源を入れる→一番目のボタンを押すと、ファクトリーモードに入れる。


DEBUGMEGをONにすると、色々な情報が表示される。表示された表示名でググると、Realtek RTD2xxxのソースが引っかかるけど、これ使ってるのだろうか? 古くてあまり当てにならないけど、雰囲気は分かる。BANK SCALERの(一部の?)ADDR/VALUEの変更が出来るようだけど、何か良く分からん。


にしてもデジタルで使うのに不要な機能がいっぱいだなぁ。レイテンシが気になる。

簡略化 (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)

こうなる。そして、欲しかったのはこれ。


t*(t*(1.9992744*t + 0.273199) + 1.4994558) + 0.2269824
Wolfram|Alphaだと、候補の一つとして出してくれるようだ。
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