2017年 07月 11日

Redis のメモリ消費量を見積る

実際に値をセットしてみて、そのキーの容量を求めることができるコマンドがある。

https://github.com/sripathikrishnan/redis-rdb-tools

  • redis-memory-for-key [key]

このコマンドは DUMP key コマンドを発行した結果を再度 Python でパースしながら消費容量を計算している。割と面倒くさいことをして正確に出そうとしてる。

参考:ダメな方法

DUMP してサイズを見る

DUMP key して出てくる文字列のサイズを単純に見ると、これはファイルに書き出すときの形式になっており、文字列が LZF で圧縮されていたりする。

ついでにいうとキーや期限などのオーバーヘッドの容量が含まれない。

DEBUG OBJECT key の serializedlength

DUMP された結果のサイズを表示しているようで、DUMP と同様に圧縮されたサイズがでるっぽい。

2017年 07月 07日

Eマウント ピンホールカメラの広角化 - 2

https://lowreal.net/2017/06/10/2, https://lowreal.net/2017/06/15/2 とやって、前回 T=0.05mm やもう少し小さい穴を試せなかったのでさらに追試です。画角は焦点距離で14mm相当です。

T=0.05mm のステンレスシムと、φ0.1 のエンドミルを手に入れたので φ0.175mmのピンホールをあけて撮影してみました。φ0.175mm というのは、ちゃんと加工できれいればって話なんですが、エンドミルが細すぎてどうしてもしなってしまうので、ちょっと小さめになっている気がします。

見ての通りで、厚さを減らしたにも関わらず周辺光量減光はあまり改善しませんでした。よくよく観察してみると、白色光源に向けたとき、画像周辺部分では青カブリするような挙動をするため、単純な周辺光量減光というよりは、光線角度が急すぎるせいでセンサー上のマイクロレンズで捉えられる範囲外になっているような気もします。

無理して14mmにするよりレンズキャップ全面位置 (25mm) ぐらいが丁度よさそうです。

他の作例



2017年 07月 05日

MeArm っぽいロボットアームの制御

MeArm のパクりっぽいやつ(設計はオープンだからパクりとはいわない気はする)を AliExpress で買ってみました。https://shop.mime.co.uk/

ロボットアームは複数の関節の回転運動を同時に行って特定座標への移動を行います。よって、実座標系からサーボモータの制御回転角への変換処理が必要になります。

FK IK

  • Forward Kinematics
  • Inverse Kinematics

FK は関節の根本から回転させていって先端の座標を求める処理で、IK は先端座標から逆算して関節の回転角を求める処理です。ロボットアームの場合は IK ができれば良いことになります。

MeArm の構造と関節角度

MeArm は根本にサーボモータが設置されておりリンク機構を介して関節を動かしているので、サーボモータの角度と関節の角度の対応を理解する必要があります。

  • X/Y/Z 論理座標系
  • IK 回転角
  • 物理サーボモータ回転角

の3つの要素があり、順に変換していく必要があります。

ベース

ベースは簡単です。90°で正面を向くようにして、0〜180°で可動します

ショルダー (後ろからみて右)

ショルダーも簡単です。90°でアームが真上を向くようにして、30〜150° (後〜前) 程度で可動します。

エルボー (後ろからみて左)

これは若干ややこしいです。MeArm の構造の場合、ショルダーの動きによって肘関節も連動してしまうためです。

エルボー用サーボは90°でアームが後ろに向くようにして、0°でアームが真上、180°でアームが真下に向くようにしてあります。

このセットアップでショルダー用サーボが90°、エルボー用サーボが90°のとき、肘関節も90°になります。ショルダー用サーボの角度が変化すると、肘関節もその角度分だけ動くため、エルボー用サーボに与える角度はショルダー用サーボに与える角度をひいて求める必要があります。

実装

mbed 用の実装Arduino用のインターフェイスを混ぜたような実装を書いて実験しました。

正確に動かすためには正確に制御方法を理解する方法があるので、結局ある程度自分でコードを試行錯誤して理解する必要がある気がします。

キャリブレーション方法が特に難しいのですが、以下のような方法で行いました。ロジックがあっていることが大前提ですが「動かしてみてあってるか確認する」のが難しいので、コードの検証はしっかりやらないとダメです。

  • 電源を切った状態で複数の座標にアームを動かしてみて、そのときの各サーボモータの角度を記録する
  • 実際に座標を計算して、サーボモータへ入力すべき角度との差分や係数を求める

#include <Arduino.h>
#include <Servo.h>

#include "math.h"
#include <Servo.h>

#define DEBUG 0

