tohokuaikiのチラシの裏

技術的ネタとか。

endroid/qr-codeのSVGでQRコードを作って2つ以上置くと問題が生じる件

Packagistでqrcodeと検索すると一番上に出てくるパッケージがこれ。

SVG出力もしてくれて使いやすいんだけど、SVGQRコードを2つ以上置くときに問題が発生した。

f:id:tohokuaiki:20201006190857p:plain
こんな感じになってしまう。
(左のQRコードの真ん中が黒いのはQRコードを読めないようにしただけ)

原因

原因は、1つ目のQRコード

<defs><rect id="block" width="3" height="3" fill="#000000" fill-opacity="1"/></defs>

としていて、2つ目のQRコード

<defs><rect id="block" width="6" height="6" fill="#000000" fill-opacity="1"/></defs>

としているんだけど、このrect#blockが被ってしまっているので、1つ目のwidth/heightを2つ目のQRコードにも使ってしまうのであった。

対応

コードを見たら

https://github.com/endroid/qr-code/blob/759e7745a80c17ab3b6b946550451413df0795d1/src/Writer/SvgWriter.php#L41

        $blockDefinition->addAttribute('id', 'block');

https://github.com/endroid/qr-code/blob/759e7745a80c17ab3b6b946550451413df0795d1/src/Writer/SvgWriter.php#L45

                    $block->addAttribute('xlink:href', '#block', 'http://www.w3.org/1999/xlink');

おぉっと、これはハードコーディングですね。

ということで、これは出力したSVGタグをstr_replaceで回避するしかない…プルリクエスト出しておいたらAcceptされてめでたしめでたし。

HTMLのcanvasで任意の4点をクリックして四角形を描く

なんか、ライブラリであるかな?と思ったけど、意外と書いちゃった方が楽なんじゃないかと思って書いた。

結論から申し上げますと、外積がメッチャ役に立った。

任意の4点から四角形を描く方法

なんかこれどう考えればいいんだろう?って思ったけど、良いのがあった。

4点をpi(i=1,2,3,4)とします.

逆の発想で線分が交わるかどうかを考えます.

線分p1-p2と線分p3-p4が交わりかつ重なっていない場合には, 四角形p1 p3 p2 p4は凸四角形になります (証明は幾何学計算幾何学の本に載っていると思います).

なので線分p1-p2と線分p3-p4, 線分p1-p3とp2-p4, 線分p1-p4と線分p2-p3が交わるかどうかを調べて交わっているものがあればそれを採用し凸四角形を作ります.

アルゴリズムをまとめると

p1-p2とp3-p4の交差判定 交わったら凸四角形p1 p3 p2 p4を描く 交わらなかったら次へ p1-p3とp2-p4の交差判定 交わったら凸四角形p1 p2 p3 p4を描く 交わらなかったら次へ p1-p4とp2-p3の交差判定 交わったら凸四角形p1 p2 p4 p3を描く 交わらなかったら凸四角形が作れないと判断する となります.

おー。なるほど。四角形なら高々3回の判定でできるわけだ。

…で、2つの線分が交点を作るかどうか…って。

2つの線分が交点を作るかどうかの判定

これも良いのがあった。

これを判定部分にすると入力値の4点pに対して四角形を描くのに都合の良い配列sが得られる。

というわけで、これを合体させて

こんな感じ。

p = [[79, 30], [283, 44], [19, 228], [325, 219]];

let is_cross = function(line1, line2) {
    var l1_from  = line1[0];
    var l1_to    = line1[1];
    var l2_from  = line2[0];
    var l2_to    = line2[1];
    var line_formula = function(l){
        return l[1]-l1_from[1]-(l[0]-l1_from[0])*(l1_to[1]-l1_from[1])/(l1_to[0]-l1_from[0]);
    }
    return line_formula(l2_from)*line_formula(l2_to)<0;
}
if (is_cross([p[0], p[1]] , [p[2], p[3]])){
    s = [p[0], p[2], p[1] , p[3]];
}
else if (is_cross([p[0], p[2]] , [p[1], p[3]])){
    s = [p[0], p[1], p[2] , p[3]];
}
else {
    s = [p[0], p[1], p[3] , p[2]];
}
console.table(s);
// s = [[79, 30], [19, 228], [325, 219], [283, 44]]

