OpenCvSharpをつかう その20(ASP.NET MVCで使う Ajax編)

その19 に少し手を加え、Ajaxによりページ遷移なしに結果画像を表示できるようにしてみます。

完成形はこちらに置いてみました。自由に動かしてみてください。
http://notiz.flnet.org/CannyWebApp

今回は全部がASP.NET MVCの話で、OpenCvSharpの新しい話はありません。このシリーズに入れて良いのか微妙ですがご容赦ください。

OpenCvSharpをつかう 記事一覧

目標

先にも書きましたが、以下のページで完成形を動かしてみることができます。
http://notiz.flnet.org/CannyWebApp

2つのスライダで、Cannyエッジ検出の閾値を自由に変えられます。「Run Canny」を押すと画像が更新されます。
f:id:Schima:20140126224704p:plain

※ヘンな画像を送り付けられると困るので、アップロード機能は無くしています。
※我が家の非力なAtomサーバなので、落ちているかもしれません。ご承知おきください。

ビュー

前回は空テンプレートで作成しましたが、jQueryなどを使いたいので、今回はMVCのテンプレートで始めます。
プロジェクト作成周りは省略します。HomeControllerとIndexのビューがある前提で始めます。

Views/Shared/_Layout.cshtml

MVCAjaxを使ったり、jQuery UIを使ったりするための準備です。もうちょっと良い書き方がある?

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width" />
        <title>@ViewBag.Title</title>
        @Styles.Render("~/Content/css")
        @Scripts.Render("~/bundles/modernizr")
        <link rel="stylesheet" href="~/Content/themes/base/jquery.ui.all.css">
    </head>
    <body>
        @Scripts.Render("~/bundles/jquery")
        @RenderSection("scripts", required: false)
        <script src="@Url.Content("~/Scripts/jquery-ui-1.8.24.js")" type="text/javascript"></script>
        <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>

        @RenderBody()
    </body>
</html>

Views/Home/Index.cshtml

目標のところで貼った画像のようなビューをつくります。スライダはjQuery UIを使用しています。

@{
    ViewBag.Title = "Index";
}

<script>
    $(function () {
        var initialThreshold1 = 80;
        var initialThreshold2 = 180;
        $("#slider1").slider({
            min: 0,
            max: 255,
            value: initialThreshold1,
            step: 1,
            animate: "fast",
            slide: function (event, ui) {
                $("#amount1").html(ui.value);
                $("#threshold1").val(ui.value);
                //$("#form").submit();
            }
        }).css({
            width: "300px",
        });
        $("#slider2").slider({
            min: 0,
            max: 255,
            value: initialThreshold2,
            step: 1,
            animate: "fast",
            slide: function (event, ui) {
                $("#amount2").html(ui.value);
                $("#threshold2").val(ui.value);
                //$("#form").submit();
            }
        }).css({
            width: "300px",
        });

        // 最初の処理
        $("#amount1").html(initialThreshold1);
        $("#amount2").html(initialThreshold2);
        $("#threshold1").val(initialThreshold1);
        $("#threshold2").val(initialThreshold2);
        $("#form").submit();
    });

</script>

<h1>Index</h1>

@using (Ajax.BeginForm("Canny", "Home",
                       new AjaxOptions {
                           HttpMethod = "POST",
                           InsertionMode = InsertionMode.Replace,
                           UpdateTargetId = "resultImage"}, 
                       new { id = "form" }))
{
    <input type="hidden" id="threshold1" name="threshold1" />
    <input type="hidden" id="threshold2" name="threshold2" />

    <div style="margin-top: 10px;">
        Threshold 1: <span id="amount1" style="font-weight: bold"></span>
        <div id="slider1"></div>
    </div>
    <div style="margin-top: 5px;">
        Threshold 2: <span id="amount2" style="font-weight: bold"></span>
        <div id="slider2"></div>
    </div>
    <input type="submit" value="Run Canny" style="margin-top: 15px; font-size:larger; width:256px;" />
}

<div id="resultImage" style="margin-top:25px;"></div>

ここのキモはAjax.BeginFormで、これでAjaxによる操作を可能にします。AjaxOptionsUpdateTargetId で、AjaxによりDOMのどの要素が書き換わるかを指定しています。今回は、一番下の空の<div>を指定してあり、ここに結果画像の<img>タグが入れ込まれるイメージです。

コントローラ

Controllers/HomeController.cs

おおむね前回と似ています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.UI.WebControls.WebParts;
using OpenCvSharp;

namespace CannyWebApp.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/

        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Canny(int threshold1, int threshold2)
        {
            if (!Request.IsAjaxRequest())
            {
                return new EmptyResult();
            }

            // 異常な値のリクエストを修正
            threshold1 = Math.Min(255, Math.Max(0, threshold1));
            threshold2 = Math.Min(255, Math.Max(0, threshold2));

            // 用意された画像を読み込み
            using (var image = new IplImage(Server.MapPath(@"~/Content/Lenna.bmp"), LoadMode.GrayScale))
            {
                // Cannyエッジ検出
                using (var cannyImage = new IplImage(image.Size, BitDepth.U8, 1))
                {
                    Cv.Canny(image, cannyImage, threshold1, threshold2);

                    // Canny画像をPNGエンコードでバイト配列に変換し、さらにBase64エンコードする
                    byte[] cannyBytes = cannyImage.ToBytes(".png");
                    string base64 = Convert.ToBase64String(cannyBytes);
                    ViewBag.Base64Image = base64;
                    return PartialView("_CannyImage");
                }
            }
            
        }
    }
}

Content/Lenna.bmp は用意して置いてください。Server.MapPathで、動作時には実際の物理パスに置き換えられます。

最も違うのは、PartialViewで返しているところでしょう。これにより、ビューの一部分だけを差し替えるようなことができます。
この時点では_CannyImageビューは無いので文字列が赤くなっていると思います。次はこれを作ります。ビューを作るにあたって必要なデータ、すなわちCannyの結果画像は、ViewBag.Base64Imageに入れてとっておきます。

PartialView

Views/Home/_CannyImage.cshtml

ソリューションエクスプローラのViewsのところで右クリックし、新規作成します。このとき、「部分ビューとして作成する」に必ずチェックを入れてください。(部分ビューとPartialViewがいささか結びつきにくいですね。)
f:id:Schima:20140126230424p:plain

中身はシンプルに、<img>タグ1つ。その19で取った方法同様に、Base64文字列によりオンメモリの画像データを表示します。

@Html.Raw(String.Format("<img src='data:image/png;base64,{0}' />", ViewBag.Base64Image));


これで完成です。こんなふうになるでしょうか。

Index.cshtmlのスライダのコールバックでコメントアウトしているところを有効にすると、スライダ操作の度に画像が更新されるようになります。OpenCVのHighGUIには充分追いついた感じで、ローカルで動かす分には大変面白いです。