constexpr float DEG2RADIAN(float deg) {
	return deg * M_PI / 180;
}
constexpr float RADIAN2DEG(float rad) {
	return rad * 180 / M_PI;
}

class MeArm {
private:
	Servo base, shoulder, elbow, claw;
	float baseServoMin, baseServoMax, baseCoefficient, baseOffset;
	float shoulderServoMin, shoulderServoMax, shoulderCoefficient, shoulderOffset;
	float elbowServoMin, elbowServoMax, elbowCoefficient, elbowOffset;
	float clawServoMin, clawServoMax, clawCoefficient, clawOffset;

	float currentX, currentY, currentZ;
public:
	static const int16_t LENGTH_SEGMENT0 = 18; // base to shoulder
	static const int16_t LENGTH_SEGMENT1 = 80; // shoulder to elbow
	static const int16_t LENGTH_SEGMENT2 = 80; // elbow to wrist
	static const int16_t LENGTH_SEGMENT3 = 55; // wrist to grip point
	static const int16_t LENGTH_BASE_HEIGHT = 60;
	static const int16_t MAX_REACH = 220;

	static constexpr float lawOfCosines(float adj1, float adj2, float opp) {
		return acos((adj1*adj1 + adj2*adj2 - opp*opp) / (2*adj1*adj2));  
	}


	MeArm() {
		setServoCoefficient(
			0, 180, 1,  0,
			0, 180, -1, 0,
			0, 180, -1, 0,
			0, 180, 1, 0
		);
	}

	void begin(int pinBase, int pinShoulder, int pinElbow, int pinClaw) {
		base.attach(pinBase);
		shoulder.attach(pinShoulder);
		elbow.attach(pinElbow);
		claw.attach(pinClaw);
		base.write(90);
		shoulder.write(90);
		elbow.write(90);
		claw.write(90);
	}

	void setServoCoefficient(
		float _baseServoMin, float _baseServoMax, float _baseCoefficient, float _baseOffset,
		float _shoulderServoMin, float _shoulderServoMax, float _shoulderCoefficient, float _shoulderOffset,
		float _elbowServoMin, float _elbowServoMax, float _elbowCoefficient, float _elbowOffset,
		float _clawServoMin, float _clawServoMax, float _clawCoefficient, float _clawOffset
	) {
		baseServoMin = _baseServoMin;
		baseServoMax = _baseServoMax;
		baseCoefficient = _baseCoefficient;
		baseOffset = _baseOffset;
		shoulderServoMin = _shoulderServoMin;
		shoulderServoMax = _shoulderServoMax;
		shoulderCoefficient = _shoulderCoefficient;
		shoulderOffset = _shoulderOffset;
		elbowServoMin = _elbowServoMin;
		elbowServoMax = _elbowServoMax;
		elbowCoefficient = _elbowCoefficient;
		elbowOffset = _elbowOffset;
		clawServoMin = _clawServoMin;
		clawServoMax = _clawServoMax;
		clawCoefficient = _clawCoefficient;
		clawOffset = _clawOffset;
	}

	// https://developer.mbed.org/users/eencae/code/MeArm/docs/tip/MeArm_8cpp_source.html
	// by eencae
	bool solveInverseKinematics(
			float x, float y, float z,
			float& thetaBase, float& thetaShoulder, float& thetaElbow
	) const {
		float target = sqrt(x * x + y * y);
		if (target > MAX_REACH) return false;

		float wrist  = target - LENGTH_SEGMENT3;
		float shoulder = LENGTH_SEGMENT0;

		// shoulder to wrist
		float s2w = sqrt( (wrist - shoulder) * (wrist - shoulder) + (z - LENGTH_BASE_HEIGHT) * (z - LENGTH_BASE_HEIGHT) );
		if (s2w == 0.0) {
			return false;
		}

		if (DEBUG) {
			Serial.print("x = "); Serial.print(x); Serial.print(" ");
			Serial.print("y = "); Serial.print(y); Serial.print(" ");
			Serial.print("z = "); Serial.print(z); Serial.print(" ");
			Serial.print("target = "); Serial.print(target); Serial.print(" ");
			Serial.print("wrist = "); Serial.print(wrist); Serial.print(" ");
			Serial.print("shoulder = "); Serial.print(shoulder); Serial.print(" ");
			Serial.print("s2w = "); Serial.print(s2w); Serial.print(" ");
		}

		float thetaSw = acos( (wrist - shoulder) / s2w );
		if (isnan(thetaSw)) {
			Serial.println("invalid thetaSw");
			return false;
		}

		if (z < LENGTH_BASE_HEIGHT) {
			thetaSw *= -1.0;
		}

		float thetaElbow0 = lawOfCosines(LENGTH_SEGMENT1, LENGTH_SEGMENT2, s2w);
		if (isnan(thetaElbow0)) {
			Serial.println("thetaElbow0 is NaN");
			return false;
		}
		float thetaShoulder0 = lawOfCosines(s2w, LENGTH_SEGMENT1, LENGTH_SEGMENT2);
		if (isnan(thetaShoulder0)) {
			Serial.println("thetaShoulder0 is NaN");
			return false;
		}
		thetaShoulder = thetaSw + thetaShoulder0;
		thetaElbow = thetaElbow0;
		thetaBase = atan2(y, x);
		return true;
	}