HTMLで書くとこんな感じ。

コピペで動くコードだとこんな。

<html>
<head>
<script>
var points = [];
drawSquare = function(e) {
    let cvs = document.getElementById('c');
    let ctx = cvs.getContext('2d');
    points.push([e.offsetX, e.offsetY]);
    if (points.length > 4){
        points.shift();
    }
    ctx.clearRect(0, 0, cvs.width, cvs.height);
    ctx.strokeStyle = "#14ff10";
    ctx.fillStyle   = "#ff6b6b";
    ctx.lineWidth   = 2;
    for (let i in points){
        ctx.beginPath();
        ctx.arc(points[i][0], points[i][1], 3, 0, Math.PI*2, true);
        ctx.fill();
    }
    if (points.length === 4) {
        // draw square
        let p = points.slice(0, points.length);
        let s = [];
        let is_cross = function(line1, line2) {
            var l1_from  = line1[0];
            var l1_to    = line1[1];
            var l2_from  = line2[0];
            var l2_to    = line2[1];
            var line_formula = function(l){
                return l[1]-l1_from[1]-(l[0]-l1_from[0])*(l1_to[1]-l1_from[1])/(l1_to[0]-l1_from[0]);
            }
            return line_formula(l2_from)*line_formula(l2_to)<0;
        }
        if (is_cross([p[0], p[1]] , [p[2], p[3]])){
            s = [p[0], p[2], p[1] , p[3]];
        }
        else if (is_cross([p[0], p[2]] , [p[1], p[3]])){
            s = [p[0], p[1], p[2] , p[3]];
        }
        else {
            s = [p[0], p[1], p[3] , p[2]];
        }
        // draw lines
        for (let i=0,j=s.length; i<j; i++){
            ctx.beginPath();
            ctx.moveTo(s[i][0] , s[i][1]);
            let k = (i === s.length -1 ? 0 : i + 1);
            ctx.lineTo(s[k][0] , s[k][1]);
            ctx.stroke();
        }
    }
}
</script>
</head>
<body>
<canvas width="400" height="400" style="border:1px solid #ccc; margin: 20px;" id="c" onclick="drawSquare(event)"></canvas>
</body>
</html>

更に、時計回りか反時計回りか分かるように判定ロジックを入れて確認できるようにした。

<html>
<head>
<script>
var drawVerticalLineAnim = function(p1, p2) {
    let count = 50;
    let counter = 0;
    let cvs = document.getElementById('c');
    let ctx = cvs.getContext('2d');

    var render = function() {
        counter++;
        let next = [
           Math.round( (p2[0] - p1[0])/count * counter ) + p1[0],
           Math.round( (p2[1] - p1[1])/count * counter ) + p1[1]
            ];
        ctx.beginPath();
        ctx.moveTo(p1[0], p1[1]);
        ctx.lineTo(next[0], next[1]);
        ctx.closePath();
        ctx.stroke();

        // 描画を繰り返す条件
        if (counter <= count ){
            requestAnimationFrame(render);
        }
      
    };
    render();
};

