<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>programming | yukin.net</title>
	<atom:link href="https://yukin.net/category/programming/feed/" rel="self" type="application/rss+xml" />
	<link>https://yukin.net</link>
	<description>stay curious</description>
	<lastBuildDate>Thu, 18 Jan 2024 00:28:39 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9</generator>

<image>
	<url>https://yukin.net/wp-content/uploads/2023/10/cropped-yukin.fabicon-e1697637677176-32x32.png</url>
	<title>programming | yukin.net</title>
	<link>https://yukin.net</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>jsPsychを使ったPsychomotor Vigilance Task (PVT)</title>
		<link>https://yukin.net/programming/jspsych/pvt_by_jspsych/</link>
		
		<dc:creator><![CDATA[yukin]]></dc:creator>
		<pubDate>Fri, 12 Jan 2024 00:57:17 +0000</pubDate>
				<category><![CDATA[jspsych]]></category>
		<category><![CDATA[心理実験]]></category>
		<guid isPermaLink="false">https://yukin.net/?p=1710</guid>

					<description><![CDATA[スマホを使ったオンライン調査でもPVT（Psychomotor Vigilance Task）がやりたいという相談を受けて書いてみたコードを、備忘録として残しておく。※実際に実験に使ったコードではないので参考程度にお願い [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>スマホを使ったオンライン調査でもPVT（Psychomotor Vigilance Task）がやりたいという相談を受けて書いてみたコードを、備忘録として残しておく。<br>※実際に実験に使ったコードではないので参考程度にお願いします。</p>



<p>PVTは反応時間を測る課題でタイミング制御が重要なので、ある程度評価が確立されている<a rel="noopener" target="_blank" href="https://www.jspsych.org/">jsPsych</a>を使うことにした。<span class="marker-under">jsPsych</span>はJavaScriptで書かれたライブラリで、WEBブラウザ上で動作する心理実験を比較的簡単に？書くことができる。<br>もともと<a rel="noopener" target="_blank" href="https://www.psychopy.org/">PsychoPy</a>に精通していたら、それのJavaScript版である<a rel="noopener" target="_blank" href="https://github.com/psychopy/psychojs">PsychoJS</a>を使う選択肢もあったかもしれない。</p>



<p>ちなみに、ラボそしてオンラインでの心理実験に使うツールを比較した研究（<a rel="noopener" target="_blank" href="https://doi.org/10.7717/peerj.9414">https://doi.org/10.7717/peerj.9414</a>）があって、基本的にはここを参考に<span class="marker-under">jsPsych</span>を選定した。</p>



<h2 class="wp-block-heading"><span>jsPsychをスマホで使う</span></h2>



<p>jsPsychはPCブラウザ用に書かれたライブラリで、スマホで動作させるときに問題になったのがボタンタップの拾い方だった。</p>



<p>もともと<code>jspsych-html-button-response.js</code>では、<code>addEventListener('click', ...</code>としてボタンを準備している。このままスマホで使うと、ボタンをタッチした瞬間ではなく<span class="marker-under-red">タッチしたあとに離した瞬間</span>がボタン押しのタイミングとして記録されてしまう。</p>



<p>なのでこの箇所を、<code>addEventListener('touchstart', ...</code>に書き換えてみて、ひとまずちゃんとタッチした瞬間を記録しているっぽいので良しとしている。が、もし読者の方でご指摘あればぜひ教えていただけるとありがたいです。</p>



<h2 class="wp-block-heading"><span>コード本体</span></h2>



<p>いずれもう少し中身を整理して公開したい。が、今はこのまま</p>



<h3 class="wp-block-heading"><span>注意事項など</span></h3>



<ul class="wp-block-list">
<li>jsPsychのバージョンは6.3を使っていた。（ので、今の最新バージョンで動くかはわかりません）</li>



<li>このコードは<a rel="noopener" target="_blank" href="http://www.cognition.run">www.cognition.run</a>上で動作させるように書いたので、jsPsychの読み込みなどHTML部分は省略されていることに注意。</li>



<li>細部の詰めが甘い点が複数あるので、このまま実験には使えないです。</li>



<li>このコードを使って実験が失敗しても筆者は責任を負いません。自己責任でお願いします。</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: Code: ダブルクリックで編集モード; notranslate">
// experiment settings
const testDurM  =    3;      // test duration in minutes
const testDur   = 1000 * 60 * testDurM; //convert testDur to msec
const isiMin    = 1000;      // minimum ISI in msec
const isiMax    = 4000;      // maximum ISI in msec
const maxDur    = 1000 * 30; // maximum length for clock trial
const fbDur     = 1000 * 1.5;// display length for RT feedback
const TH_fStart =  100;      // if(RT &lt; TH_fStart) { respType: false_start}
const tmpTH_lps =  355;      // if(RT ≥ tempTH_lps){ respType: lapse } 

const id = jsPsych.data.getURLVariable(&#039;id&#039;);
const sessionID = jsPsych.randomization.randomID(20);
let testStart = 0;

// log properties
jsPsych.data.addProperties({
  ISIminimum: isiMin,
  ISImaximum: isiMax,
  testDuration: testDur,
  maxTrialDuration: maxDur,
  feedbackTrialDuration: fbDur,
  session: sessionID,
});

// original function(s)
function getRandomInt(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.round(Math.random() * (max - min) + min);
};

// change button styles
const PUSHstyle = &#039;&lt;style&gt; .jspsych-btn {font-size:3.5em; position:fixed; bottom:15vh; left:50%; transform:translate(-50%);} &lt;/style&gt;&#039;;
const CONTstyle = &#039;&lt;style&gt; .jspsych-btn {font-size:2em; position:fixed; bottom:15vh; left:50%; transform:translate(-50%);} &lt;/style&gt;&#039;;

/* create timeline */
var timeline = &#x5B;];

/* welcome message and enter fullscreen*/
var welcome = {
  type: &quot;fullscreen&quot;,
  fullscreen_mode: true,
  message: &#039;&lt;h2&gt;PVT検査&lt;/h2&gt;&lt;p style = font-size:2ex&gt;id = &#039; + id +
    &#039;&lt;br&gt;これからPVT課題を実施していただきます。&#039; +
    &#039;スマートフォンでの参加をお願いいたします。&#039; +
    &#039;&lt;br&gt;&lt;br&gt;所要時間は約&#039; + testDurM + &#039;分です。&lt;/p&gt;&#039; + CONTstyle,
  //css_classes: &#039;.jspsych-btn {bottom: -0em}&#039;,
  button_label: &#x5B;&#039;次に進む&#039;],
  data: { stimType: &#039;welcomeMsg&#039; },
  /* prompt:&#039;&lt;p&gt;Is this activity&lt;/p&gt;&#039; */
};
timeline.push(welcome);

/*
// ask sleepiness
const KSSqs = &#x5B;
  &quot;非常にはっきり目覚めている&quot;,
  &quot; &quot;,
  &quot;目覚めている&quot;,
  &quot; &quot;,
  &quot;どちらでもない&quot;,
  &quot; &quot;,
  &quot;眠い&quot;,
  &quot; &quot;,
  &quot;とても眠い（眠気と戦っている）&quot;
];
var KSS_trial = {
  type: &#039;survey-multi-choice&#039;,
  preamble: &#039;&lt;style&gt; .jspsych-btn {font-size:2em} &lt;/style&gt;&#039;,
  questions: &#x5B;
    {
      prompt: &quot;今の眠気を以下の９段階でお答えください。&quot;,
      name: &#039;KSS&#039;,
      options: KSSqs,
      required: true,
      horizontal: false,
    }
  ],
  button_label: &#x5B;&#039;次に進む&#039;],
  data: { stimType: &#039;KSS&#039; },
  autocomplete: false,
  force_response: true,
};
timeline.push(KSS_trial);

// ask predicted performance
var prdct_performance = {
  type: &quot;survey-html-form&quot;,
  premble: &#039;performance prediction&#039;,
  button_label: &#039;次に進む&#039;,
  html: &#039;&lt;p style = font-size:2ex&gt; Mean reaction time &lt;input type=&quot;text&quot; id=&quot;resp-box&quot; name=&quot;prdctRT&quot; pattern=&quot;^(&#x5B;0-9]{3,})$&quot; /&gt;&lt;/p&gt;&#039; + 
  &#039;&lt;style&gt; .jspsych-btn {font-size:2em} &lt;/style&gt;&#039;,
  autofocus: &#039;resp-box&#039;
}
timeline.push(prdct_performance);
*/

var inst_trial = {
  type: &quot;html-button-response-m&quot;,
  stimulus: &#039;&lt;p style = font-size:2ex&gt;これから白い枠とPUSHボタンが表示されます。白い枠の中に数字が表示されたらなるべく早くPUSHボタンをタップしてください。&#039; +
    &#039;&lt;br&gt;PUSHボタンを押すと開始します。&lt;/p&gt;&#039; + PUSHstyle,
  choices: &#x5B;&#039;PUSH&#039;],
  on_finish: function () {
    testStart = performance.now();
  },
  data: { stimType: &#039;instTrial&#039; },
};
timeline.push(inst_trial);

// ISI
let t_dur         = 0;      // temp val
let t_dur_left    = 0;      // temp val
let presentAgain  = false;

/// ISI_child
var isi_trial_child = {
  type: &#039;html-button-response-m&#039;,
  stimulus: &#039;&lt;div id=&quot;clock&quot;&gt; &lt;/div&gt;&#039;,
  choices: &#x5B;&#039;PUSH&#039;],
  data: { stimType: &#039;ISI&#039; },
  prompt: &#039;&lt;div id=&quot;prompt&quot;&gt;課題実施中...&lt;/div&gt;&#039; + PUSHstyle,
  response_ends_trial: true,
  trial_duration: function() {
    return t_dur;
  },
  on_start: function() {
    console.log(&quot;ISI_child_started t_dur = &quot; + t_dur);
  },
  on_finish: function (data) {
    if (!(data.rt === null)) {// button push occured
      // log RRT
      let lastRRT = 1 / data.rt * 1000;
      data.rrt = lastRRT;
      // for repeating isi_trial_child
      t_dur_left = t_dur - data.rt;
      console.log(&#039;false_start: t_dur_left = &#039; + t_dur_left);
      presentAgain  = true;
      jsPsych.data.get().addToLast({respType: &#039;false_start&#039;});
    } else { // button push did not occured
    　//本来なら、ボタン押しなくMaxDurに到達した際は、MaxDurがRTとなるのでそのあたりの処理の
      t_dur_left = 0;
      presentAgain  = false;
    }
  }
};

/// parent ISI loop
var isi_trial = {
  timeline: &#x5B;isi_trial_child],
  on_timeline_start: function () {
    if(!presentAgain){
      t_dur = getRandomInt(isiMin, isiMax);
      console.log(&#039;init ISI with t_dur = &#039; + t_dur);
    } else {
      t_dur = t_dur_left;
      console.log(&#039;repeating ISI with t_dur = &#039; + t_dur);
    }
  },
  loop_function: function(data){
    if(presentAgain &amp;&amp; t_dur_left &gt; 0){
      return true;
    } else {
      return false;
    }
  }
};

let start_time = performance.now();
var interval = &#x5B;];
let lastRT = 0;
// main trial
var main_trial = {
  type: &#039;html-button-response-m&#039;,
  stimulus: &#039;&lt;div id=&quot;clock&quot;&gt; &lt;/div&gt;&#039;,
  choices: &#x5B;&#039;PUSH&#039;],
  trial_duration: maxDur,
  response_ends_trial: true,
  data: { stimType: &#039;clock&#039; },
  prompt: &#039;&lt;div id=&quot;prompt&quot;&gt;課題実施中...&lt;/div&gt;&#039; + PUSHstyle,
  on_start: function () {
    start_time = performance.now();
    interval = setInterval(function () {
      var time_elapsed = Math.floor(performance.now() - start_time);
      var time_elapsed_str = time_elapsed.toString();
      document.querySelector(&#039;#clock&#039;).innerHTML = time_elapsed_str;
    }, 1)
  },
  on_finish: function (data) {
    clearInterval(interval);
    // log response type
    if (!(data.rt === null)) {
      let lastRRT = 1 / data.rt * 1000;
      //console.log(lastRRT);
      data.rrt = lastRRT;
      if(data.rt &lt;= TH_fStart){
        jsPsych.data.get().addToLast({respType: &#039;false_start&#039;});
      }else if(data.rt &gt; tmpTH_lps){
        jsPsych.data.get().addToLast({respType: &#039;lapse_rt≥&#039; + tmpTH_lps});
      }else {
        jsPsych.data.get().addToLast({respType: &#039;hit&#039;});
      }
    } else {
      jsPsych.data.get().addToLast({respType: &#039;noResp_in&#039; + maxDur + &#039;ms&#039;})
    }

  },
  post_trial_gap: 0
};

//feedback
var fb_trial = {
  type: &#039;html-button-response-m&#039;,
  stimulus: &#039;&lt;div id=&quot;clock&quot;&gt; &lt;/div&gt;&#039;,
  choices: &#x5B;&#039;PUSH&#039;],
  prompt: &#039;&lt;div id=&quot;prompt&quot;&gt;課題実施中...&lt;/div&gt;&#039; + PUSHstyle,
  trial_duration: fbDur,
  response_ends_trial: false,
  data: { stimType: &#039;fb&#039; },
  on_start: function () {
    lastRT = jsPsych.data.get().last(1).select(&#039;rt&#039;)
    interval = setInterval(function () {
      document.querySelector(&quot;#clock&quot;).innerHTML = Math.floor(lastRT.values).toString();
      // activate pressed button during trial
      if (document.querySelector(&#039;button&#039;).disabled) {
        document.querySelector(&#039;button&#039;).disabled = false;
      }
    }, 10);
  },
  on_finish: function () {
    clearInterval(interval);
  }
};

// timeline
var PVT_timeline = {
  timeline: &#x5B;isi_trial, main_trial, fb_trial],
  randomize_order: false,
  loop_function: function (data) {
    let et = performance.now() - testStart;
    if (et &gt; testDur) {
      return false; // end test loop
    } else {
      return true;  // continue loop
    }
  }
};
timeline.push(PVT_timeline);

// finish
var endFullscreen = {
  type: &#039;fullscreen&#039;,
  fullscreen_mode: false,
  button_label: &#x5B;&#039;終了&#039;],
  data: { stimType: &#039;endFullscreen&#039; },
};
timeline.push(endFullscreen);

// end message
var endMsg = {
  type: &#039;html-button-response&#039;,
  stimulus: function () {
    let trialDat = jsPsych.data.get().filter({ stimType: &#039;clock&#039; });
    let meanRT = Math.round(trialDat.select(&#039;rt&#039;).mean());
    return &#039;&lt;p&gt;お疲れさまでした。&lt;br&gt;ボタンを押して終了します。&lt;br&gt;&#039; +
      &#039;mean RT = &#039; + meanRT + &#039; ms&lt;/p&gt;&#039; + CONTstyle
  },
  choices: &#x5B;&#039;終了&#039;],
  trial_duration: 10000,
  data: { stimType: &#039;endMsg&#039; },
};
timeline.push(endMsg);

/* start the experiment */
jsPsych.init({
  timeline: timeline,
  on_finish: function () {
    confirm(&#039;データを記録しました。ブラウザを閉じても大丈夫です。&#039;)
    // jsPsych.data.displayData(&#039;csv&#039;); //終了後CSV形式でデータを画面上に表示
    // jsPsych.data.get().localSave(&#039;csv&#039;, &#039;data.csv&#039;);
  }
});
</pre></div>


<p>ちなみに、同時に読み込んでいたカスタムCSSはこちら</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: css; title: Code: ダブルクリックで編集モード; notranslate">
body {
    background-color: black;
    color: white;
}

p {
    font-size: 4ex;
    padding: 0, 0, 0, 0;
    line-height: 1.4em;
    color: white;
    
}

#clock {
    border: 2px solid whitesmoke;
    width: 3em;
    height: 1.1em;
    position: fixed;
    left: 50%;
    transform:translate(-50%);
    top: 40vh;
    margin: 0 auto;
    color: red;
    font-size: 5em;
    line-height: 1em;
}

#prompt {
    color: white;
    position: fixed;
    top: 20vh;
    left: 50%;
    transform:translate(-50%);
    font-size: 2ex;
    text-align: center;
    margin: 0 auto;
}
</pre></div>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Inkscapeで作ったtiffファイルをpythonで圧縮する</title>
		<link>https://yukin.net/programming/compress_tiff_by_python/</link>
		
		<dc:creator><![CDATA[yukin]]></dc:creator>
		<pubDate>Tue, 26 Dec 2023 04:51:00 +0000</pubDate>
				<category><![CDATA[programming]]></category>
		<category><![CDATA[python]]></category>
		<guid isPermaLink="false">https://yukin.net/?p=1631</guid>

					<description><![CDATA[Inkscapeではtiffでエクスポートするときに圧縮が選べなくてファイルサイズがインフレしたので、Pythonのtifffileでまとめて圧縮したときのメモ 必要なパッケージ コード本体 pathに保存してあるtif [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Inkscapeではtiffでエクスポートするときに圧縮が選べなくてファイルサイズがインフレしたので、Pythonの<code>tifffile</code>でまとめて圧縮したときのメモ</p>



<h2 class="wp-block-heading" id="packages"><span>必要なパッケージ</span></h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: Code: ダブルクリックで編集モード; notranslate">
pip install tifffile 
pip install imagecodecs #これがインストールされてなくてハマった
</pre></div>


<h2 class="wp-block-heading" id="code_body"><span>コード本体</span></h2>



<p><code>path</code>に保存してあるtiffファイルをまとめて処理する。</p>



<p>処理後のファイルは<span class="marker-under">_lzw.tiff</span>を付加して同じフォルダに保存している。</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: Code: ダブルクリックで編集モード; notranslate">
#python
import os
from tifffile import TiffFile, imwrite

# フォルダ内のすべてのTIFFファイルを取得
path = &quot;処理するTIFFファイルが入ってるフォルダへのパス&quot;
files = &#x5B;file for file in os.listdir(path) if file.endswith(&quot;.tif&quot;) or file.endswith(&quot;.tiff&quot;)]

# すべてのTIFFファイルをLZW圧縮されたTIFFファイルに変換
for tiff_file in files:

    # TIFFファイルを読み込む
    full_path = os.path.join(path, tiff_file)
    print(full_path)
    with TiffFile(full_path) as tif:

        #DPI情報をとっておく
        x_dpi = tif.pages&#x5B;0].tags.get(282, None)
        x_dpi = round(x_dpi.value&#x5B;0] / x_dpi.value&#x5B;1])
        y_dpi = tif.pages&#x5B;0].tags.get(283, None)
        y_dpi = round(y_dpi.value&#x5B;0] / y_dpi.value&#x5B;1])
        # unit_dpi = tif.pages&#x5B;0].tags.get(296, None)

        #新しいファイル名を設定
        new_tiff_file = os.path.join(path, os.path.splitext(tiff_file)&#x5B;0] + &quot;_lzw.tiff&quot;)
        #DPIを設定し保存する
        img = tif.asarray()
        imwrite(new_tiff_file, img, compression=&quot;lzw&quot;, resolution=(x_dpi, y_dpi))
</pre></div>


<h2 class="wp-block-heading" id="memo"><span>メモ</span></h2>



<p><code>ggplot2</code>の<code>ggsave()</code>でtiffを書き出すときは<code>compression="lzw"</code>をつければいいが、今回はggplot2で書き出したファイルに少し加工を加える必要があったので、わざわざpythonでやることになった。</p>



<p>Inkscapeも裏はpython動いてるみたいなので、うまくプラグイン的なのを作ることもできそう。</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