	bool convertIKAnglesToServoAngles(
			float thetaBase, float thetaShoulder, float thetaElbow,
			float& thetaBaseServo, float& thetaShoulderServo, float& thetaElbowServo
		) {
		thetaElbow = M_PI - thetaShoulder - thetaElbow;
		// thetaElbow = thetaElbow - thetaShoulder - (M_PI/2.0);
		if (DEBUG) {
			Serial.print(" beta="); Serial.print(RADIAN2DEG(thetaElbow)); Serial.print(" ");
		}

		// convert angles to real angles
		thetaBaseServo = RADIAN2DEG(thetaBase) * baseCoefficient + baseOffset;
		if (thetaBaseServo < baseServoMin) thetaBaseServo = baseServoMin;
		if (baseServoMax < thetaBaseServo) thetaBaseServo = baseServoMax;

		thetaShoulderServo = RADIAN2DEG(thetaShoulder) * shoulderCoefficient + shoulderOffset;
		if (thetaShoulderServo < shoulderServoMin) thetaShoulderServo = shoulderServoMin;
		if (shoulderServoMax < thetaShoulderServo) thetaShoulderServo = shoulderServoMax;

		thetaElbowServo = RADIAN2DEG(thetaElbow) * elbowCoefficient + elbowOffset;
		if (thetaElbowServo < elbowServoMin) thetaElbowServo = elbowServoMin;
		if (elbowServoMax < thetaElbowServo) thetaElbowServo = elbowServoMax;
		return true;
	}

	bool goDirectlyTo(float x, float y, float z) {
		float thetaBase, thetaShoulder, thetaElbow;
		bool ok = solveInverseKinematics(x, y, z, thetaBase, thetaShoulder, thetaElbow);
		if (DEBUG) {
			Serial.print("goDirectlyTo "); Serial.print(ok); Serial.print(" ");
			Serial.print(RADIAN2DEG(thetaBase)); Serial.print(" ");
			Serial.print(RADIAN2DEG(thetaShoulder)); Serial.print(" ");
			Serial.print(RADIAN2DEG(thetaElbow)); Serial.print(" ");
		}
		if (ok) {
			convertIKAnglesToServoAngles(
				thetaBase, thetaShoulder, thetaElbow,
				thetaBase, thetaShoulder, thetaElbow
			);
			if (DEBUG) {
				Serial.print("converted: ");
				Serial.print(thetaBase); Serial.print(" ");
				Serial.print(thetaShoulder); Serial.print(" ");
				Serial.print(thetaElbow); Serial.print(" ");
			}
			base.write(thetaBase);
			shoulder.write(thetaShoulder);
			elbow.write(thetaElbow);
			currentX = x;
			currentY = y;
			currentZ = z;
			if (DEBUG) Serial.println("");
			return true;
		} else {
			if (DEBUG) Serial.println("");
			return false;
		}
	}

	bool gotoPoint(float x, float y, float z, float step=5, float wait=20) {
		float x0 = currentX; 
		float y0 = currentY; 
		float z0 = currentZ;
		float distance = sqrt((x0-x)*(x0-x)+(y0-y)*(y0-y)+(z0-z)*(z0-z));
		for (float i = 0; i < distance; i+= step) {
			bool ok = goDirectlyTo(x0 + (x-x0)*i/distance, y0 + (y-y0) * i/distance, z0 + (z-z0) * i/distance);
			if (!ok) return false;
			delay(wait);
		}
		bool ok = goDirectlyTo(x, y, z);
		if (!ok) return false;
		delay(wait);
		return true;
	}

	void openGripper() {
		moveGripper(M_PI/2);
	}

	void closeGripper() {
		moveGripper(0);
	}

	void moveGripper(float thetaClaw) {
		float thetaClawServo = RADIAN2DEG(thetaClaw) * clawCoefficient + clawOffset;
		if (thetaClawServo < clawServoMin) thetaClawServo = clawServoMin;
		if (clawServoMax < thetaClawServo) thetaClawServo = clawServoMax;
		claw.write(thetaClawServo);
	}
};