var points = [];
drawSquare = function(e) {
    let cvs = document.getElementById('c');
    let ctx = cvs.getContext('2d');
    if (points.length === 4){
        points = [];
    }
    points.push([e.offsetX, e.offsetY]);
    ctx.clearRect(0, 0, cvs.width, cvs.height);
    ctx.strokeStyle = "#14ff10";
    ctx.fillStyle   = "#ff6b6b";
    ctx.lineWidth   = 2;
    for (let i in points){
        ctx.beginPath();
        ctx.arc(points[i][0], points[i][1], 3, 0, Math.PI*2, true);
        ctx.fill();
    }
    if (points.length === 4) {
        // draw square
        let p = points.slice(0, points.length);
        let s = [];
        let is_cross = function(line1, line2) {
            var l1_from  = line1[0];
            var l1_to    = line1[1];
            var l2_from  = line2[0];
            var l2_to    = line2[1];
            var line_formula = function(l){
                return l[1]-l1_from[1]-(l[0]-l1_from[0])*(l1_to[1]-l1_from[1])/(l1_to[0]-l1_from[0]);
            }
            return line_formula(l2_from)*line_formula(l2_to)<0;
        }
        if (is_cross([p[0], p[1]] , [p[2], p[3]])){
            s = [p[0], p[2], p[1] , p[3]];
        }
        else if (is_cross([p[0], p[2]] , [p[1], p[3]])){
            s = [p[0], p[1], p[2] , p[3]];
        }
        else {
            s = [p[0], p[1], p[3] , p[2]];
        }
        // draw lines
        for (let i=0,j=s.length; i<j; i++){
            let k = (i === s.length -1 ? 0 : i + 1);
            drawVerticalLineAnim(s[i], s[k]);
            /*
            ctx.beginPath();
            ctx.moveTo(s[i][0] , s[i][1]);
            let k = (i === s.length -1 ? 0 : i + 1);
            ctx.lineTo(s[k][0] , s[k][1]);
            ctx.stroke();
            */
        }
        checkDirection(s);
    }
}
var checkDirection = function(s){
    let anti = '反';
    
    let S = 0;
    for (let i=0,j=s.length; i<j; i++){
        let a = s[i];
        let b = i == j-1 ? s[0] : s[i+1];
        S += a[0] * b[1] - b[0] * a[1];
    }
    console.log('---'); 
    if (S > 0) {
        anti =  '';
    }
    document.getElementById('direction').innerText = anti + '時計回り';
};
</script>
</head>
<body>
<p id="direction"></p>
<canvas width="400" height="400" style="border:1px solid #ccc; margin: 20px;" id="c" onclick="drawSquare(event)"></canvas>
</body>
</html>

時計回りか反時計回りかの判定ロジック

外積を使うとできる。 外積なんて20年振りに効いたわ。

当たり前だけど、多角形の場合にも使える。

外積は符号に「右向き」「左向き」の意味を持つのでこの外積の符号で判定する。

ベクトルABから見て・・・ACとの外積とADとの外積の2つがどっちとも正なら同じ向きにあるので交わらない。外積の符号が逆ならどっちかが反対回りなので交わっている。文で書くと理解できないけど、図にするとすぐわかる。自分で図を書くとすぐわかるかな。

f:id:tohokuaiki:20201006131202p:plain
交わらない場合の外積

f:id:tohokuaiki:20201009150735p:plain
交わる場合の外積

交差する判定も、外積で行った

<html>
<head>
<script>
var drawVerticalLineAnim = function(p1, p2) {
    let count = 50;
    let counter = 0;
    let cvs = document.getElementById('c');
    let ctx = cvs.getContext('2d');

    var render = function() {
        counter++;
        let next = [
           Math.round( (p2[0] - p1[0])/count * counter ) + p1[0],
           Math.round( (p2[1] - p1[1])/count * counter ) + p1[1]
            ];
        ctx.beginPath();
        ctx.moveTo(p1[0], p1[1]);
        ctx.lineTo(next[0], next[1]);
        ctx.closePath();
        ctx.stroke();

        // 描画を繰り返す条件
        if (counter <= count ){
            requestAnimationFrame(render);
        }
      
    };
    render();
};

