Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
8
Help us understand the problem. What is going on with this article?
@hellscare

Webで作るカメラアプリ

More than 1 year has passed since last update.

将来的にPWAがiOSでも全実装されることを期待して、Webでカメラアプリを作ってみようと思い作ってみました。

お題

拙作ノスタルジックフォトフィルターをWebアプリ化します。
もう9年前なんですねー懐かしい。
※アプリは配布を終了しております。

IMG_0147.PNG

ソース

ここにあります。
https://github.com/miura333/nosfilter-pwa2

開発環境

項目 内容 備考
サーバー ロリポップ 独自ドメイン取得、無料SSL
言語 PHP7.1
フレームワーク Laravel 5.7.13
DB 使用せず
画像処理ライブラリ GD
検証端末 iOS12.0.3, 11.3

ロリポ環境にLaravel入れて動作させています。
全然フレームワークのメリットを活かしていませんすいません。

前提条件

  • PHPで書きます
  • サーバー内になるべくファイルを残さないようにします

解説

カメラから画像取得

  • カメラですが、manifest.jsonのdisplayがfullscreen、またはmetaタグのapple-mobile-web-app-capableをyesに設定し、フルスクリーンで表示させようとするとエラーとなりカメラ映像を表示させることができませんでした。
  • SafariのPWA対応、というよりはフルスクリーンか否かによって動作のする、しないが変わるようです。
manifest.json
{
    "name": "nosfilter-pwa",
    "short_name": "nosFilter",
    "background_color": "#ffffff",
    "icons": [{
        "src": "./icon-152.png",
        "sizes": "152x152",
        "type": "image/png"
    },{
        "src": "./icon-120.png",
        "sizes": "120x120",
        "type": "image/png"
    }],
    "start_url": "./?utm_source=homescreen",
    "display": "browser",
    "theme_color": "#f7f7f7"
}
  • HTML側ですが、ボタン押下後にカメラ映像をcanvasに描画し、それをjpeg形式で出力しています。
  • 描画する前にカメラの縦横サイズとcanvasのサイズを同じにします。
index.blade.php
<body class="cameraBody">
    <video id="video" autoplay playsinline></video>
    <div class="cameraSpacer"></div>
    <div class="cameraButtonParent">
        <div class="cameraButtonParent2">
            <input id="btnPicture" type="button" disabled="true" value="OK" class="cameraBtn"></input>
        </div>
    </div>
    <canvas id="imageCanvas" style="display:none;" width="300" height="300"></canvas>
    {!! Form::open(['url' => '/result', 'id' => 'formResult']) !!}
    {!! Form::hidden('imageData', null, ['id' => 'imagePost']) !!}
    {!! Form::close() !!}

    <script>
        const medias = {audio : false, video : {
            facingMode : {
              exact : "environment" // リアカメラにアクセス
            }
        }},
        video  = document.getElementById("video");

        navigator.getUserMedia(medias, successCallback, errorCallback);

        var width = 0, height = 0;

        video.addEventListener( "loadedmetadata", function (e) {
            width = this.videoWidth;
            height = this.videoHeight;
        }, false );

        function successCallback(stream) {
            video.srcObject = stream;

            var button = document.getElementById("btnPicture");
            var canvas = document.getElementById("imageCanvas");

            button.disabled = false;
            button.onclick = function() {
                canvas.width = width;
                canvas.height = height;
                canvas.getContext("2d").drawImage(video, 0, 0, width, height, 0, 0, width, height);
                var img = canvas.toDataURL("image/jpeg");

                $('#imagePost').val(img);
                $('#formResult').submit();
            };
        };

        function errorCallback(err) {
            alert(err);
        };
    </script>
</body>

画像処理

  • 画像処理はFormで受け取ったデータをファイル保存、そのファイルをGDで読み込んでいます。
  • GDでデータから読み込む方法が分からなかったための苦肉の策。最終的にファイルは削除しています。
  • あとはGD使ってピクセルデータにアクセスし画像処理の流れです。

以下はソースの一部、グレースケール化している箇所の抜粋です。

imageController.php

$img = str_replace('data:image/jpeg;base64,', '', $request->imageData);
$img = str_replace(' ', '+', $img);
$data = base64_decode($img);
$timestamp = time();
$file = $timestamp . ".jpeg";
Storage::disk('local')->put('public/'.$file, $data);
$storagePath = asset('storage/');
Log::debug($storagePath);

$src = imagecreatefromjpeg('storage/'.$file);

$imageWidth = imagesx($src);
$imageHeight = imagesy($src);

$grayImage = imagecreatetruecolor($imageWidth, $imageHeight);

for($i = 0; $i < $imageWidth; $i++){
    for($j = 0; $j < $imageHeight; $j++){
        //get pixel
        $rgb = imagecolorat ($src, $i, $j);
        $r = ($rgb >> 16) & 0xFF;
        $g = ($rgb >> 8) & 0xFF;
        $b = $rgb & 0xFF;

        $gray = ($r*0.125)+($g*0.25)+($b*0.0625);
        $dstColor = imagecolorallocate($grayImage, $gray, $gray, $gray);
        imagesetpixel($grayImage, $i,$j, $dstColor);
    }
}

画像データ生成

  • 画像をjpeg保存しそれをbase64エンコード
  • 先頭に引数で来た時と同様のヘッダを付与しViewに渡します。このヘッダが無いとimgタグで表示されません。
imageController.php
imagejpeg($resultImage, 'storage/'.$fileOut, 80);

$dataOut = file_get_contents('storage/'.$fileOut);
$imageData64 = base64_encode($dataOut);

unlink('storage/'.$fileOut);

return view('result')->with(['imageData' => 'data:image/jpeg;base64,'.$imageData64, 'width' => $imageWidth, 'height' => $imageHeight]);

できた

IMG_8220.JPG

感想

  • 遅い。Wi-fiなら兎も角4G回線で試すとシャッター押下後の動作が非常に重いです。
  • 当初pngでデータ取得をしていたのですが、jpegに変えても体感的にそれほど変わりませんでした。
  • javascript版も作ってみようと思いますが、仮に商用利用を考えた場合にjavascriptだと画像処理部分のソースをコピーできてしまうのでどうしようかなあと。
  • とりあえず写真を撮影してピクセルデータをいじってフィルターかけるくらいは出来る、ということが分かりました。写真をアップするためだけにアプリを作って欲しいみたいな依頼が来たら提案してみようかと。
  • 画像処理サーバーはメモリ消費がやばそう。

参考

https://qiita.com/umamichi/items/0e2b4b1c578e7335ba20
https://qiita.com/mu_tomoya/items/d35dbb580a561c562ca6
http://kimizuka.hatenablog.com/entry/2017/11/06/140337

8
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
hellscare
コーディングから経営まで、なんでもやります。健康第一。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
8
Help us understand the problem. What is going on with this article?