MeArm arm;

void setup() {
	Serial.begin(9600);
	Serial.println("start");

	if (0) {
		Serial.println("debug");
		Servo middle, left, right, claw ;  // creates 4 "servo objects"
		middle.attach(2);
		left.attach(3);
		right.attach(4);
		claw.attach(5);
		middle.write(90);
		left.write(90);
		right.write(90);
		claw.write(120);
		for (;;);

		uint16_t prev = 0;
		for (;;) {
			uint16_t read = static_cast<uint32_t>(analogRead(0)) * 180 / 512;
			if (read != prev) {
				Serial.print("read=");
				Serial.println(read);
				left.write(read);
				prev = read;
				delay(250);
			}
		}
		for (;;) {
			Serial.println("0");
			left.write(0);
			right.write(0);
			delay(2000);
			Serial.println("90");
			left.write(90);
			right.write(90);
			delay(2000);
			Serial.println("180");
			left.write(180);
			right.write(180);
			delay(2000);
		}
	}

	arm.begin(2, 4, 3, 5);
	arm.setServoCoefficient(
		0, 180, 1,  0,
		20, 170, -1, 170,
		20, 130, -1, 106,
		75, 115, -0.5, 115
	);
	arm.goDirectlyTo(0, 100, 50);
	// arm.openGripper();
}

void loop() {
	Serial.println("loop");
//	arm.gotoPoint(0,100,50);
//
//	arm.goDirectlyTo(0, 110, 60);  delay(1000);// b=90 s=90 e=30
//	arm.goDirectlyTo(100, 110, 60);  delay(1000);// b=90+45 s=100 e=40
//	arm.goDirectlyTo(100, 160, 60);  delay(1000);// b=90+45 s=120 e=45

	arm.openGripper();
	arm.gotoPoint(0,110,60); arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint(100,110,60);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint(100,160,60);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint( 0,160,60);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint(-100,160,60);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint(-100,110,60);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint( 0,110,60);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.closeGripper();
	delay(1000);

	arm.openGripper();
	arm.gotoPoint(0,110,20); arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint(100,110,20);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint(100,160,20);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint( 0,160,20);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint(-100,160,20);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint(-100,110,20);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint( 0,110,20);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.closeGripper();
	delay(1000);

	arm.openGripper();
	arm.gotoPoint(0,110,110); arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint(100,110,110);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint(100,160,110);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint( 0,160,110);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint(-100,160,110);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint(-100,110,110);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.gotoPoint( 0,110,110);  arm.openGripper(); delay(250);arm.closeGripper();  delay(250);
	arm.closeGripper();
	delay(1000);
}

ref.

2017年 07月 04日

自分の瞳孔を内側から見る方法を発見してしまった

鏡を使うと外から瞳孔を見ることはできるけど、内側は解剖しないと普通は見れない。しかしピンホールを使うと直接自分の瞳孔を観察できた。

T=0.05mm のステンレス板に φ0.1mm の穴をあけてみたところ (ピンホール)、これに目をできるだけ近づけてそこそこ強い光源を見ると、自分の瞳孔を見ることができることに気がついた。

簡単に再現する方法

ある程度小さい穴なら以下のような現象が再現できるので、手でピンホールを作って室内蛍光灯とかを見るだけでよかった。

こんな感じにして穴を片目にあてる。単に穴を通して向こう側が見えるだけに思えるが、もう片方の眼にあたる光を手で遮ったりすると瞳孔の動きがわかる。(瞳孔は左右の眼で同時に変化するので、観察している眼と逆の眼の光量を変えてやる)

歪な形の穴を通した場合、光量によって歪な形のまま大きさが変化する。いまいちなんで瞳孔の動きが観察できるのかわからない。

まだら模様の円内と、ぎざぎざした円縁

一定の光源のはずの円内はまだら模様にみえる。

その周辺にはぎざぎざした縁の円があって、急激に暗くなる。

なぜこれが瞳孔だと確認できるのか

片方の眼を遮光した状態で、もう片方の眼にピンホールをあてると大きな円が見える。この状態のまま片目の遮光を外すと急激に円が収縮する。もう一度遮光しなおすとゆるやかに円が拡大する。

よくみると静止していても円は拡大したり収縮したりする。

光源と網膜の間でこのような動きをするのは瞳孔だけなので、これは瞳孔だと思う。

瞳孔内のまだら模様は何なのか

まだら模様は眼球を回転させても移動しない。瞳孔の大きさも関係ない。常に一定の模様が見える。

角膜・水晶体・硝子体のうちのどれかが関係してそうだけどわからない。ちなみに角膜前についた埃も見えるのだけど、まだら模様よりすこし遅れて動くことから、たぶん角膜表面よりは瞳孔に近そう。

なぜ眼の外のピンホールで瞳孔が見えるのか

よくわからない。ピンホールを近づけすぎると、ピンホールが作る像が瞳孔でケラれてしまうからかな? だとすると瞳孔が見えているというよりは虹彩の影が見えているといえる。

2017年 06月 20日

一度セットアップした Raspberry Pi を使いまわす

Raspbian を書きこんで適当にセットアップする

NOOBS は使わないこと。

SDカードのコピー

dd で普通にもってくる。

$ diskutil list
$ diskutil umountDisk /dev/disk2
$ sudo dd if=/dev/rdisk2 of=raspi.img bs=1m

raspbian-shrink で書きこみサイズを減らす

dd をそのまま書きこんでもいいのだが、SDカードの容量分書きこむハメになる。ほとんど無意味な情報を書きこむことになるので大変アホっぽい。

raspbian-shrink というツールを使うと大幅にイメージを小さくできる。Mac の場合は以下の手順に従う。

https://github.com/aoakley/cotswoldjam/blob/master/raspbian-shrink/raspbian-shrink-mac.txt

Docker が必要なので、適当にいれておく。homebrew なら

brew install docker
brew cask install docker

あとは手順に従うだけ。一度セットアップしておけばコマンドライン1発でshrinkできる。

領域自動拡大

一度セットアップしたイメージから起動するとファイルシステムの自動拡張が行われない。一度だけ行われるようになっているから。これを初期状態に戻す。

FAT でマウントされる /boot/cmdline.txt に以下を追記する。init_resize.sh は初回だけパーティション拡張を行い、/boot/cmdline.txt を書きかえる動作をする。

 init=/usr/lib/raspi-config/init_resize.sh

別の SD カードに書きこむ

作った .img を Etcher を使って書きこむ。配布されているイメージと同様。

2017年 06月 16日

長波JJY・短波JJY (標準電波) をWebAudioで聞く

長波JJY (40kHz/60kHz)

https://play-morse.lowreal.net/jjy.html

現行 JJY

  • 15分、45分のコールサイン送出
  • できるだけ高精度に送出する

あたりにこだわって実装してみた。基本的には JJY を CW モードで聞いている状態を再現するというコンセプトだけど、出力周波数を 13333 に設定すれば、よくあるヘッドフォン端子から3倍高調波 (約40kHz) を出すみたいのもできるはず。

このJJYはかなり単純かつ速度が遅いので人力でもデコードできそうですね。

短波JJY(10MHzなど)

https://play-morse.lowreal.net/vhf-jjy.html

2001年に停波・閉局してしまった短波 JJY を Wikipedia の解説をもとに実装してみました。といっても自分では聞いたことがないのであってるのか謎なんですが……

当時使われていた音声がアーカイブとして残っていてダウンロードできるのですが、再配布できないため音声送出は未実装になっています。


現行 JJY は聴覚受信してみるとリズムが不安定なうえに変化がなさすぎるので、聞いていて面白いのは短波 JJY のほうですね。1秒音が目立たないこと、1分ごとに追加の音、5分ごとに大きく音が変わり、10分ごとに呼び出し符号が入って、1時間ごとに無音となるので、BGMとして便利な気がします。

オマケ:時報

https://play-morse.lowreal.net/jiho.html

NHK の時報 + NTT の時報をミックスしたような時報。

  • 0/30 の3秒前から 440Hz
  • 10秒ごとに 880Hz
  • 秒は 1760Hz
2017年 06月 09日

広色域時代の画像の正しい扱いかた

モバイル端末も iPhone7 など DCI-P3 サポートが増えてきて、CSS での広色域サポートもはじまりつつあるなかで、サーバサイドなどで画像をとりあつかうときに、正しく扱えていないというのはとても微妙です。HTTPS 対応が当然になっていくように、広色域対応も当然のこととなっていくことでしょう。

正しいやりかたといっても簡単で、

  • 付属するICCプロファイルをそのままにする (一番簡単)
  • なんらかの事情で sRGB にするならするでプロファイル変換する (CMSが必要)

ということです。全然当たり前で面白くないですね。でも考慮してないことが多いのではないでしょうか。

まず付属するICCプロファイルを保持するというのは一番簡単で確実です。