var points = [];
drawSquare = function(e) {
    let cvs = document.getElementById('c');
    let ctx = cvs.getContext('2d');
    if (points.length === 4){
        points = [];
    }
    points.push([e.offsetX, e.offsetY]);
    ctx.clearRect(0, 0, cvs.width, cvs.height);
    ctx.strokeStyle = "#14ff10";
    ctx.fillStyle   = "#ff6b6b";
    ctx.lineWidth   = 2;
    for (let i in points){
        ctx.beginPath();
        ctx.arc(points[i][0], points[i][1], 3, 0, Math.PI*2, true);
        ctx.fill();
    }
    if (points.length === 4) {
        // draw square
        let p = points.slice(0, points.length);
        let s = [];
        /*
        // 線分の式を用いる方法
        let is_cross = function(line1, line2) {
            var l1_from  = line1[0];
            var l1_to    = line1[1];
            var l2_from  = line2[0];
            var l2_to    = line2[1];
            var line_formula = function(l){
                return l[1]-l1_from[1]-(l[0]-l1_from[0])*(l1_to[1]-l1_from[1])/(l1_to[0]-l1_from[0]);
            }
            return line_formula(l2_from)*line_formula(l2_to)<0;
        }
        */
        // 外積を用いる方法
        let is_cross = function(line1, line2){
            var a = line1[0]; // A
            var b = line1[1]; // B
            var c = line2[0]; // C
            var d = line2[1]; // D
            let s = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
            let t = (b[0] - a[0]) * (d[1] - a[1]) - (d[0] - a[0]) * (b[1] - a[1]);
            return s * t < 0 ;
        }
        if (is_cross([p[0], p[1]] , [p[2], p[3]])){
            s = [p[0], p[2], p[1] , p[3]];
        }
        else if (is_cross([p[0], p[2]] , [p[1], p[3]])){
            s = [p[0], p[1], p[2] , p[3]];
        }
        else {
            s = [p[0], p[1], p[3] , p[2]];
        }
        // draw lines
        for (let i=0,j=s.length; i<j; i++){
            let k = (i === s.length -1 ? 0 : i + 1);
            drawVerticalLineAnim(s[i], s[k]);
            /*
            ctx.beginPath();
            ctx.moveTo(s[i][0] , s[i][1]);
            let k = (i === s.length -1 ? 0 : i + 1);
            ctx.lineTo(s[k][0] , s[k][1]);
            ctx.stroke();
            */
        }
        checkDirection(s);
    }
}
var checkDirection = function(s){
    let anti = '反';
    
    let S = 0;
    for (let i=0,j=s.length; i<j; i++){
        let a = s[i];
        let b = i == j-1 ? s[0] : s[i+1];
        S += a[0] * b[1] - b[0] * a[1];
    }
    console.log('---'); 
    if (S > 0) {
        anti =  '';
    }
    document.getElementById('direction').innerText = anti + '時計回り';
};
</script>
</head>
<body>
<p id="direction"></p>
<canvas width="400" height="400" style="border:1px solid #ccc; margin: 20px;" id="c" onclick="drawSquare(event)"></canvas>
<script>
window.onload = function(){
//    drawVerticalLineAnim([10, 10], [100, 100]);
};
</script>
</body>
</html>

先ほどの判定だとzero divが起こってしまう可能性があったので。

Pythonで画像のトリミングをいい感じでやってくれる(CentOS8)

この記事を見てやってみようと思いました。

環境

こんな感じ。

[root@centos8 ~]# cat /etc/redhat-release
CentOS Linux release 8.2.2004 (Core)
[root@centos8 ~]# which python3.6
/usr/bin/python3.6
[root@centos8 ~]# which pip3.6
/usr/local/bin/pip3.6

とりあえず、環境設定

[vagrant@centos8 ~]$ pip3.6 install opencv-python
Collecting opencv-python
  Downloading https://files.pythonhosted.org/packages/38/a9/cd39fd25df434b5d9451dc266c12b72f68282a2b9bd5d7b4aa2d57d6c20e/opencv-python-4.4.0.44.tar.gz (88.9MB)
    100% |=====================| 88.9MB 16kB/s
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-build-85w38wrx/opencv-python/setup.py", line 9, in <module>
        import skbuild
    ModuleNotFoundError: No module named 'skbuild'

    ----------------------------------------
Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-build-85w38wrx/opencv-python/

となってコケる… こちらを見るとpip自体が古いみたい。

