mjpg-streamerのカメラ制御インターフェースを日本語化して使いやすくする方法

みなさん、こんにちは。

今回は、mjpg-streamerのカメラ制御インターフェースを日本語化する方法についてご紹介します。監視カメラシステムをmjpg-streamerで構築している方にとって、使いやすさが大きく向上するはずです。ぜひ最後までお付き合いください。

なお、本記事は Raspberry Pi 4BとLogicool C270で構築された監視カメラシステムで動作検証を行っています。

 


 

Raspberry Piとmjpg-streamerを活用した監視カメラシステム

 

Raspberry Piの代表的な利用方法のひとつが監視カメラシステムの構築です。

USB接続のWEBカメラと mjpg-streamer を組み合わせることで、簡単にライブ映像をWEBブラウザに配信できます。mjpg-streamerは軽量でRaspberry Piと相性もよく、多くのユーザーに利用されています。また、設定も比較的シンプルです。

 


 

カメラの画質調整にはコントロール機能が重要

 

実際に監視カメラを運用していると、配置場所によって「もう少し明るくしたい」「コントラストを調整したい」といったニーズが出てきます。

このようなニーズに対応するため、mjpg-streamer にはカメラ制御のための WEBインターフェースが用意されており、ブラウザから直接、明るさ・コントラスト・ホワイトバランスなどを調整できます。

でも、インターフェースが英語でわかりにくい!

mjpg-streamerのWEBインターフェースは英語表示です。当然、カメラ制御用のインターフェースも英語です。

「Brightness」「White Balance Temperature」など、意味がある程度分かる人は良いのですが、英語に慣れていない人にとっては、どの項目が何を意味するのかが直感的にわかりにくく、操作ミスにもつながりかねません。

 

そこで、日本語化してみました

mjpg-streamer のソースコードを確認すると、カメラの制御項目は v4l2-ctl コマンドで取得されています。
この出力される英語の項目を、日本語に置き換えることで、カメラ制御インターフェースをより使いやすくできるのではと考え、実装することにしました。

 

実装のポイント

ポイントは3点です。

  1. control.htm に日本語対応の辞書オブジェクトを作成
  2. 描画時に辞書に従って英語→日本語へ置き換え
  3. すでに存在するHTML構造とJavaScriptの仕組みを活かす形で、必要最小限の改修にとどめた

 

実際の日本語化コードの一部

control.htmに以下のような辞書を用意して、日本語に変換しています。

const translations = {
"Brightness": "明るさ",
"Contrast": "コントラスト",
"Saturation": "彩度",
"White Balance, Automatic": "ホワイトバランス(自動)",
...
};

これをJavaScriptで動的に適用し、インターフェース上の表示に反映させます。私の環境(Logicool C270)では、すべての制御項目が正しく日本語で表示され、問題なく動作しました。

コントロール画面
日本語化されたカメラ制御インターフェース画面

 

参考までに、修正したcontrol.htmのソース全文は以下の通りです。既存のwww/control.htmに上書きしてもらえば動作するはずです。面倒な人は私のGitHubに日本語化ずみのmjpg-streamerのリポジトリがありますので、そちらをご利用ください。