一方で小さい画像だとICCプロファイルのサイズが無視できなくなるので色再現よりファイルサイズを優先したいという場合もあります。この場合は sRGB に変換するのが適切です。プロファイルなしを sRGB として扱うブラウザが多く、今 sRGB として取り扱ってないブラウザもそのうち sRGB 扱いするであろう期待があるためです。(画像だけがモニタプロファイルに直接影響されブラウザ内で閉じていない状況というのは canvas や CSS color との整合性がとれないので)

しかしいくらファイルサイズを減らしたいといっても、単にICCプロファイルを不必要なメタデータとして削除するだけの実装がしばしばあって、これはかなりよくない実装です。

いくつかの実装で正しいやりかたをするにはどうすれば良いか調べてみました。

外部から実装をみたときの考慮ポイント

  • ICCプロファイルを保持するか
  • 保持しないなら適切に変換されるか

プロファイルを単純に捨てるとどうなるか?

プロファイルなしの画像をどう扱うかで変わってきますが、sRGB としてみなす環境を想定する場合だと

  • 元画像が sRGB なら問題ありません。
  • 元画像が広色域 (AdobeRGB / DCI-P3 など) だと大きく色が変わります。画像全体の色が変化するので、ホワイトバランスが崩れて元の画像の印象と大きく変わってきます。彩度も、正しく sRGB 変換に変換するのと違って大きく下がります。

テスト画像

http://www.color.org/version4html.xalter にあるうち Upper_Right.jpg は変換に失敗すると色がおかしいのが一目でわかるので便利です。

exiftool -profiledescription *.jpg          
======== Lower_Left.jpg
Profile Description             : GBR
======== Lower_Right.jpg
Profile Description             : Adobe RGB (1998)
======== Upper_Left.jpg
Profile Description             : e-sRGB
======== Upper_Right.jpg
Profile Description             : sYCC 8-bit
    4 image files read

exiftool だとカラープロファイルがない場合は Profile Description もでません。

ImageMagick で strip を使っているなら要注意

まずはコマンドラインから。

特に何もせずに resize などをしてもプロファイルは失われません。verbose の出力に元のプロファイルと関係なく sRGB とかでるけど、これは謎で、出力プロファイルとは関係ありません。作業用スペースなのかな?

$ convert Upper_Right.jpg -resize 128x -verbose x.jpg
Upper_Right.jpg=>x.jpg JPEG 261x196=>128x96 128x96+0+0 8-bit sRGB 26878B 0.010u 0:00.009


$ exiftool -profiledescription x.jpg             
Profile Description             : sYCC 8-bit


画像の表示に直接関係のないメタデータを一括で削除するコマンドとして strip がありますが、これをするとカラープロファイルも失われます。入力に sRGB 以外の画像が入る可能性があるなら strip は単体でつかってはいけません。

sRGB に変換 (-profile) しつつカラープロファイルなどメタデータを削除 (-strip) するなら以下の通りにします。プロファイル変換のため little-cms 対応のビルドが必要です。

convert Upper_Right.jpg -profile /System/Library/ColorSync/Profiles/sRGB\ Profile.icc -strip x.jpg

LCMS 対応ビルドを homebrew で入れる場合

デフォルトでは有効にならないので、オプションが必要です

$ brew install imagemagick --with-little-cms --with-little-cms2

$ convert --version
Version: ImageMagick 7.0.5-9 Q16 x86_64 2017-06-01 http://www.imagemagick.org
Copyright: © 1999-2017 ImageMagick Studio LLC
License: http://www.imagemagick.org/script/license.php
Features: Cipher DPC HDRI Modules 
Delegates (built-in): bzlib freetype jng jpeg lcms ltdl lzma png tiff xml zlib

Delegates に lcms が入っていればオッケー

ImageMagick (Perl)

Perl に限らず ImageMagick の API はどれもほぼ一緒なはずです。

ちなみに little-cms 付きでビルドしていないと色域変換ができないにも関わらずエラーにならないので罠いです。これは Perl だけかもしれないですが……

コマンドラインと同じように Strip したい場合 (sRGB に変換すべきケース) では以下のようにします

#!/usr/bin/env perl
use utf8;
use strict;
use warnings;
use v5.10.0;
use lib lib => '/usr/local/lib/perl5/site_perl';

use Image::Magick;
use Path::Class;

warn Image::Magick->new->Get('version');
#=> ImageMagick 7.0.5-9 Q16 x86_64 2017-06-02 http://www.imagemagick.org

my $p = Image::Magick->new;
$p->Read("./Upper_Right.jpg");