$ sudo su -
[root@centos8 ~]# pip3.6 install -U pip
WARNING: Running pip install with root privileges is generally not a good idea. Try `pip3.6 install --user` instead.
Collecting pip
  Downloading https://files.pythonhosted.org/packages/4e/5f/528232275f6509b1fff703c9280e58951a81abe24640905de621c9f81839/pip-20.2.3-py2.py3-none-any.whl (1.5MB)
    100% |=====================| 1.5MB 904kB/s
Installing collected packages: pip
Successfully installed pip-20.2.3

[root@centos8 ~]# logout
[vagrant@centos8 ~]$ pip3.6 install opencv-python
WARNING: pip is being invoked by an old script wrapper. This will fail in a future version of pip.
Please see https://github.com/pypa/pip/issues/5599 for advice on fixing the underlying issue.
To avoid this problem you can invoke Python with '-m pip' instead of running pip directly.
Defaulting to user installation because normal site-packages is not writeable
Collecting opencv-python
  Downloading opencv_python-4.4.0.44-cp36-cp36m-manylinux2014_x86_64.whl (49.5 MB)
     |=====================| 49.5 MB 498 kB/s
Collecting numpy>=1.13.3
  Downloading numpy-1.19.2-cp36-cp36m-manylinux2010_x86_64.whl (14.5 MB)
     |=====================| 14.5 MB 12.9 MB/s
Installing collected packages: numpy, opencv-python
Successfully installed numpy-1.19.2 opencv-python-4.4.0.44

インストールできた。

プログラムを動かしてみる

Pythonはほとんどやったことありません。

とりあえず、コピペしてみる。

import cv2
import numpy as np

from IPython.display import display, Image

def display_cv_image(image, format='.png'):
    decoded_bytes = cv2.imencode(format, image)[1].tobytes()
    display(Image(data=decoded_bytes))

img = cv2.imread("適当な画像.jpg")
display_cv_image(img)

# グレイスケール化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二値化
ret,th1 = cv2.threshold(gray,200,255,cv2.THRESH_BINARY)
display_cv_image(th1)

おもむろに、

$ python3.6 opencv_test.py

してみると

ModuleNotFoundError: No module named 'IPython'

なるほど…ipython(全く知らない)が必要っぽいな…ということで

$ pip3.6 install ipython

すると、何やらインストールした。この後で $ python3.6 opencv_test.py すると動いた。

display_cv_image()ってなんやねん…

これ、バイナリを出力してくれるのかと思ったけど、なんか違う。リダイレクトさせても

<IPython.core.display.Image object>

としか出ない…うーん。

とりあえずファイルに出力する

display_cv_imageというのがイマイチ良くわからないけど、とりあえずこんな感じでファイルに出力するだけにする。

def display_cv_image(image, output='output.jpg'):
     cv2.imwrite(output, image)

先ほどのエントリのコードを

import cv2
import numpy as np

from IPython.display import display, Image

def display_cv_image(image, output='output.jpg'):
     cv2.imwrite(output, image)
#    decoded_bytes = cv2.imencode(format, image)[1].tobytes()
#    display(Image(data=decoded_bytes))

img = cv2.imread("IMG_8270.jpg")
# display_cv_image(img)

# グレイスケール化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二値化
ret,th1 = cv2.threshold(gray,200,255,cv2.THRESH_BINARY)
display_cv_image(th1)

すると動いた。やった。

輪郭検出

エントリの

# 二値化
ret,th1 = cv2.threshold(gray,200,255,cv2.THRESH_BINARY)
display_cv_image(th1)