<html style="overflow-y: auto;">
  <head>
    <script type="text/javascript" src="jquery.js"></script>
    
	<link type="text/css" href="jquery.ui.custom.css" rel="stylesheet" />
    <script type="text/javascript" src="jquery.ui.core.min.js"></script>    
    <script type="text/javascript" src="jquery.ui.widget.min.js"></script>    
    <script type="text/javascript" src="jquery.ui.tabs.min.js"></script>    
            
    <link type="text/css" rel="stylesheet" href="JQuerySpinBtn.css" />
    <script type="text/javascript" src="JQuerySpinBtn.js"></script>    
    
	<script type="text/javascript">
	$(function() {
		$("#tabs").tabs();
	});
	
	$(document).ready(function() {
		//top.resizeTo($(window).width(), $(document).height() + (top.outerHeight - $(window).height()));
	});
	</script>

  </head>
  <body style="overflow-y: auto;">
    <script type="text/javascript"> 
        // 翻訳用の辞書
        const translations = {
            // タイトルの翻訳
            "User Controls": "ユーザーコントロール",
            "Camera Controls": "カメラコントロール",
            // コントロール名の翻訳
            "Brightness": "明るさ",
            "Contrast": "コントラスト",
            "Saturation": "彩度",
            "Hue": "色相",
            "White Balance, Automatic": "ホワイトバランス(自動)",
            "Gamma": "ガンマ",
            "Gain": "ゲイン",
            "Power Line Frequency": "電源周波数",
            "White Balance Temperature": "ホワイトバランス温度",
            "Sharpness": "シャープネス",
            "Backlight Compensation": "逆光補正",
            "Auto Exposure": "自動露出",
            "Exposure Time, Absolute": "露出時間(絶対値)",
            "Exposure, Dynamic Framerate": "露出、動的フレームレート",
            "Pan, Absolute": "パン(絶対値)",
            "Tilt, Absolute": "チルト(絶対値)",
            "Focus, Absolute": "フォーカス(絶対値)",
            "Focus, Automatic Continuous": "フォーカス(自動)",
            "Zoom, Absolute": "ズーム(絶対値)",
            "JPEG quality": "JPEG品質",
            
            // メニュー選択肢の翻訳
            "Disabled": "無効",
            "Enabled": "有効",
            "Manual Mode": "手動モード",
            "Auto Mode": "自動モード",
            "Shutter Priority Mode": "シャッター優先モード",
            "Aperture Priority Mode": "絞り優先モード",
            "50 Hz": "50 Hz",
            "60 Hz": "60 Hz",
            "Off": "オフ",
            "On": "オン"
        };
        
        // 翻訳関数
        function translate(text) {
            return translations[text] || text;
        }
        		
		function setControl(dest, plugin, id, group, value) {
          $.get('./?action=command&dest=' +		dest +
          						'&plugin=' +	plugin+
          						'&id='+ 		id + 
          						'&group='+ 		group + 
          						'&value=' +		value );
        }

        function setControl_bool(dest, plugin, id, group, value) {
          if (value == false)
            setControl(dest, plugin, id, group, 0);
          else
            setControl(dest, plugin, id, group, 1);
        }

        function setControl_string(dest, plugin, id, group, value) {
          if (value.length < minlength) {
            alert("The input string has to be least"+minlength+" characters!");
            return;
          }
          $.get('./?action=command&dest=' +		dest +
          						'&plugin=' +	plugin+
          						'&id='+ 		id + 
          						'&group='+ 		group + 
          						'&value=' +		value , 
			function(data){
             alert("Data Loaded: " + data);
           });
        }
                        
        function setResolution(plugin, controlId, group, value) {
	        $.get('./?action=command&dest=0'	+		// resolution command always goes to the input plugin
					'&plugin=' +	plugin+
					'&id'+ 			controlId + 
					'&group=1'	+					// IN_CMD_RESOLUTION == 1,		
					'&value=' +		value, 
				function(data){
				     if (data == 0) {
				     	$("#statustd").text("Success");
				     } else {
				     	$("#statustd").text("Error: " + data);
				     }
		        }
	        );
        }
                
        function addControl(plugin_id, suffix) {
        var dest = suffix=="in"?0:1;
        $.getJSON(suffix+"put_"+plugin_id+".json",
          function(data) {
            $.each(data.controls, function(i,item){
              $('<tr/>').attr("id", "tr_"+suffix+"_"+plugin_id+"_"+item.group+"_"+item.id).appendTo("#controltable_"+suffix+"-"+plugin_id);
              // BUTTON type controls does not have a label 
              if (item.type == 4) {
                $("<td/>").appendTo("#tr-"+item.id);
              } else {
                if (item.type == 6) { // Class type controls
                  $("<td/>").text(translate(item.name)).attr("style", "font-weight:bold;").appendTo("#tr_"+suffix+"_"+plugin_id+"_"+item.group+"_"+item.id);
                  return;
                } else {
                  $("<td/>").text(translate(item.name)).appendTo("#tr_"+suffix+"_"+plugin_id+"_"+item.group+"_"+item.id);
                }
              }

              $("<td/>").attr("id", "td_ctrl_"+suffix+"_"+plugin_id+"_"+item.group+"-"+item.id)
              	.appendTo("#tr_"+suffix+"_"+plugin_id+"_"+item.group+"_"+item.id);
              if((item.type == 1) || (item.type == 5)) { // integer type controls
                if ((item.id == 10094852) && (item.group == 1) && (item.dest == 0)) { //V4L2_CID_PAN_RELATIVE
				  $("<button/>")
                  .attr("type", "button")
                  .attr("style", "width: 50%; height: 100%;")
                  .text("<")
                  .click(function(){setControl(dest, plugin_id, item.id, item.group, 200);})
                  .appendTo("#td_ctrl_"+suffix+"_"+plugin_id+"_"+item.group+"-"+item.id);
                  $("<button/>")
                  .attr("type", "button")
                  .attr("style", "width: 50%; height: 100%;")
                  .text(">")
                  .click(function(){setControl(dest, plugin_id, item.id, item.group, -200);})
                  .appendTo("#td_ctrl_"+suffix+"_"+plugin_id+"_"+item.group+"-"+item.id);
                } else if ((item.id == 10094853) && 
                		   (item.group == 1) && 
                		   (item.dest == 0)){ // V4L2_CID_TILT_RELATIVE
        		   $("<button/>")
                  .attr("type", "button")
                  .attr("style", "width: 50%; height: 100%;")
                  .text("^")
                  .click(function(){setControl(dest, plugin_id, item.id, item.group, -200);})
                  .appendTo("#td_ctrl_"+suffix+"_"+plugin_id+"_"+item.group+"-"+item.id);
                  $("<button/>")
                  .attr("type", "button")
                  .attr("style", "width: 50%; height: 100%;")
                  .text("ˇ")
                  .click(function(){setControl(dest, plugin_id, item.id, item.group, 200);})
                  .appendTo("#td_ctrl_"+suffix+"_"+plugin_id+"_"+item.group+"-"+item.id);
                } else { // another non spec control
                    var options = {min: item.min, max: item.max, step: item.step,}
		            $("<input/>")
		              .attr("value", item.value)
		              .attr("id", "spinbox-"+item.id)
		              .SpinButton(options)
		              .bind("valueChanged", function() {setControl(dest, plugin_id, item.id, item.group, $(this).val());})
		              .appendTo("#td_ctrl_"+suffix+"_"+plugin_id+"_"+item.group+"-"+item.id);
                } 
              } else if (item.type == 2) { // boolean type controls
                if (item.value == "1")
                  $("<input/>")
                    .attr("type", "checkbox")
                    .attr("checked", "checked")
                    .change(function(){setControl_bool(dest, plugin_id, item.id, item.group, ($(this).attr("checked")?1:0));})
		            .appendTo("#td_ctrl_"+suffix+"_"+plugin_id+"_"+item.group+"-"+item.id);
                else
                  $("<input/>")
                    .attr("type", "checkbox")
                    .change(function(){setControl_bool(dest, plugin_id, item.id, item.group, ($(this).attr("checked")?1:0));})
                    .appendTo("#td_ctrl_"+suffix+"_"+plugin_id+"_"+item.group+"-"+item.id);
              } else if (item.type == 7) { // string type controls
                  $("<input/>").attr("value", item.value).appendTo("#td_ctrl_"+suffix+"_"+plugin_id+"_"+item.group+"-"+item.id);
              } else if (item.type == 3) { // menu
                $("<select/>")
                  .attr("name", "select-"+item.id)
                  .attr("id", "menu-"+item.id)
                  .attr("style", "width: 100%;")
                  .change(function(){setControl(dest, plugin_id, item.id, item.group, $(this).val());})
                  .appendTo("#td_ctrl_"+suffix+"_"+plugin_id+"_"+item.group+"-"+item.id);
                $.each(item.menu, function(val, text) {
                    const translatedText = translate(text);
                    if (item.value == val) {
                      $("#menu-"+item.id).append($('<option></option>').attr("selected", "selected").val(val).html(translatedText));
                    } else {
                      $("#menu-"+item.id).append($('<option></option>').val(val).html(translatedText));
                    }
                });
              } else if (item.type == 4) { // button type
                $("<button/>")
                  .attr("type", "button")
                  .attr("style", "width: 100%; height: 100%;")
                  .text(translate(item.name))
                  .click(function(){setControl(dest, plugin_id, item.id, item.group, 0);})
                  .appendTo("#td_ctrl_"+suffix+"_"+plugin_id+"_"+item.group+"-"+item.id);
              } else if (item.type == 7) { // string  type
                $("<input/>")
                    .attr("type", "text")
                    .attr("maxlength", item.max)
                    .change(function(){setControl_string(dest, plugin_id, item.id, item.group, $(this).text());})
                    .appendTo("#td_ctrl_"+suffix+"_"+plugin_id+"_"+item.group+"-"+item.id);
              } else {
                 alert("Unknown control type: "+item.type);
              }
            });
          }
        );
        }

	    $.getJSON("program.json", 
	    	function(data) {
	    		$.each(data.inputs, 
	    			function(i,input){
		        		$("<li/>").attr("id", "li_in-"+input.id).appendTo("#ul_tabs");
						$("<a/>").attr("href", "#controldiv_in-"+input.id)
							.text(translate(input.name)).appendTo("#li_in-"+input.id);
						$("<div/>").attr("id", "controldiv_in-"+input.id).appendTo("#tabs");
						$("<table/>").attr("id", "controltable_in-"+input.id).appendTo("#controldiv_in-"+input.id);
		    		}
	    		)
	    		
	    		$.each(data.outputs, 
	    			function(i,output){
		        		$("<li/>").attr("id", "li_out-"+output.id).appendTo("#ul_tabs");
						$("<a/>").attr("href", "#controldiv_out-"+output.id)
							.text(translate(output.name)).appendTo("#li_out-"+output.id);
						$("<div/>").attr("id", "controldiv_out-"+output.id).appendTo("#tabs");
						$("<table/>").attr("id", "controltable_out-"+output.id).appendTo("#controldiv_out-"+output.id);
		    		}
	    		)
	    	
	    		$.each(data.inputs, 
	    			function(i,input){
	    				addControl(input.id, "in");
		    		}
	    		)
	    		
	    		$.each(data.outputs, 
	    			function(i,output){
	    				addControl(output.id, "out");
		    		}
	    		)
	    		
	    		$( "#tabs" ).tabs();
	    	}
	    );

       $(function() {
			
		});
        /*$.getJSON("input.json",
          function(data) {
          	$.each(data.formats, function(i,item){
          	  if (item.current == "true") {
          	  $("<select/>")
                  .attr("id", "select-resolution")
                  .attr("style", "width: 100%;")
                  .change(function(){setResolution($(this).val());})
                  .appendTo("#resolutions");
           	    $.each(item.resolutions, function(val,res){
           	    	if (item.currentResolution == val) {
                      $("#select-resolution").append($('<option></option>').attr("selected", "selected").val(val).html(res));
                    } else {
                      $("#select-resolution").append($('<option></option>').val(val).html(res));
                    }
           	    });
          	  }
          	});
          });*/
    </script>
    
    <div id="tabs">
		<ul id="ul_tabs"></ul>
  	</div>

  </body>
</html>

注意点

  • カメラによって制御項目が異なる場合があります。そのため、カメラを変更した際には辞書を再編集する必要があるかもしれません。
  • 英語表記が標準である理由の一つは、カメラのファームウェアやOSの地域設定による影響を受けないためです。翻訳ミスがないよう、確認しながら作業するのがベストです。

 


 

おわりに

 

mjpg-streamerの英語インターフェースが障壁になっていた方も、日本語化することで格段に使いやすくなるはずです。
特に、非エンジニアの方や教育・福祉用途など、幅広いユーザーにとって大きな助けとなるでしょう。

ご興味のある方は、ぜひ一度日本語化にチャレンジしてみてください!

 

本日も最後までお読みいただきありがとうございました。

それでは、よいIoTライフを!

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール