2023-03-14 13:56

[OpenCV] 以最亮顏色的色相來進行灰階處理

OpenCV-Python 教學 https://docs.opencv.org/3.4.16/d6/d00/tutorial_py_root.html

原始圖片

原圖因為燈光散射的關係,整個圖片都出現發紅的現象


預設灰階 cv2.imread('01.jpg', cv2.IMREAD_GRAYSCALE)

有部分的色差變得不明顯,之後要進行辨識變得很困難


色相比例的灰階

透過色相的方式就好很多


HSV 的數值定義

  • H: 色相 0~360度
  • S: 飽和度 0~1
  • V: 明度 (黑)0~1(白)

OpenCV HSV 的數值定義

  • H: 0 ~ 180 => H / 2
  • S: 0 ~ 255 => S * 255
  • V: 0 ~ 255 => V * 255

相依套件:

pip install opencv-python
pip install py-linq     # https://viralogic.github.io/py-enumerable/

程式:

from py_linq import Enumerable
import cv2
import numpy as np


def get_hue(img):
    ''' 圖片色相 '''

    img = cv2.resize(img, (32, 32), interpolation = cv2.INTER_LINEAR)
    img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    pixel_hsv = np.reshape(img_hsv,(-1,3))

    result = Enumerable(pixel_hsv)\
        .order_by_descending(lambda p: p[2])\
        .then_by_descending(lambda p: p[1])\
        .first()

    print(f'[result]: {result}')
    return result[0]



def get_gray_rate(hue):
    ''' 根據色相計算灰階比例 '''

    hue_full = np.uint8([[[hue,255,255]]])
    hue_bgr = cv2.cvtColor(hue_full, cv2.COLOR_HSV2BGR)
    (hue_b, hue_g, hue_r) = hue_bgr[0,0]

    hue_total =  0.0 + hue_b + hue_g + hue_r

    return [
        hue_b / hue_total,
        hue_g / hue_total,
        hue_r / hue_total,
    ]



img = cv2.imread('01.jpg')

hue = get_hue(img)
gray_rate = get_gray_rate(hue) # 灰階比例

print(f'hue:{hue}, gray_rate: {gray_rate}')

# 灰階轉換
img_gray = cv2.transform(img, np.array([gray_rate]))

cv2.imshow("img", img)
cv2.imshow("img_gray", img_gray)
cv2.waitKey(0)
cv2.destroyAllWindows()
2023-03-14 13:23

[ThreeJS] 噴水粒子

要完成這個效果有三個問題要解決

  1. 自然的粒子分布
  2. 粒子的拋物線移動
  3. 粒子的擴散效果

我採用簡單的方式達成

  1. 用亂數產生分段的粒子團
  2. 用二次貝茲曲線建立拋物線
  3. 用粒子團的放大產生擴散效果

因為我用簡單的亂數分布粒子,粒子團會有立方體現象,球體分布會更好,但多個區段連起來就不明顯了。

二次貝茲曲線最大的困難在計算出結尾座標的位置,透過轉換到世界空間去定位出平面高度的座標,再轉換回物體空間來計算出結尾座標。

動畫用 AnimationMixer 去控制,只要定出每段粒子團的開始播放時間,粒子團就會接續的移動。

circle.png

程式:

/* 初始化渲染器 */
const renderer = new THREE.WebGLRenderer({ antialias: true });
document.getElementById('Container').appendChild(renderer.domElement);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);

/* 初始化場景 */
const scene = new THREE.Scene();
scene.background = new THREE.Color('#111'); /* 背景顏色 */
scene.add(new THREE.AmbientLight('#FFF', 0.5)); /* 加入環境光 */

/* 地面 */
const floorMesh = new THREE.Mesh(
    new THREE.PlaneGeometry(200, 200),
    new THREE.MeshBasicMaterial({ color: '#DDD', depthWrite: false })
);
floorMesh.rotation.x = THREE.MathUtils.degToRad(-90);
scene.add(floorMesh);


/* 初始化鏡頭 */
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 300);
camera.position.set(0, 10, 22);

/* 初始化軌道控制,鏡頭的移動 */
const orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
orbitControls.update();


/* 動畫刷新器 */
const mixers = [];

function newMixer(target) {
    const mixer = new THREE.AnimationMixer(target);
    mixers.push(mixer);
    return mixer;
}

/* 動畫計時器 */
const clock = new THREE.Clock();

/* 渲染週期 */
function renderCycle() {
    const delta = clock.getDelta();
    mixers.forEach(x => x.update(delta));

    renderer.render(scene, camera);
    requestAnimationFrame(renderCycle);
}
renderCycle();



/*---------------------------------------------------------------*/

/* 水管物件,水滴粒子群組會附加在這裡 */
const pipeMesh = new THREE.Mesh(
    new THREE.CylinderGeometry(0.5, 0.5, 9, 16),
    new THREE.MeshBasicMaterial( {color: '#eea236'} )
);
pipeMesh.rotation.x = THREE.MathUtils.degToRad(0);
pipeMesh.position.set(0, 4, 0);
scene.add(pipeMesh);


const dripGroup = new THREE.Group();
dripGroup.position.set(0, 4, 0);
pipeMesh.add(dripGroup);


/* 水滴材質 */
const dripMaterial = new THREE.PointsMaterial({
    map: new THREE.TextureLoader().load('circle.png'),
    color: '#0aa',
    size: 1,
    opacity: 0.7,
    depthWrite: false,
    transparent: true,
});


/* 建立水滴,用亂數建立粒子點 */
for (let i = 0; i < 60; i++) {
    const vertices = [];

    for (let j = 0; j < 40; j++) {
        const x = Math.random() - 0.5;
        const y = Math.random() - 0.5;
        const z = Math.random() - 0.5;
        vertices.push(x, y, z);
    }

    const geometry = new THREE.BufferGeometry();
    geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));

    const particles = new THREE.Points(geometry, dripMaterial);
    dripGroup.add(particles);

    newMixer(particles); /* 水滴動畫 */
}


/*---------------------------------------------------------------*/

const cycle = 2; /* 循環週期,會影響水流速度 */
const scale = 8; /* 擴散大小 */
const length = 14; /* 水流長度 */


/* 二次貝茲曲線,用來取得拋物線的座標點 */
const curve = new THREE.QuadraticBezierCurve3();
curve.v1.set(0, length, 0); /* 曲線控制點座標 */


/* 計算結尾座標 */
const rootPos = dripGroup.getWorldPosition(new THREE.Vector3());
const quaternion = dripGroup.getWorldQuaternion(new THREE.Quaternion());
const toPos = curve.v1.clone();
toPos.applyQuaternion(quaternion); /* 轉換到世界空間 */

/* 當水流是向上時,增加平面位置 */
if (toPos.y > (length / 3)) {
    toPos.x *= 1.8;
    toPos.z *= 1.8;
}
toPos.y = Math.min(-rootPos.y, toPos.y * 1.5); /* 將結尾拉回平面高度 */
toPos.applyQuaternion(quaternion.conjugate()); /* 轉換回物體空間 */
curve.v2.copy(toPos); /* 曲線結尾點座標 */


/* 建立拋物線及擴散動畫 */
const points = curve.getPoints(10); /* 曲线分段 */

const curveTime = [];
const curvePos = [];
points.forEach((v, i) => {
    curveTime.push(cycle * i / points.length);
    curvePos.push(v.x, v.y, v.z);
});
const posTrack = new THREE.VectorKeyframeTrack('.position', curveTime, curvePos);
const scaleTrack = new THREE.VectorKeyframeTrack('.scale', [0, cycle], [0, 0, 0, scale, scale, scale]);
const clip = new THREE.AnimationClip('scale', cycle, [posTrack, scaleTrack]);

mixers.forEach((mixer, i) => {
    const action = mixer.clipAction(clip);
    action.time = cycle * i / mixers.length;
    action.play();
});
2023-03-10 14:33

TypeScript 強轉型的地雷

用 Visual Studio 撰寫 TypeScript 一整個就很開心開心,只要好好的進行型態宣告,就可享受到跟強型別一樣的 IntelliSense 提示,實在太開心了,然後就大意了,忘記了其實骨子裡還是 JavaScript 這件事。

在取回後端的 json 資料的時候,貪圖方便就直接進行強轉型,哈哈!這裡存在不確定性,我就踩到地雷了!當然如果後端是可靠的,直接強轉型是不會有問題的。

我想都已經進行型別宣告了,難到不能自動轉型嗎?哈哈!在網路上找了很久都沒找到,找到的方式都需要二次宣定去做轉型。

簡單的展示一下這個問題,建立一個 any 的物件 a,然後強轉型成 DataModel 的物件 b,這時候 a 跟 b 其實還是同一個 Instance

interface DataModel {
    id: number;
    status: string;
}

let a: any = { id: '1', status: 3 };

let b: DataModel = a as DataModel;

在存取 property 時都還是 a 的資料型態,這樣在調用該型態的方法時就會出錯。

為了型態的正確必須進行轉換:

let b: DataModel = <DataModel>{
    id: Number(a.id),
    status: String(a.status),
};
2023-03-08 16:45

[Angular] 簡單製作多國語系

我想要用一種簡單而且直覺的方式製作多國語系,想像的用法如下:

user-list.component.html

<h2>{{lang['T_編輯使用者']}}</h2>

<div>
    <p *ngIf="lang.id == 'en'">
    Complicated English description...
    </p>
    <p *ngIf="lang.id == 'tw'">
    複雜的中文描述...
    </p>
</div>

<button type="submit">{{lang['B_儲存']}}</button>

<select [(ngModel)]="lang.id" (change)="lang.change()">
    <option *ngFor="let x of lang.options | keyvalue" [value]="x.key">{{x.value}}</option>
</select>

我希望能使用中文變數,有熟悉的文字可以讓程式看起來親切一點,對於複雜的文字區段也保留彈性,簡單的切換方式,以及狀態保留,讓 browser 紀錄選擇的語系。

user-list.component.ts

import { Component, OnInit } from '@angular/core';
import { AppLang } from 'app/app.lang';

@Component({
    selector: 'app-user-list',
    templateUrl: './user-list.component.html'
})
export class UserListComponent implements OnInit {

    /** 狀態選項 */
    statusItems = {
        'y': this.lang.E_啟用,
        'n': this.lang.E_停用,
    };

    constructor(
        public lang: AppLang
    ) { }

    ngOnInit() {
        let msg = String(this.lang.M_請填寫_0_的刻度值).format(3);
    }
}

已經想好使用情境了,接著來就是完成實作了!

app.lang.ts

import { Injectable } from '@angular/core';

/** 多國語言 */
@Injectable()
export class AppLang {

    constructor() {
        let _: any = {}; /* 語系字典 */
        let self: any = this;
        let names: string[] = Object.keys(this).filter(n => !'id,options,change'.includes(n));

        /* 重組語系字典 */
        Object.keys(this.options).forEach(id => {
            _[id] = {};
            names.forEach(name => _[id][name] = self[name][id]);
        });

        /* 複寫 change */
        this.change = function () {
            if (!_[this.id]) { this.id = 'tw'; }

            Object.assign(this, _[this.id]);
            localStorage['langId'] = this.id;
        }

        this.change ();
    }


    id: string = localStorage['langId'];

    options: any = {
        tw: '中文',
        en: 'English',
    };

    /** 變更語言 */
    change() { }



    /*=[ Title ]=============================================*/

    T_編輯使用者: any = {
        tw: '編輯使用者',
        en: 'Edit User',
    };
    //...

    /*=[ Field ]=============================================*/

    F_名稱: any = {
        tw: '名稱',
        en: 'Name',
    };
    //...

    /*=[ Button ]=============================================*/

    B_儲存: any = {
        tw: '儲存',
        en: 'Save',
    };
    //...


    /*=[ Enum ]=============================================*/

    E_啟用: any = {
        tw: '啟用',
        en: 'Enable',
    };
    E_停用: any = {
        tw: '停用',
        en: 'Disable',
    };
    //...


    /*=[ Message ]=============================================*/

    M_請填寫_0_的刻度值: any = {
        tw: '請填寫 {0} 的刻度值',
        en: 'Please fill in the scale value of {0}.',
    };
    //...

}

app.module.ts 配置

//...
import { AppLang } from 'app/app.lang';

@NgModule({
    declarations: [
        //...
    ],
    imports: [
        //...
    ],
    providers: [
        //...
        AppLang
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }
2023-03-08 14:21

[Angular] 將 select, radio 改成一般等於 (==)

Angular 預設的 select, radio 比對是用全等於(===),這對用 string 的選項來說沒甚麼問題,但如果是數字選項就會出現型態不一致的問題,就必須型態轉換,這有點煩人,所以想要改成一般等於(==),在 Angular 的原始碼找到可以複寫判斷句的地方。

app.module.ts

import { RadioControlValueAccessor, SelectControlValueAccessor } from '@angular/forms';

/* 將 SelectControlValueAccessor 的全等於改成一般等於 */
SelectControlValueAccessor.prototype.compareWith = function (a, b) {
    return a == b;
};

/* 將 RadioControlValueAccessor 的全等於改成一般等於 */
RadioControlValueAccessor.prototype.writeValue = function (value: any) {
    let self: any = this;
    self._state = value == this.value;
    self._renderer.setProperty(self._elementRef.nativeElement, 'checked', self._state);
};
2023-03-07 16:55

[ThreeJS] 單次動畫

/* 單次動畫 */
function animateOnce(mixer, prop, dur, values) {
    /* 停止 & 清除 先前的動畫 */
    mixer.stopAllAction();
    mixer.uncacheRoot(mixer.getRoot());

    /* 增加動畫片段 */
    const track = new THREE.KeyframeTrack(prop, [0, dur], values);
    const clip = new THREE.AnimationClip('move', dur, [track]);
    const action = mixer.clipAction(clip);
    action.clampWhenFinished = true;
    action.setLoop(THREE.LoopOnce);

    /* Promise 來處理完成事件,這樣就可以用 await */
    return new Promise((resolve) => {
        const finished = function () {
            mixer.removeEventListener('finished', finished);
            resolve();
        };
        mixer.addEventListener('finished', finished);

        action.play(); /* 播放動畫 */
    });
}

想要能像 SVG 一樣簡單的控制模型的移動,ThreeJS 並沒有直接提供我想要的模式,試了很久終於成功了。

mixer 中的 action 是不斷添加的,這會混亂播放動畫,所以在播放新的動畫前必須[停止]且[清除]先前的動畫。

為了可以簡單串接不同模型的動畫,所以就想用 await 來達成,所以加上 Promise 的包裝。

完整程式

/* 初始化渲染器 */
const renderer = new THREE.WebGLRenderer({ antialias: true });
document.getElementById('Container').appendChild(renderer.domElement);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);

/* 初始化場景 */
const scene = new THREE.Scene();
scene.background = new THREE.Color('#000'); /* 背景顏色 */
scene.add(new THREE.AmbientLight('#FFF', 0.5)); /* 加入環境光 */
scene.add(new THREE.AxesHelper(50)); /* 3D 軸標示 */

/* 初始化鏡頭 */
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 300);
camera.position.set(0, 4, 12);

/* 初始化軌道控制,鏡頭的移動 */
const orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
orbitControls.update();


/* 動畫刷新器 */
const mixers = [];

function newMixer(target) {
    const mixer = new THREE.AnimationMixer(target);
    mixers.push(mixer);
    return mixer;
}

/* 動畫計時器 */
const clock = new THREE.Clock();

/* 渲染週期 */
function renderCycle() {
    const delta = clock.getDelta();
    mixers.forEach(x => x.update(delta));

    renderer.render(scene, camera);
    requestAnimationFrame(renderCycle);
}
renderCycle();



/*---------------------------------------------------------------*/

/* 單次動畫 */
function animateOnce(mixer, prop, dur, values) {
    /* 停止 & 清除 先前的動畫 */
    mixer.stopAllAction();
    mixer.uncacheRoot(mixer.getRoot());

    /* 增加動畫片段 */
    const track = new THREE.KeyframeTrack(prop, [0, dur], values);
    const clip = new THREE.AnimationClip('move', dur, [track]);
    const action = mixer.clipAction(clip);
    action.clampWhenFinished = true;
    action.setLoop(THREE.LoopOnce);

    /* Promise 來處理完成事件,這樣就可以用 await */
    return new Promise((resolve) => {
        const finished = function () {
            mixer.removeEventListener('finished', finished);
            resolve();
        };
        mixer.addEventListener('finished', finished);

        action.play(); /* 播放動畫 */
    });
}


const cubeA = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshBasicMaterial( {color: '#0F0'} )
);
const mixerA = newMixer(cubeA);
scene.add(cubeA);


const cubeB = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshBasicMaterial( {color: '#00F'} )
);
const mixerB = newMixer(cubeB);
scene.add(cubeB);


async function run() {
    await animateOnce(mixerA, '.position[x]', 3, [0, 3]);
    await animateOnce(mixerB, '.position[y]', 3, [0, 2]);
    await animateOnce(mixerA, '.position[x]', 3, [3, 0]);
    await animateOnce(mixerB, '.position[y]', 3, [2, 0]);
}

run();
2023-03-07 16:06

[ThreeJS] 用 SVG 貼圖顯示中文

HTML

<div id="Container"></div>

<div id="LabelTpl" style="display:none;">
    <svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 30 30">
        <text x="15" y="15" fill="#fff" text-anchor="middle"></text>
    </svg>
</div>

<script src="../Scripts/three.145/three.min.js"></script>
<script src="../Scripts/three.145/controls/OrbitControls.js"></script>
<script src="../Scripts/three.145/loaders/GLTFLoader.js"></script>
<script src="main.js"></script>

main.js

/* 初始化渲染器 */
const renderer = new THREE.WebGLRenderer({ antialias: true });
document.getElementById('Container').appendChild(renderer.domElement);

renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);


/* 初始化場景 */
const scene = new THREE.Scene();
scene.background = new THREE.Color('#000'); /* 背景顏色 */
scene.add(new THREE.AmbientLight('#FFF', 0.5)); /* 加入環境光 */
scene.add(new THREE.AxesHelper(50)); /* 3D 軸標示 */

/* 初始化鏡頭 */
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 300);
camera.position.set(0, 4, 12);


/* 初始化軌道控制,鏡頭的移動 */
const orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
orbitControls.update();


/* 渲染週期 */
function renderCycle() {
    renderer.render(scene, camera);
    requestAnimationFrame(renderCycle);
}
renderCycle();



/*---------------------------------------------------------------*/

function svgBase64(svg) {
    svg = svg.replace(/\s+</g, '<').replace(/>\s+/g, '>');
    return 'data:image/svg+xml,' + encodeURIComponent(svg);
}


const textureLoader = new THREE.TextureLoader();
const $labelTpl = document.querySelector('#LabelTpl');
const $labelText = document.querySelector('#LabelTpl text');


/* 環線 */
function addCircle(label,  z) {
    $labelText.innerHTML = label;
    let imageData = svgBase64($labelTpl.innerHTML);

    const particles = new THREE.Points(
        new THREE.EdgesGeometry(new THREE.CircleGeometry(10, 6)),
        new THREE.PointsMaterial({
            map: textureLoader.load(imageData),
            color: '#FFF',
            size: 4,
            depthWrite: false,
            transparent: true,
        })
    );
    particles.position.z = z;
    scene.add(particles);


    const circle = new THREE.LineSegments(
        new THREE.EdgesGeometry(new THREE.CircleGeometry(10, 60)),
        new THREE.LineBasicMaterial({ color: '#FFF' })
    );
    circle.position.z = z;
    scene.add(circle);
}

addCircle('冬至', 4);
addCircle('夏至', -4);
2023-03-07 13:01

[ThreeJS] 解決陰影造成的條紋

 會出現條紋現象是因為雙面材質,只要設置為單面材質就可以解決。

/* 載入模型 */
new THREE.GLTFLoader().load('Model.glb', function (gltf) {
    const model = gltf.scene;

    model.traverse(obj => {
        if (!obj.isMesh) { return; }

        obj.frustumCulled = false;
        obj.castShadow = true;
        obj.receiveShadow = true;

        /* 解決陰影造成的條紋 */
        obj.material.side = THREE.FrontSide;
        obj.material.shadowSide = THREE.FrontSide;
    });

    scene.add(model);
});
2023-03-07 12:49

[ThreeJS] DragControls 拖移出現異常的原因

 需要進行拖移的模型其[原點]最好設置在[幾何中心]

因為拖移出現異常,我為了解決這個問題,認真看 DragControls 的原始碼,所有處理拖移的邏輯都相當周全,基本上不應該出現異常現象。

在用 console.log 查看拖移的座標數值時,才發現原來是模型的[原點]設置在[場景中心]造成的。

2023-03-07 11:37

[ThreeJS] 載入壓縮過 GLTF 檔

/* 解壓縮 lib 載入器
 * https://threejs.org/docs/#examples/en/loaders/DRACOLoader
 * https://google.github.io/draco/
 */
const dracoLoader = new THREE.DRACOLoader();

/* 指定解壓器的位置 */
dracoLoader.setDecoderPath('../Scripts/three.145/libs/draco/');


/* GLTF 載入器 */
const gltfLoader = new THREE.GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);

/* 載入模型 */
gltfLoader.load('Model.glb', function (gltf) {
    const model = gltf.scene;
    scene.add(model);
});
2023-03-03 15:08

[ThreeJS] 材質發黑是因為金屬屬性

剛開始接觸 ThreeJS 時,被材質發黑這個問題,困擾很久,一直沒找到問題點,不管怎麼增加 HemisphereLight 都無效,用了四個方向 DirectionalLight 才呈現能接受的效果。

後來才發現問題點是金屬屬性 (metalness),因為金屬有鏡面效果會映像出周圍的景象,然後我又沒有設置 environment map,整個場景 (scene) 都是黑漆漆的,所以材質映像出來也會是黑色的。





2023-02-21 10:33

[Python] Flask Log 配置

import os
import logging
import logging.handlers

from flask import Flask, g, request, json

app = Flask(__name__)


#[ Log 配置 ]#############################################################
 # 用來記錄無法處理的錯誤 (PS: 使用 WSGI 會依附 Apache 的設定,可以不用配置)

# https://docs.python.org/zh-cn/3/library/logging.html
formatter = logging.Formatter("%(asctime)s [%(levelname)s]  %(message)s")

# https://docs.python.org/zh-tw/3/library/logging.handlers.html#timedrotatingfilehandler
handler = logging.handlers.TimedRotatingFileHandler("log/web-api",
    when = "D",
    interval = 1,
    backupCount = 7,
    encoding = "UTF-8",
    delay = False,
    utc = True)
handler.setFormatter(formatter)

app.logger.addHandler(handler)
2023-02-21 10:13

[Python] Flask 自訂日期的 Json 轉換

import os
import datetime
import time

from flask import Flask, g, request, json
from flask.json import JSONEncoder


class CustomJsonEncoder(JSONEncoder):
    # 針對日期自訂 Json 轉換

    def default(self, obj):
        if isinstance(obj, datetime.date):
            return obj.isoformat().replace('T', ' ')

        return super().default(obj)


app = Flask(__name__)

app.json_encoder = CustomJsonEncoder
app.config['JSON_AS_ASCII'] = False  # 返回結果可以正確顯示中文
2023-02-21 10:05

[Python] Flask MySQL 連線管理

import os
import mysql.connector as sql

from werkzeug.exceptions import HTTPException, BadRequest
from flask import Flask, g, request, json


app = Flask(__name__)



#[ DB 處裡 ]#############################################################

# https://docsxyz.com/zh-hant/wiki/python/connector-python-connectargs
db_config = {
    'host'      : "localhost",
    'user'      : "XXXX",
    'passwd'    : "XXXX",
    'db'        : 'XXXX',
    'use_pure'  : True,
    'autocommit': True,
    'charset'   : 'utf8',
}


@app.before_request
def before_request():
    # 在 request 前開啟 DB 連線
    # g 是 Flask global 在每個 request 有獨立的 context

    g.cnt = sql.connect(**db_config)
    g.cursor = g.cnt.cursor(dictionary = True)



@app.after_request
def after_request(response):
    # 在 request 後結束 DB 連線

    cursor = g.get('cursor', None)
    if cursor is not None:
        # 當 cursor 還有 row 沒有取出,close 會發生錯誤
        if cursor.with_rows : cursor.fetchall()
        cursor.close()

    cnt = g.get('cnt', None)
    if cnt is not None:
        cnt.close()

    return response
2023-02-21 09:55

[Python] Flask 筆記

相依套件:
-------------------------------------------------------------------------------
python-3.8.10-amd64.exe

pip install Flask
pip install flask_cors
pip install mysql-connector-python
pip install pycryptodomex
pip install py-linq     # https://viralogic.github.io/py-enumerable/


Apache CGI 配置,用虛擬 Script 指向到 app.cgi
-------------------------------------------------------------------------------


ScriptAlias /web-api  D:/iog-project/web-api/app.cgi

<Directory "D:/iog-project/web-api/">
Options ExecCGI 
AllowOverride all
Require local
</Directory>


-------------------------------------------------------------------------------

# 正常執行
flask run

# 除錯執行  (flask 會提供除錯功能,並將 logger 從 warning 提升到 debug 
export FLASK_ENV=development    # for linux / git  bash 
set FLASK_ENV=development       # for windows cmd
flask run

# 列出設定的路徑
flask routes


其他
-------------------------------------------------------------------------------

因為 web-api 的格式是 Json,所以遵照 JS 的命名風格,欄位名稱開頭小寫第二個字大寫

2023-02-21 09:53

[Python] Flask 錯誤處裡


from werkzeug.exceptions import HTTPException, BadRequest
from flask import Flask, g, request, json

app = Flask(__name__)


#[ 錯誤處裡 ]#############################################################
@app.errorhandler(Exception)
def handle_exception(e):
    if isinstance(e, HTTPException): return e  # 讓 HTTPException 交由下一個處理

    app.logger.exception("Internal Server Error.")  # log 錯誤訊息
    if app.debug : return e

    return json.jsonify({'message': 'Internal Server Error.'}), 500


@app.errorhandler(HTTPException)
def handle_exception(e):
    response = e.get_response()
    return json.jsonify({'message': e.description}), e.code
2022-09-08 13:39

架構解釋

以前在專案開發時對分層架構所定下的原則,去避免不必要的地雷,以及每一層的要處裡的職權。

我會考慮分層架構,是希望提高系統的嚴謹度,以及 Method 的共用性,所以 Dao 層級就要避免功能導向,不然就是把單頁的程式拆分到多個層去而已。

 

DomainModel / ViewModel

  • 這三類都屬於 POCO 類型的物件,單純的資料載體,不允許有動作的 method,不可以外部取資料
  • 相同資料的欄位命名必須一致,反例: ProjectId, Pid, pid, PID, p_id
  • 有欄位就要有資料,明明有欄位定義卻不從 DB 取資料,這會造成不必要的雷

 

DomainModel

  • 欄位定義可與 DB 一致
  • 將 DB 的資料進行彙整,成為完整的資料體

 

ViewModel

欄位定義與 View 的表單(Form)一致,與 DomainModel 可能很相似,但有些欄位只會用在表單上,所以 DomainModel 不適合用在表單 Binding 上

例如:

  • 同意上述條款
  • 舊密碼, 新密碼, 確認密碼

 

Dao

  • 回傳 DomainModel
  • 定義單純的 BD 操作 List, GetById, Save, Insert, Update
  • 與 ORM 不同,是進一步將資料操作簡化
  • 保證 Method 的通用性,避免功能導向,例如: 有一個 Method 是專門為了A畫面功能而存在的

 

Service

  • 回傳 DomainModel
  • 驗證商業邏輯,如必要資料欄位,數值範圍等...
  • 調用一個或多個 Dao 來完成商業邏輯
  • 處理 DB 交易,來協調多個 Dao 的調用

 

Controller

  • 處理 DomainModel 到 ViewModel 的轉換
  • 調用 Service 進行 DomainModel 的資料處理
  • 調配 Responses 的結果 Html, Json, PDF ...
  • 驗證資料類型的正確,數值﹑日期
  • 負責 Access 的權限阻擋

 

View

  • View 是被動的,不能存取任何 Controller / Service / Dao
  • 負責資料呈現及格式化,如 日期﹑金額 ...
  • Ajax 只能對 Controller 調用

 

附註

  1. 同一層之間不可以互相參考,這容易發生循環參考,例如:
    • Controller 呼叫 Controller
    • Service 呼叫 Service
    • Dao 呼叫 Dao
  2. 同一層之間有相同邏輯可以抽離到 Support 類
     
  3. 建議定義命名規範,讓每一個人寫出來的程式像是同一個人寫的,好處是降低支援或接手的人的困難度
     
  4. 任何第三方的 API 都需要包裝,隔離直接相依的問題,而且可以單獨測試
     

 

架構關係全貌

 

Model 傳遞的關係

 

用 Interface 隔離實作

 

Web Api 的呼叫關係

 

2022-09-05 11:29

[轉載] opencv js getPerspectiveTransform,perspectiveTransform 方法使用

轉載自: opencvjs getPerspectiveTransform,perspectiveTransform方法使用

OpenCV JavaScript版本,使用getPerspectiveTransform,PerspectiveTransform方法。

JavaScript和python版本不同的是Mat的创建方法不同,python会在内部自动把数据转换成Mat类,也JavaScript不会,所以刚开始没有找到JavaScript创建Mat方法,走了很多弯路。

这只是测试代码,没有使用项目中真实的数据,所有有一定的偏差。

如果有什么错误,欢迎纠正。

下面上代码:

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>Hello OpenCV.js</title>
</head>

<body>
  <h2>Hello OpenCV.js</h2>
  <h2>getPerspectiveTransform,PerspectiveTransform方法使用</h2>
  <p id="status">OpenCV.js is loading...</p>
  <div>
    <button onClick="myclick()">输出结果</button>
  </div>
  <script type="text/javascript">
    function myclick() {
      //代码 getPerspectiveTransform
      //创建数据
      let srcTri = cv.matFromArray(4, 1, cv.CV_32FC2, [56, 65, 368, 52, 28, 387, 389, 390]);
      let dstTri = cv.matFromArray(4, 1, cv.CV_32FC2, [0, 0, 300, 0, 0, 300, 300, 300]);
      //转换的数据
      let M = cv.getPerspectiveTransform(srcTri, dstTri);
      console.log("getPerspectiveTransform M", M);

      //==== PerspectiveTransform ======
      //point点的数据一定要是一维的,opencv会自己去处理
      let points = [
        1, 2,
        3, 4,
        5, 6,
        7, 8
      ];
      //原数据
      points = cv.matFromArray(4,1,cv.CV_32FC2,points);
      //转换后的数据
      let points_trans = new cv.Mat();
      cv.perspectiveTransform(points, points_trans,M);
      console.log("points", points);
      console.log("points_trans", points_trans);

    }
    function onOpenCvReady() {
      document.getElementById('status').innerHTML = 'OpenCV.js is ready.';

    }
  </script>
  <script async src="https://docs.opencv.org/4.x/opencv.js" onload="onOpenCvReady();" type="text/javascript"></script>
  <script src="https://docs.opencv.org/4.x/utils.js" type="text/javascript"></script>
</body>

</html>

getPerspectiveTransform结果

perspectiveTransform结果

2022-08-01 11:37

[轉載] NLog Variables

轉載自:當條件等于不作業時.net核心Nlog過濾器

我必須從我的啟動類傳遞變數值

LogManager.Configuration.Variables["environment"] = "Development";

我在我的 nlog.config 檔案中添加了以下過濾器

<rules>
    <logger name="*" minlevel="Error" writeTo="logfile">
        <filters>
            <when condition="equals('${var:environment}', 'Development')" action="Ignore" />
        </filters>
    </logger>
</rules>

即使我將值作為 Development 傳遞,該訊息仍會被記錄而不是忽略。

但是,當我對它的作業值進行硬編碼時

您在 NLog中發現了一個錯誤,但如果您這樣做,它應該可以作業(也會更快):

<rules>
    <logger name="*" minlevel="Error" writeTo="logfile">
        <filters defaultAction='log'>
            <when condition="'${var:environment}' == 'Development'" action="Ignore" />
        </filters>
    </logger>
</rules>

您也可以用 minLevel="${var:EnvironmentMinLevel:whenEmpty=Error}" 處理,這比 <filters> 快得多

<rules>
    <logger name="*" minlevel="${var:EnvironmentMinLevel:whenEmpty=Error}" writeTo="logfile" />
</rules>

設定 Variables 要記得呼叫 Reconfig,或者在 config 中設定 autoReload="true"

NLog.LogManager.Configuration.Variables["EnvironmentMinLevel"] = "Off";
NLog.LogManager.ReconfigExistingLoggers();

另請參閱 https://github.com/NLog/NLog/wiki/Filtering-log-messages#semi-dynamic-routing-rules

2022-07-15 12:52

[轉載] X-Y PROBLEM

轉載自:酷壳 X-Y PROBLEM