# little-cms (lcms) 付きでビルドしていないとエラーもなく失敗するので注意
my $err = $p->Profile(
	name               => '/System/Library/ColorSync/Profiles/sRGB Profile.icc',
	'rendering-intent' => 'Perceptual',
);
warn "$err" if "$err";

# メタデータ削除
$p->Strip;

$p->Write("x.jpg");

Profile() メソッドを呼ぶと変換もしてくれるようです。ドキュメントを見るといまいち変換してくれるのかわかりにくいけど変換してくれます。引数もわかりにくいけど name にファイル名を渡すのがお手軽っぽいです。上にも書いた通り little-cms 対応ビルドじゃないとダメで、エラーにもならないのでとても注意が必要です。

jpegtran

Independent JPEG Group's JPEGTRAN, version 8d  15-Jan-2012
Copyright (C) 2012, Thomas G. Lane, Guido Vollbeding
$  jpegtran -outfile x.jpg  Upper_Right.jpg 

デフォルトでカラー変換なしでプロファイルが消滅します。保持するためには以下のようにしてメタデータを全て保持するしかない。

$ jpegtran -copy all -outfile x.jpg Upper_Right.jpg 

ICC プロファイルだけ残す設定はないようです。

プロファイルを変換するオプションもないので、sRGB にしつつプロファイルを削除したい場合は別のツール (たとえば ImageMagick) が必要となります。

mozjpeg

/usr/local/opt/mozjpeg/bin/jpegtran -v
mozjpeg version 3.2 (build 20170501)
Copyright (C) 2009-2016 D. R. Commander
Copyright (C) 2011-2016 Siarhei Siamashka
Copyright (C) 2015-2016 Matthieu Darbois
Copyright (C) 2015 Google, Inc.
Copyright (C) 2014 Mozilla Corporation
Copyright (C) 2013-2014 MIPS Technologies, Inc.
Copyright (C) 2013 Linaro Limited
Copyright (C) 2009-2011 Nokia Corporation and/or its subsidiary(-ies)
Copyright (C) 2009 Pierre Ossman for Cendio AB
Copyright (C) 1999-2006 MIYASAKA Masaru
Copyright (C) 1991-2016 Thomas G. Lane, Guido Vollbeding

Emulating The Independent JPEG Group's software, version 8d  15-Jan-2012
 /usr/local/opt/mozjpeg/bin/jpegtran  -outfile x.jpg  Upper_Right.jpg 

オリジナルの jpegtran と同様デフォルトでカラー変換なしでプロファイルが消滅します。保持する方法も一緒です。

Progressive encoding with "jpegrescan" optimization. It can be applied to any JPEG file (with jpegtran) to losslessly reduce file size.

https://github.com/mozilla/mozjpeg

(強調は引用者) losslessly って書いてあってモヤモヤしますね。デフォルトでロスしてる。

プロファイルを変換するオプションもないので、sRGB にしつつプロファイルを削除したい場合は別のツール (たとえば ImageMagick) が必要となります。

jpegoptim

$ jpegoptim --version
jpegoptim v1.4.4  x86_64-apple-darwin16.0.0
Copyright (c) 1996-2016, Timo Kokkonen

libjpeg version: 8d  15-Jan-2012
Copyright (C) 2012, Thomas G. Lane, Guido Vollbeding
$ jpegoptim --stdout Upper_Right.jpg  > x.jpg 
Upper_Right.jpg 261x196 24bit N Exif IPTC XMP ICC Adobe JFIF  [OK] 27862 --> 27193 bytes (2.40%), optimized.

デフォルトでは消さないようです。

オプションで -s / --strip-all をつけると ICC プロファイルも消えてしまうため、ICCプロファイルだけ残す場合は以下のようにめんどうな感じになります。

$  jpegoptim --strip-com --strip-exif --strip-iptc --strip-xmp --stdout Upper_Right.jpg  > x.jpg 

プロファイルを変換するオプションもないので、sRGB にしつつプロファイルを削除したい場合は別のツール (たとえば ImageMagick) が必要となります。

cwebp

$ cwebp -version
0.6.0
$ cwebp Upper_Right.jpg -o x.jpg
Saving file 'x.jpg'
File:      Upper_Right.jpg
Dimension: 261 x 196
Output:    1722 bytes Y-U-V-All-PSNR 44.43 45.04 44.25   44.49 dB
block count:  intra4: 59
              intra16: 162  (-> 73.30%)
              skipped block: 54 (24.43%)
bytes used:  header:             47  (2.7%)
             mode-partition:    290  (16.8%)
 Residuals bytes  |segment 1|segment 2|segment 3|segment 4|  total
    macroblocks:  |       5%|       5%|       8%|      80%|     221
      quantizer:  |      36 |      36 |      32 |      23 |
   filter level:  |      11 |       8 |       6 |       5 |