image, contours, hierarchy = cv2.findContours(th1, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 面積の大きいもののみ選別
areas = []
for cnt in contours:
    area = cv2.contourArea(cnt)
    if area > 10000:
        epsilon = 0.1*cv2.arcLength(cnt,True)
        approx = cv2.approxPolyDP(cnt,epsilon,True)
        areas.append(approx)

cv2.drawContours(img,areas,-1,(0,255,0),3)
display_cv_image(img)

をすると、エラーが出た。

got 2)
[vagrant@centos8 opencv]$ python3.6 trimming.py
Traceback (most recent call last):
  File "trimming.py", line 22, in <module>
    image, contours, hierarchy = cv2.findContours(th1, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
ValueError: not enough values to unpack (expected 3, got 2)

にある「opencvの最新版であるOpenCV4では、」ということで

[vagrant@centos8 opencv]$ pip3.6 show opencv-python
WARNING: pip is being invoked by an old script wrapper. This will fail in a future version of pip.
Please see https://github.com/pypa/pip/issues/5599 for advice on fixing the underlying issue.
To avoid this problem you can invoke Python with '-m pip' instead of running pip directly.
Name: opencv-python
Version: 4.4.0.44
Summary: Wrapper package for OpenCV python bindings.
Home-page: https://github.com/skvark/opencv-python
Author: None
Author-email: None
License: MIT
Location: /home/vagrant/.local/lib/python3.6/site-packages
Requires: numpy
Required-by:

4.4.0.44だった。ということで、

image, contours, hierarchy = cv2.findContours(th1, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

contours, hierarchy = cv2.findContours(th1, cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)

にして動いた。

laravel-mixでnpm run watchしたら無限ループに陥ったので対処したこと

何気なくsassを書き直したら、npm run watchしてるのが無限ループしてた…

原因は画像のタイムスタンプ?

原因を探ってみると、

header {
    background-image: url("../images/icon_header.png");
}

が原因っぽい。

試しに、npm run prodしてみると、無事にコンパイルされて

  DONE  Compiled successfully in 24206ms                                                                   2:45:32 AM 

                                                    Asset       Size  Chunks                    Chunk Names 
                                             /css/app.css  724 bytes       1  [emitted]         /js/user
  images/icon_header.png?5154d06ae582f6e2548c6d920674da59   1.04 KiB          [emitted] 

とか出る。images/icon_header.png のタイムスタンプ見ると

なるほど、sassコンパイルする時に画像も変更するらしい。変更するんだけど、

$ md5sum ~/resources/images/icon_header.png ~/public/images/icon_header.png
5154d06ae582f6e2548c6d920674da59  ~/resources/images/icon_header.png
5154d06ae582f6e2548c6d920674da59  ~/public/images/icon_header.png

という感じでファイル自体は変更してなさそう。

対応方法

この機能をOFFにしてしまえばいいっぽい。

直接的な対応方法

laravel-mixのオプションでこの機能をOFFにするには

mix.options({
  processCssUrls: false
})

とする。

…なんだけど、これをOFFにするのはrun prodする時だけで良いので

const is_prod = mix.inProduction() ? true : false;
mix.webpackConfig({
  devtool: is_prod ? "" : "inline-source-map",
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': __dirname + '/resources/js'
    }
  },
}).options({
  processCssUrls: is_prod
})
.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css/')

…ということで、watchしなければ問題ないので、productionコンパイルの時だけは聞くようにした

Laravelのview(bladeテンプレート)からVue.jsにグローバルパラメータを渡したい時など

最初、

<router-view app_name="{{ config('app.name') }}"></router-view>

とかしてたけど、こんなの逐一やってらんねー。

検索したけどあんまり見つからない…ajaxで渡せとかそんな面倒なこと…

ということで、「ぼくのかんがえるさいきょうのLaravelからVueへの変数の渡し方」

Laravel 側

Controllerで変数を渡す

<?php
    /**
     * @brief vue用のViewを返す
     */
    public function vue()
    {
        $params = [
            'app_name' => config('app.name')
        ];
        return view('index')
            ->with('params', $params);
    }

bladeでJavaScriptグローバル変数に振ってやる

@section('content')
<div id="app">
    <router-view></router-view>
</div>
<script>var globalParams = @json($params);</script>
<script src="{{ asset(mix('js/app.js')) }}" defer></script>
@endsection

Vue側

Vueのrootにcomputedにgparamをつける

import Vue from 'vue';
// 省略
const app = new Vue({
    el: '#app',
    // 省略
    computed: {
        gparam: function(){
            return globalParams;
        }
    },

Vueの各Vueやコンポーネントでは$rootでcallする。

<template>
<div>
     <h1>{{ $root.gparam.app_name }}</h1>

ドヤァ