对于X-Y Problem的意思如下:

  1. 有人想解决问题X
  2. 他觉得Y可能是解决X问题的方法
  3. 但是他不知道Y应该怎么做
  4. 于是他去问别人Y应该怎么做?

简而言之,没有去问怎么解决问题X,而是去问解决方案Y应该怎么去实现和操作。于是乎:

  1. 热心的人们帮助并告诉这个人Y应该怎么搞,但是大家都觉得Y这个方案有点怪异。
  2. 在经过大量地讨论和浪费了大量的时间后,热心的人终于明白了原始的问题X是怎么一回事。
  3. 于是大家都发现,Y根本就不是用来解决X的合适的方案。

X-Y Problem最大的严重的问题就是:在一个根本错误的方向上浪费他人大量的时间和精力

示例

举个两个例子:

Q) 我怎么用Shell取得一个字符串的后3位字符?
A1) 如果这个字符的变量是$foo,你可以这样来 echo ${foo:-3}
A2) 为什么你要取后3位?你想干什么?
Q) 其实我就想取文件的扩展名
A1) 我靠,原来你要干这事,那我的方法不对,文件的扩展名并不保证一定有3位啊。
A1) 如果你的文件必然有扩展名的话,你可以这来样来:echo ${foo##*.}

再来一个示例:

Q)问一下大家,我如何得到一个文件的大小
A1) size = ls -l $file | awk '{print $5}'
Q) 哦,要是这个文件名是个目录呢?
A2) 用du吧
A3) 不好意思,你到底是要文件的大小还是目录的大小?你到底要干什么?
Q)  我想把一个目录下的每个文件的每个块(第一个块有512个字节)拿出来做md5,并且计算他们的大小 ……
A1) 哦,你可以使用dd吧。
A2) dd不行吧。
A3) 你用md5来计算这些块的目的是什么?你究竟想干什么啊?
Q) 其实,我想写一个网盘,对于小文件就直接传输了,对于大文件我想分块做增量同步。
A2) 用rsync啊,你妹!

这里有篇文章说明了X-Y Problem的各种案例说明,我从其中摘出三个来让大家看看:

你试图做X,并想到了用Y方案。所以你去问别人Y,但根本不提X。于是,你可以会错过本来可能有更好更适合的方案,除非你告诉大家X是什么。

— from Re: How do I keep the command line from eating the backslashes? by revdiablo

有些人问怎么做Y,但其它他想做的是X。他问怎么做Y是因为他觉得Y是最好搞定X的方法。 于是大家不断地回答“试试这个,试试那个”来帮助他,而他总是在说“这个有问题,那个有问题,因为……”。基本不同的情况,其它的方案可能会更好。

— from Re: Re: Re: Re: regex to validate e-mail addresses and phone numbers by Limbic~Region

X-Y Problem又叫“过早下结论”:提问者其实并不非常清楚想要解决的X问题,他猜测用Y可以搞定,于是他问大家如何实现Y。

— from <Pine.GHP.4.21.0009061210570.8800-100000@hpplus03.cern.ch> by Alan J. Flavell

其实这个问题在我之前的《你会问问题吗》里提到的那篇How To Ask Questions the Smart Way中的提到过,你可以移步去看一下

所以,我们在寻求别人帮助的时候,最好把我们想解决的问题和整个事情的来龙去脉说清楚。

一些变种

我们不要以为X-Y Problem就像上面那样的简单,我们不会出现,其实我们生活的这个世界有有各种X-Y Problem的变种。下面我个人觉得非常像XY Problem的总是:

  • 其一、大多数人有时候,非常容易把手段当目的,他们会用自己所喜欢的技术和方法来反推用户的需求,于是很有可能就会出现X-Y Problem – 也许解决用户需求最适合的技术方案是PC,但是我们要让他们用手机。
  • 其二、产品经理有时候并不清楚他想解决的用户需求是什么,于是他觉得可能开发Y的功能能够满足用户,于是他提出了Y的需求让技术人员去做,但那根本不是解决X问题的最佳方案。
  • 其三、因为公司或部门的一些战略安排,业务部门设计了相关的业务规划,然后这些业务规划更多的是公司想要的Y,而不是解决用户的X问题。
  • 其四、对于个人的职业发展,X是成长为有更强的技能和能力,这个可以拥有比别人更强的竞争力,从而可以有更好的报酬,但确走向了Y:全身心地追逐KPI。
  • 其五、本来我们想达成的X是做出更好和更有价值的产品,但最终走到了Y:通过各种手段提升安装量,点击量,在线量,用户量来衡量。
  • 其六、很多团队Leader都喜欢制造信息不平等,并不告诉团队某个事情的来由,掩盖X,而直接把要做的Y告诉团队,导致团队并不真正地理解,而产生了很多时间和经历的浪费。

所有的这些,在我心中都是X-Y Problem的变种,这是不是一种刻舟求剑的表现?

参考