デフォルトでICCプロファイルが消滅します。

cwebp  -metadata icc Upper_Right.jpg -o x.jpg

で ICC プロファイルだけ残せます。

プロファイルを変換するオプションもないので、sRGB にしつつプロファイルを削除したい場合は別のツール (たとえば ImageMagick) が必要となります。

ref

2017年 05月 30日

デジカメRAWファイルのサイドカーJPEGファイルを削除する Lightroom プラグイン

WIFI転送のためαシリーズで撮影するときは RAW+JPEG にしています。

RAW+JPEG で撮った写真を Lightroom でメモリーカードからコピーしてくるとき、JPEG ファイルも同時にコピーされてきてしまいます。しかし RAW があれば JPEG は基本的に再現可能といえるわけですから、もはやとっておく意味はなく、ディスクの無駄づかいということになります。1枚あたり10MB超であることも珍しくはないので、無視できるほどではないなと思います。

理想的にはこの挙動をやめてRAWだけをコピーできれば、取り込み時間短縮にもなって嬉しいわけですが、現状の Lightroom ではできないようです。

Lightroom プラグインにしてみた

Lightroom プラグインあまりおもしろくないのでやる気が沸かなかったのですが、せっかくなので書いてみました。既に機能的にありそうですけどないんですかね……

  1. 対象ファイルを探す (キャンセル付きモーダルプログレス)
  2. 確認
  3. 実際の削除 (バックグランドプログレス・左上にでるやつ)

JS で非同期に慣れていれば、Lua は実質的に JS とほぼ一緒なので考えかたは難しくない気がします。しかしリファレンスマニュアルがあるとはいえいまいち情報が少ないのが面倒な感じです。

>

2017年 05月 21日

複数デバイス時のロケーション履歴

同一アカウントでログインしているタブレットを家に置いたまま家を出たりすると、ロケーション履歴が乱れてしまう。ロケーション履歴は複数台ログインして記録している場合、それらが合算された履歴になるため。

この場合、本当に常時持ち歩くデバイスのロケーション履歴だけを有効にしたくなる。具体的には以下のヘルプに書いてある。

https://support.google.com/accounts/answer/3118687?hl=ja

ログインしているどのデバイスでもいいので、ロケーション履歴の設定を辿れば、その時点でログインしているすべてのデバイスごとに有効/無効を切り替えられる。

HTML要素が見える範囲にあるかの判定

HTML上のある要素が見える範囲にあるかを JavaScript で判定したい。なんか1発で判定できるメソッドがあった気がしたが見つからなかったので、以下のようになった。

/**
 * element の一部でも見える位置にあるか
 * margin が指定された場合、その分上下左右にビューポートを拡大(または負の場合は縮小)しているとみなして判定する
 */
function isInViewport (e, margin) {
	const rect = e.getBoundingClientRect();
	if (!margin) margin = 0
	return (
		rect.bottom >= -margin &&
		rect.right  >= -margin &&
		rect.top    <= window.innerHeight + margin &&
		rect.left   <= window.innerWidth  + margin
	);
}

/**
 * element 全体が見えているか
 */
function isWholeInViewport (e) {
	const rect = e.getBoundingClientRect();
	return (
		rect.top    >= 0 &&
		rect.left   >= 0 &&
		rect.bottom <= window.innerHeight &&
		rect.right  <= window.innerWidth
	);
}

id:vantguarde 「なんか1発で判定できるメソッドがあった気がしたが」一発ではないかもしれないけどIntersection Observerかしら https://wicg.github.io/IntersectionObserver/

http://b.hatena.ne.jp/vantguarde/20170522#bookmark-338453319

とのこと!! 書いてみるもんだ〜 ありがとうございます。

IntersectionObserver の場合はたとえば以下のように。画像をスクロールに応じて遅延ロードさせたい場合を想定

const observer = new IntersectionObserver( (entries) => {
	for (let entry of entries) {
		const section = entry.target;
		const imgs = section.imgs || section.querySelectorAll('img');
		section.imgs = imgs;
		for (let img of imgs) {
			if (entry.isIntersecting) {
				const src = img.getAttribute('data-src');
				if (img.src !== src) {
					img.src = src;
				}
			} else {
				img.removeAttribute('src');
			}
		}
	}
}, {
	rootMargin: "1000px",
	threthold: 0.0
});
const sections = document.querySelectorAll('section.dir');
for (let section of sections) {
	observer.observe(section);
}