2021年8月25日 星期三

X window 簡單的繪圖及貼圖程式

 主程式 main.cpp:

#include <opencv2/opencv.hpp> // should include first to prevent conflic
#include <opencv2/highgui.hpp>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xresource.h>
#include <X11/cursorfont.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
using namespace std;
typedef unsigned char u8int;
typedef const char*   c_str;
Display *display;
Window     child;
GC        gCtx;      
XColor     fgColor, black, white ,red, green, blue;
XImage  *bg = nullptr;
int width   = 0;
int height  = 0;
void drawRect(int x, int y, int w, int h){
    XDrawRectangle(display, child, gCtx, x, y, w, h);
}
void drawLine(int x1, int y1, int x2, int y2){
    XDrawLine(display, child, gCtx, x1, y1, x2, y2);
}
void drawArc(int x, int y, int w, int h, int ang1, int ang2){
    XDrawArc(display, child, gCtx, x, y, w, h, ang1 << 6, ang2 << 6);
}
void setColorMap(XColor &c, const char *str){
    auto colormap = DefaultColormap(display, 0);
    XParseColor(display, colormap, str, &c);
    XAllocColor(display, colormap, &c);
}
void fgSetColor(XColor &c) {
    fgColor = c;
    XSetForeground(display, gCtx, c.pixel);
}
void setBackGround(u8int *src=nullptr, int sw=0, int sh=0, int sch=0){
    if (bg) XDestroyImage(bg);
    XWindowAttributes info;
    XGetWindowAttributes(display, child, &info);
    Visual *v = info.visual;
    width  = info.width ;
    height = info.height;
    int depth = info.depth;
    int sline = sch * sw;
    int dline= 4 * width;
    int size = dline * height;
    u8int *fb = (u8int *)malloc(size);
    if(! fb) {
        bg = nullptr;
        return;
    }
    bg = XCreateImage(display, v, depth,
        ZPixmap, 0, (char *)fb, width, height, 32, 0
    );
    if(! bg) {
        free(fb);
        return;
    }
    if(! src) {
        memset(fb, 0, size);
        return;
    }
    u8int *rgb = src;
    int zch = sch > 4 ? 4 : sch;
    float xs = (float)sw / width;
    float ys = (float)sh / height;
    float fy = 0;
    for(int y = 0; y < height; y ++) {
        float fx = 0;
        int d = 0;
        int s = 0;
        for (int x = 0; x < width; x ++) {
            fb[d] = rgb[s];
            for(int z = 1; z < zch; z ++) fb[d + z] = rgb[s + z];
            fx += xs;
            d += 4;
            s = sch * (int)fx;
        }
        fy += ys;
        fb += dline;
        rgb = src + sline * (int)fy;
    }
    XPutImage(display, child, gCtx, bg, 0, 0, 0, 0, width, height);
}
void setBackGround(cv::Mat &cam){
    return setBackGround((u8int *)cam.data,
        cam.cols,
        cam.rows,
        cam.channels()
    );
}
void bgRestore(int x0, int y0, int sw = 0, int sh = 0) {
    if (x0 < width && y0 < height){
        if (x0 < 0) x0 = 0;
        if (y0 < 0) y0 = 0;
        if (x0 + sw >=  width) sw = width  - x0;
        if (y0 + sh >= height) sh = height - y0;
        if (sw > 0 && sh > 0) XPutImage(display, child, gCtx,
            bg, x0, y0,
            x0, y0, sw, sh
        );
    }
}
int main(int argc, char **argv){    
    if(argc < 2) {
        printf("no file\n");
        return 1;
    }
    cv::Mat jpeg = cv::imread(argv[1]);// 8UC3
    display = XOpenDisplay(getenv("DISPLAY"));
    if (! display)  {
        printf("X error\n");
        return 3;
    }
    child = XCreateSimpleWindow(display,
        XDefaultRootWindow(display),
        0, 0, 600, 600,
        0,// border width
        WhitePixel(display, 0),
        BlackPixel(display, 0)
    );
    XSetStandardProperties(display, child, "MyXapp",
        "Icon", None, nullptr, 0, nullptr
    );
    gCtx = XCreateGC(display, child, 0, 0);
    setColorMap(black , "#000000");
    setColorMap(white , "#FFFFFF");
    setColorMap(red   , "#FF0000");
    setColorMap(green , "#00FF00");
    setColorMap(blue  , "#0000FF");  
    unsigned char shape[8] = {0xff, 0xff,0xff, 0xff, 0xff, 0xff,0xff, 0xff};
    unsigned char mask[8]  = {0xff, 0xff,0xff, 0, 0, 0xff,0xff, 0xff};// 1: show, 0: hidden
    Pixmap shapeid = XCreateBitmapFromData(display, child, (char *)shape, 8, 8);
    Pixmap maskid  = XCreateBitmapFromData(display, child, (char *)mask, 8, 8);
    Cursor cursorID = XCreatePixmapCursor(display, shapeid, maskid, &green, &black, 0, 0);
    XDefineCursor(display, child, cursorID);
    XFreePixmap(display, shapeid);
    XFreePixmap(display, maskid);
    XFreeCursor(display, cursorID);
    XSelectInput(display, child, ExposureMask | ButtonPressMask);
    XMapWindow(display, child);
    XFlush(display);
    fgSetColor(red);
    XEvent evt;
    
    printf("press mouse right button to exit.\n");
    while(true) {
        XNextEvent(display, &evt);
        if (evt.type == Expose) {
            if(evt.xexpose.count > 0) continue; // until count = 0
            setBackGround(jpeg);
            int right = width  - 1;
            int down  = height - 1;
            int cx    = width  / 2;
            int cy    = height / 2;
            drawLine(0, cy, right, cy);
            drawLine(cx, 0, cx, down);
            drawArc(0, 0, right, down, 0, 360);
            drawRect(0, 0, right, down);
            bgRestore(cx - 100, cy - 100, 200, 200);
        }        
        if (evt.type == ButtonPress)  {
            if(evt.xbutton.button == 3) {
                printf("press right\n");
                break;
            }
        }
    }
    if (bg) XDestroyImage(bg);
    XFreeGC(display, gCtx);
    XDestroyWindow(display, child);
    XCloseDisplay(display);    
}

事先準備好一個 test.jpeg, 編譯程式並執行:

g++   main.cpp  -lX11  `pkg-config  --cflags  --libs  opencv4`  && ./a.out  test.jpeg

2021年8月20日 星期五

linux 上使用 opencv 抓取螢幕影像

 不囉唆, 直接看原始碼 x1.cpp:
#include <opencv2/opencv.hpp>
#include <X11/Xutil.h>
#include <stdio.h>
using namespace std;
int main(int argc, char ** argv){
    XWindowAttributes x = { 0 };// x window 屬性(參數群)
    auto display = XOpenDisplay(nullptr);   // 打開 x window server 的顯示器
    auto root = DefaultRootWindow(display); // get default root?
    XGetWindowAttributes(display, root, &x);// 取得螢幕參數
    int height = x.height;
    int width  = x.width;
    auto buffer = XGetImage(display, root,
        0, 0, width, height,
        AllPlanes, ZPixmap
    );// 全螢幕影像

    int cvType = CV_8UC4;// default 4 bytes's A,R,G,B
    switch (buffer->bits_per_pixel) {
        case 24: cvType = CV_8UC3; break;// 3 bytes
        case 16: cvType = CV_8UC2; break;// 2 bytes
        case  8: cvType = CV_8UC1; break;// 1 bytes
    }
    const char *name = "ScreenCapture";
    cv::namedWindow(name, cv::WINDOW_AUTOSIZE);
    cv::Mat m(height, width, cvType, buffer->data);  // 暫時包成 cv::Mat
    cv::imshow(name, m); // 顯示全螢幕影像

    cv::resize(m, m, cv::Size(), 0.25, 0.25, cv::INTER_LINEAR);// 長寬各縮 1/4
    XDestroyImage(buffer) ;// 釋放 buffer
    XCloseDisplay(display);// 關閉 x window session

    cv::imshow("1/16 Screen Shot", m); // 顯示 1/16 螢幕影像
    cv::waitKey(0);
}
用 g++ 編譯並執行程式:
     g++  x1.cpp  -lX11 `pkg-config  --cflags  --libs  opencv4`  &&  ./a.out

2021年8月12日 星期四

linux 上是使用 kolinc-jvm 編譯 kotlin 程式, 程式透過 jni 呼叫原生 c 函式

 1. 上官網 https://github.com/JetBrains/kotlin/releases/download/v1.5.21/kotlin-compiler-1.5.21.zip下載 kotlin 編譯器, 開啟終端機進入下載目錄, 將它解壓縮到目錄 kotlinc 內

       cd  Downloads  &&  unzip  kotlin-compiler-1.5.21.zip

2.  安裝 openjdk 11, 將程式安裝到 /usr/lib/jvm/java-11-openjdk-amd64

       sudo  apt-get  install  openjdk-11-jdk

3. 建一個專案 project 目錄(mkdir project), 在裏面編寫一個 Makefile 方便編譯及跑程式:
buildDir  = build
main_jar  = $(buildDir)/main.jar
jniLib_jar= $(buildDir)/jniLib.jar
libjni_so = $(buildDir)/libnative.so

jdk11     = /usr/lib/jvm/java-11-openjdk-amd64/include
openjdk   = -I$(jdk11)  -I$(jdk11)/linux
opencv2   = `pkg-config  --cflags  --libs  opencv4`

kotlinDir =  /home/mint/Downloads/kotlinc
ktc       = $(kotlinDir)/bin/kotlinc  -include-runtime
coroutine = -cp $(kotlinDir)/lib/kotlinx-coroutines-core.jar

javaLib   = $(coroutine):$(jniLib_jar)
cppLib    = -shared  -fPIC  $(openjdk)  $(opencv2)

run: main  native
    java  -Djava.library.path=$(buildDir)  $(javaLib):$(main_jar)  MainKt

main:  $(buildDir)  $(main_jar)

native: $(libjni_so)

$(main_jar): main.kt  $(jniLib_jar)
    $(ktc) -d  $@  $<  $(javaLib)

$(jniLib_jar): jniLib.kt
    $(ktc) -d  $@  $<  $(coroutine)

$(libjni_so): cpp/native.cpp
    g++  -o  $@  $<  $(cppLib)

$(buildDir):
    [ -d $@ ] || mkdir $@ && echo $@ exist already

clean:
    [ -d $(buildDir) ] || echo $(buildDir) not exist && rm -rf $(buildDir)

4. 在專案目錄內寫一個 kotlin 主程式碼 main.kt:

import  kotlinx.coroutines.*
import  jniLib.*
fun  main( ) {
  runBlocking{
     GlobalScope.launch(Dispatchers.IO) { // split nonblocking coroutine
          println("coroutine hello1")
     }
     NativeClass( ).hello( ) // call  jni  c  routine
  }
}

5.  在專案目錄內寫一個 kotlin 介面程式庫 jniLib.kt:

package jniLib
class NativeClass {  
  external fun hello( ) // external  function written in c++ language
  init {
      System.loadLibrary("native")
  }
}

6. 在專案目錄內再建個 cpp 子目錄(mkdir  cpp), 在 cpp 目錄內編寫一個用 c 寫的 jni  程式碼 cpp/native.cpp:

#include <stdio.h>
#include <jni.h>
#include <opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>
const char *window = "mouseEvent";
void mouseEvent(int event, int x, int y, int flags, void *args){
    static cv::Scalar green(0,   255,   0);
    static cv::Scalar red(0,   0,   255);
    cv::Mat &image = *(cv::Mat *)args;
    switch(event) {
        case cv::EVENT_LBUTTONDOWN: // left mouse button click
                printf("left   click row: %d, col: %d\n", x, y);
                cv::circle(image, cv::Point(x, y), 2, red);
                cv::imshow(window, image);
                break;
        case cv::EVENT_RBUTTONDOWN:// right mouse button click
                printf("right  click row: %d, col: %d\n", x, y);
                cv::line(image, cv::Point(0, 0), cv::Point(x, y), green, 2, cv::FILLED);
                cv::imshow(window, image);
                break;
        case cv::EVENT_MBUTTONDOWN:// middle mouse button click
                printf("middle click row: %d, col: %d\n", x, y);
                break;
        case cv::EVENT_MOUSEMOVE:// mouse movment
                // printf("mouse move  row: %d, col: %d\n", x, y);
                break;
    }
}
extern "C" {
    JNIEXPORT void JNICALL Java_jniLib_NativeClass_hello(JNIEnv *env, jobject obj) {        
        cv::Mat fg(800, 800, CV_8UC3);// w x h
        cv::namedWindow(window, cv::WINDOW_NORMAL);
        cv::setMouseCallback(window, mouseEvent, &fg);
        cv::imshow(window, fg);
        cv::waitKey(0);//press any key to exit
    }
}

上述步驟 3,4,5,6 存檔後, 在專案目錄內開啟終端機執行 make run 跑看看, 不用 Gradle, 速度快多了

後記: 使用 cmake 語法, 編譯 c++ 動態程式庫 libnative.so:

1. 先在專案目錄內, 編輯一個 CMakeLists.txt:
cmake_minimum_required(VERSION 3.10.2)
project(native)
include_directories(
    /usr/lib/jvm/java-11-openjdk-amd64/include
    /usr/lib/jvm/java-11-openjdk-amd64/include/linux
    /usr/include/opencv4
)
set(opencvLib   -lopencv_core   -lopencv_highgui   -lopencv_imgproc)
add_library(native   SHARED   cpp/native.cpp)
target_link_libraries(native   PUBLIC   ${opencvLib})

2. 建一個子目錄 build, 在裏面執行 cmake .. 產生 Makefile, 接著 make 就能生成 libnative.so:
    [ -d build ] || mkdir   build
    cd   build   &&   cmake  ..  &&  make

2021年8月1日 星期日

關於 UTF8 與 Unicode

 UTF8 編碼是 1 ~ 4 bytes 字元編碼, Unicode 則是固定 4 bytes 字元編碼:

#include "stdio.h"
typedef unsigned char u8int;
typedef const char*   c_str;

void toHex(u8int *utf8) {
        if (utf8) {
            int wc = 0;// wide char unicode
            while (*utf8 != 0) {
                if ((*utf8 & 0xc0) == 0x80) { // 10 xx xxxx, bits accumulate
                    wc <<= 6;// self-shift 6 bits to left
                    wc |= *utf8 & 0x3f;// accumulate 6 bits
                } else {// ready to decode
                    if (wc > 0x80) printf("%5x, ", wc); // previous one

                    if (*utf8 < 0x80) { // 0x xx xxxx, 1 bytes utf8
                        printf("%2x, ", *utf8);// decode immediatelly
                        wc = 0; // reset to 0 to prevent overflow
                    } // decode later
                    else if (*utf8 < 0xe0) wc = *utf8 & 0x1f;// 2 bytes utf8, 5 bits begin +  6 bits later = 11 bits total                       
                    else if (*utf8 < 0xf0) wc = *utf8 &  0xf;// 3 bytes utf8, 4 bits begin + 12 bits later = 16 bits total                       
                    else                   wc = *utf8 &  0x7;// 4 bytes utf8, 3 bits begin + 18 bits later = 21 bits total                       
                }
                utf8 ++;
            }
            if (wc > 0x80) printf("%5x", wc);
        }
}

void putwc(int wc){ // putchar for wide char unicode
    if(wc < 0x80)       printf("%c", wc); // 7  bits: 0xxxxxxx
    else if(wc < 0x800) printf("%c%c",
            0xc0 | (wc >> 7),
            0x80 | (wc & 0x3f)
        ); // 11 bits: 110xxxxx 10xxxxxx , 5 + 6 bits
    else if(wc < 0x10000) printf("%c%c%c",
            0xe0 |  (wc >> 12),
            0x80 | ((wc >>  6) & 0x3f),
            0x80 |  (wc        & 0x3f)
        );// 16 bits: 1110xxxx 10xxxxxx 10xxxxxx, 4 + 6 + 6 bits
    else printf("%c%c%c%c",
            0xf0 | ((wc >> 18) & 7),
            0x80 | ((wc >> 12) & 0x3f),
            0x80 | ((wc >>  6) & 0x3f),
            0x80 |  (wc        & 0x3f)
        );// 21 bits: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx, 3 + 6 + 6 + 6 bits
}

void toHex(c_str utf8) { return toHex((u8int *)utf8); }
int main(){
    c_str src = "Taiwan,台灣,🀄,0123456789,\U0001F004";
    toHex(src);
    printf("\n%s\n", src);   
    putwc(0x1f004);
    printf("\n");
    return 0;
}

編譯並執行  g++ a.c && ./a.out

54, 61, 69, 77, 61, 6e, 2c,  53f0,  7063, 2c, 1f004, 2c, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 2c, 1f004
Taiwan,台灣,🀄,0123456789,🀄
🀄

2021年7月24日 星期六

在 linux 系統下簡單解碼 qrcode, 中文也可以

1. 使用 nanojpeg 先將 Jpeg 解成 bitmap, 到官網 http://keyj.emphy.de/nanojpeg 下載原始碼 nanojpeg.c :

      http://svn.emphy.de/nanojpeg/trunk/nanojpeg/nanojpeg.c

2. 接著用 quirc 將 bitmap 內的 qrcode 解碼, 到官網 https://github.com/dlbeer/quirc 下載所有原始碼:

     https://github.com/dlbeer/quirc/tree/master/lib 

3. 將上述下載的檔案全放入一個專案目錄內, 編寫一個簡單的 Makefile, 方便編譯及執行程式:

opencv = `pkg-config --cflags --libs opencv4`
LIBS = decode.o identify.o quirc.o version_db.o
run: a.out
    ./$<


a.out: qr.o $(LIBS)
    g++ $^ $(opencv)  -lfreetype  -o $@
%.o : %.c
    gcc -c $<
%.o : %.cpp
    g++ $(opencv)  -I/usr/include/freetype2 -std=c++17 -c $<
clean:
    rm -f a.out *.o
   
opencv-dev:
    sudo apt-get update
    sudo apt-get  install   libopencv-dev   libfreetype-dev

若尚未安裝好 libopencv 或 libfreetype 程式庫, 可以先執行 make  opencv-dev 安裝它


4. 最後, 用手機對準 qrcode 拍照存成檔案 sample.jpg,  寫個 c++ 程式 qr.cpp 測試一下:

#include <opencv2/core.hpp>
#include <opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/freetype.hpp>
#include "nanojpeg.c"
#include "quirc.h"
using namespace std;

int  main( ) {
    FILE *fin = fopen("sample.jpg", "rb");
    if (fin== nullptr) return 1;

    fseek(fin, 0L, SEEK_END);
    long len = ftell(fin);
    unsigned char *jpeg = (unsigned char *)malloc(len);
    if (jpeg == nullptr) {
        fclose(fin);
        return 2;
    } else {
        fseek(fin, 0L, SEEK_SET);
        fread(jpeg, 1, len, fin);
        fclose(fin);
    }

    njInit();
    if (njDecode(jpeg, len) == NJ_OK) free(jpeg);
    else {
        free(jpeg);
        return 3;
    }

    auto  font = cv::freetype::createFreeType2();// use freetype2, which is a pointer
    font->loadFontData("/usr/share/fonts/opentype/noto/NotoSerifCJK-Bold.ttc", 0);
    auto xRotate = [ ](const cv::Mat &src,
        double sx = 1.0,
        double sy = 1.0,
        int deg = 0) {// // scale first, then rotate, if deg > 0, counterclockwise
        cv::Mat dst;
        cv::resize(src, dst, cv::Size(), sx, sy, cv::INTER_LINEAR);
        if (deg != 0) cv::warpAffine(dst,
            dst,
            cv::getRotationMatrix2D(
                cv::Point2f(dst.cols/2.0, dst.rows/2.0),
                deg,
                1.0
            ),
            dst.size()
        );
        return dst;
    };
    auto toGrey = [ ](const cv::Mat &src) { // conver to 8 bits Y only
        cv::Mat dst;
        cv::cvtColor(src, dst, cv::COLOR_BGR2GRAY);
        return dst;
    };
    auto rgbBMP = xRotate(cv::Mat(njGetHeight(), njGetWidth(), CV_8UC3, njGetImage()),
        1.0/6,
        1.0/4,
        0
    );
    quirc *qr = quirc_new();
    if (qr) {
        auto grey = toGrey(rgbBMP);
        if (quirc_resize(qr, grey.cols, grey.rows) == QUIRC_SUCCESS) {
            int width  = grey.cols;
            int height = grey.rows;
            uint8_t *feed = quirc_begin(qr, &width, &height);
            uint8_t *greyLine = grey.ptr<uint8_t>(0); // line begin
            for (int j = 0; j < height; j++) {// scan every line
                for (int i = 0; i < width; i ++) *feed ++ = *(greyLine + i);// pixel feed one bye one
                greyLine += grey.cols; // next line begin
            }
            quirc_end(qr);
            int count = quirc_count(qr);// number of qrcode

            for (int i = 0; i < count; i ++) {
                quirc_data qData;
                quirc_code qCode;
                quirc_extract(qr, i, &qCode);
                int err = quirc_decode(&qCode, &qData);
                if (err == QUIRC_ERROR_DATA_ECC) {
                    quirc_flip(&qCode);
                    err = quirc_decode(&qCode, &qData);
                }
                if (err == QUIRC_SUCCESS) { // sucess
                    for (int j = 0; j < 4; j++) { // draw outline
                        auto &lba = qCode.corners[j];// line begin alias
                        auto &lea = qCode.corners[(j + 1) % 4];// line end alias
                        cv::line(rgbBMP,
                            cv::Point(lba.x, lba.y),
                            cv::Point(lea.x, lea.y),
                            cv::Scalar(0, 255, 0),
                            2
                        );
                    }                   
                    font->putText(rgbBMP,
                        string((char *)qData.payload),
                        cv::Point(0, grey.rows - 24 * (i + 1)),
                        24, // font height
                        cv::Scalar(0, 0, 255),// red color
                        -1, // solid line when thickness is negative
                        16, // to smooth
                        true
                    );
                    printf("[qrcode %d]-> %s <-[length = %d]\n", i, qData.payload, qData.payload_len);
                }
            }
        }
        quirc_destroy(qr);
    }

    njDone();
    cv::imshow("qrcode decode", rgbBMP);
    cv::waitKey(0);// show image, press any key to exit
    return 0;
}

執行 make run 看能不能解出 qrcode. 若不行, 可以調整 xRotate( ) 的長/寬放大率再試試看.

後記: 使用 freetype2 繪製文字(putText)時, 傳進去的影像型態 cv::Mat 必須使用 CV_8UC3 格式

   

2021年6月27日 星期日

程式流程小技巧

if { } else { } 是流程進行的分歧點, 當層級過多時, 程式碼的邏輯就變的難以理解, 例如像是:

          if(... ) {

                if(... ) {

                       if(... ) {

                                   ...

                       } else {

                       }

                } else { 

                       ...

                } ...

          } else if ( ) {

          } else if( ) {

          }

改用一次性的 while loop, 例如:

            do { 

                        if( ) { ... 

                                 break

                         } 

                        if( ) { ... 

                                break

                        } 

                        ...

             } while(false)

若沒有 do{ } while 指令, 只有 while { } 指令, 只要最後使用 break 來跳脫迴圈就可:

           while (true) {

                    if ( ) { 

                         ...

                         break

                     }  

                     ...

                    break

            } 

這樣就能將程式碼攤平, 變的較容易閱讀

2021年6月22日 星期二

使用 gradle 管理並編譯 kotlin 程式碼

1. 首先上 gradle  官網: https://gradle.org/releases 只要下載 binary-only 壓縮檔就可以了

2. 打開終端機, 把下載回來的檔案(gradle-7.1-bin.zip)解壓縮, 再將執行檔所在目錄加進 PATH 內, gradle 就安裝完成:

     cd Downloads  &&  unzip gradle-7.1-bin.zip

     PATH=$PATH:/home/mint/Downloads/gradle-7.1/bin

3.建個目錄儲存各種專案, 主程式目錄名稱, 同時也會被當成是專案名稱, 例如下述 example 次專案, 進入目錄內執行 gradle init 將會再當地目錄初始化成新專案, 接著跳出選項, 要依照畫面適當選取, 最後執行 gradle run 就能啟動樣版程式

     mkdir  Project  &&  cd Project

     mkdir  example  &&  cd example

     gradle init

     gradle run

觀察它所產生的檔案, 我打算利用 make 語法, 編寫一個簡短的 Makefile 方便以後初始化新專案,而不用每次初始化都要用選的:
# /home/mint/Project/Makefile
shellpwd  = $(shell pwd)
project  = `basename $(shellpwd)`
gradlebin  = /home/mint/Downloads/gradle-7.1/bin/gradle
templateKts  = /home/mint/Project/template.gradle.kts
templateApp  = /home/mint/Project/template.kt
appDir  = app
kotlinDir  = src/main/kotlin
targetDir  = $(appDir)/$(kotlinDir)
AppKt  = $(targetDir)/App.kt
settingsKts  = settings.gradle.kts
setProject  = "rootProject.name=(\"$(project)\")"
setInclude = "include(\"$(appDir)\")"
buildKts = $(appDir)/build.gradle.kts
ktsSetApp  = "application { mainClass.set(\"$(project).AppKt\") }"
ktsSetSrc = "sourceSets.main { java.srcDirs(\"$(kotlinDir)\") }"

name:
    @echo this is a kotlin project: $(project)

$(targetDir): $(appDir)/src/main
    @[ -d $@ ]|| mkdir $@

$(appDir)/src/main: $(appDir)/src
    @[ -d $@ ] || mkdir $@

$(appDir)/src: $(appDir)
    @[ -d $@ ] || mkdir $@

$(appDir):
    @[ -d $@ ] || mkdir $@

init: $(settingsKts) $(AppKt)
    @echo ... $(project) is ready

$(settingsKts):
    @if [ -f $@ ]; then echo .; else echo $(setProject)      > $@ && echo $(setInclude) >> $@; fi

$(AppKt): $(targetDir) $(buildKts)
    @if [ -f $@ ]; then echo .; else echo package $(project) > $@ && cat $(templateApp) >> $@; fi

$(buildKts):
    @if [ -f $@ ]; then echo .; else cp $(templateKts) $@ && echo $(ktsSetSrc) >> $@ && echo $(ktsSetApp) >> $@ ;fi
   
run:
    $(gradlebin)  run  -q  --args='arg1 arg2 arg3 ...'

build:
    $(gradlebin) build --offline

clean:
    rm -rf  build  $(appDir)/build  .gradle

再寫一個 /home/mint/Project/template.gradle.kts 樣版檔案, 用來產生 build.gradle.kts:

// template.gradle.kts
plugins {
    kotlin("jvm") version "1.5.10"
    application
}
repositories { mavenCentral() }
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") }

以及另一個  /home/mint/Project/template.kt 樣版範例, 用來產生 kotline 的程式碼:

// template.kt
import kotlinx.coroutines.*
import kotlin.coroutines.*
fun main() {
    runBlocking{
        launch {
            println("Hello")
        }
        launch {
            println("world")
        }
        launch {
            println("you are welcome")
        }
    }
}

將上述 3 個檔案各自存檔後, 在 Makefile 所在目錄內執行 make init, 或是使用參數 -f 指定 Makefile 位置, 上述 make init 用來先產生 settings.grtadle.kts, app/build.gradle.kts, 及 app/src/main/kotlin/App.kt 等範例代碼, 這樣 gradle run 就能編譯並且執行程式:

       make   init   -f   /home/mint/Project/Makefile

      gradle  run

不用寫程式, 就能讓上面的樣版程式跑起來, 夠神奇吧! 實際上是 gradle 先根據 build.gradle.kts 內容,先上網路倉庫(repositories), 找尋插件(plugins)將 kotlin 編譯程式  kotlin("jvm") version "1.5.10" 以及各種需要的相關(dependencies)程式庫,下載後存入硬碟, 再呼叫 kotlin 去編譯成 Java 代碼(byte code), 最後在 J(ava)V(irtual)M(achine 內執行程式. 一切全自動完成, 這就是 gradle 厲害的地方.

ps.

1. gradle 缺點是網路一定要通暢才能運作, 主要是 gradle daemon 要先跑起來.雖然可以用 gradle --offline build 離線編譯程式, 但沒了網路, 例如用 iptables 先把網路的輸出通道關閉:

    sudo  iptables  -A  OUTPUT  -m  owner  --gid-owner  mint  -j  REJECT

    gradle  --offline  build

他就無法編譯了?  這參數 --offline 目的就真的有點奇怪, 似乎名不正言不順.

 2. 如果用複製/貼上面的 Makefile 內容, 無法讓 make 運作, 可能是縮排(tab)的問題, 可以用文字編輯程式稍微修改, 因為 make 規定所有標的物的執行代碼(冒號: 下一行)必須要先縮排(tab)才能運作, 否則會出現錯誤訊息.

2021年6月13日 星期日

使用 kotlin 語言把 http 協定連線升級成 WebSocket 協定所需要的回應編碼: base64( sha1(clientKey) )

 參考資料: https://datatracker.ietf.org/doc/html/rfc6455

    // echo -n dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11 | sha1sum 
    // sha1Hex  : b37a4f2cc0624f1690f64606cf385945b2bec4ea
    // keyBase64: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
   
    val clientKey = "dGhlIHNhbXBsZSBub25jZQ=="  
    val guID      = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
    val base64sym = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    val key_GUID  = "${clientKey.trim()}$guID"
    val binary    = MessageDigest.getInstance("SHA-1").digest(key_GUID.toByteArray())
    val sha1Hex   = StringBuilder()
    val keyBase64 = StringBuilder()
    val temp3 = IntArray(3)
    var accumulate = 0
    for (c in binary)  {
        sha1Hex.append(String.format("%02x", c))
        temp3[accumulate ++] = c.toInt()
        if (accumulate == 3) {
            accumulate = 0;
            keyBase64.append(base64sym[ (temp3[0] and 0xfc) shr 2])
            keyBase64.append(base64sym[((temp3[0] and 0x03) shl 4) + ((temp3[1] and 0xf0) shr 4)])
            keyBase64.append(base64sym[((temp3[1] and 0x0f) shl 2) + ((temp3[2] and 0xc0) shr 6)])
            keyBase64.append(base64sym[  temp3[2] and 0x3f])
        }
    }
    if (accumulate > 0) {
        keyBase64.append(base64sym[ (temp3[0] and 0xfc) shr 2])
        if (accumulate == 2) {
            keyBase64.append(base64sym[((temp3[0] and 0x03) shl 4) + ((temp3[1] and 0xf0) shr 4)])
            keyBase64.append(base64sym[ (temp3[1] and 0x0f) shl 2])
        }
        keyBase64.append('=')
        if(accumulate == 1) keyBase64.append('=')
    }
    Log.d("WebSocket_key:", "$clientKey, $keyBase64, $sha1Hex") 

2021年6月10日 星期四

寫一個 Makefile 利用 openssl 產生自我簽章(selfsign)的證書(certification) 讓 Android SSLServerSocket 正常運作

將下列文字存成 Makefile, 複製到 Android 專案目錄內, 接著在終端機下指令 Make, 就會產生一個鑰匙庫(keystore.p12)放在 app/src/main/res/raw 目錄內, 而私鑰則放在 key 目錄下:
#Makefile
storepass=PKCS12_keystore_for_android_app
tempDir=/dev/shm
saveDir=app/src/main/res/raw
privateDir=key

gen: keystore.p12

keystore.p12: selfsign
    openssl pkcs12 -export -inkey $(privateDir)/prvkey.pem -passout pass:$(storepass) -in $(tempDir)/$< -out $(saveDir)/$@
    
selfsign: buildDirectory
    openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $(privateDir)/prvkey.pem -subj "/C=TW/ST=Taiwan/L=TaipeiCity/CN=localhost" -out $(tempDir)/$@
    
buildDirectory:
    [ -d $(saveDir) ] || mkdir $(saveDir)
    [ -d $(privateDir) ] || mkdir $(privateDir)

clean:
    rm -rf $(tempDir)/selfsign

Android app 可以利用 resources 將它載入, 之後就能讓 SSL Server Socket 正常運作:

        // ...
        val tls = SSLContext.getInstance("TLS").apply {
            val keyStore = KeyStore.getInstance("PKCS12")
            val storepass = "PKCS12_keystore_for_android_app"//pass phrase define in Makefile
            resources.openRawResource(R.raw.keystore).apply {
                keyStore.load(this, storepass.toCharArray())
                close()
            }
            val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply {
                init(keyStore, storepass.toCharArray())
            }
            init(kmf.keyManagers, null, null)
        }
        val sslServer = tls.serverSocketFactory.createServerSocket(8443) .apply{
            setReuseAddress(true)
        }

如果寫的是 ssl client 端程式, 則要將信任網站的簽署檔(cert file) 放入 keystore, 交由 TrustManager 來管理, Android 可以使用 BKS 格式檔:

        val tls = SSLContext.getInstance("TLS").apply {
            val keyStore = KeyStore.getInstance("PKCS12")
            val storepass = "PKCS12_keystore_for_android_app"//pass phrase define in Makefile
            resources.openRawResource(R.raw.keystore).apply {
                keyStore.load(this, storepass.toCharArray())
                close()
            }
            val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply {
                init(keyStore, storepass.toCharArray())
            }
            val trustStore = KeyStore.getInstance("BKS")
            val storepass = "PKCS12_keystore_for_android_app"//pass phrase define in Makefile
            resources.openRawResource(R.raw.truststore).apply {
                trustStore.load(this, storepass.toCharArray())
                close()
            }
            val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply {
                init(trustStore)
            }
            init(kmf.keyManagers, tmf.trustManagers, SecureRandom())
        }

2021年5月31日 星期一

理解血氧機 Pulse oximetry 的原理

參考資料

1. https://www.ti.com/lit/an/slaa655/slaa655.pdf?ts=1622422930797&ref_url=https%253A%252F%252Fwww.google.com%252F

2. https://www.nxp.com/docs/en/application-note/AN4327.pdf

3. https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4099100

4. https://highscope.ch.ntu.edu.tw/wordpress/?p=40839

動脈血(arterial blood)中含血紅素(red blood cell),血紅素中會攜帶氧氣(oxygen)的分子蛋白(protein)稱為血氧蛋白(hemoglobin), 血裏面混有含氧的血紅蛋白(oxyHemoglobin) 簡稱 HBO2, 及未攜氧的血紅蛋白 deoxyHemoglobin 簡稱 HB, 血氧飽和度(oxygen saturation) 簡稱 SaO2 定義為:

                         SaO2 = HBO2濃度 / (HBO2濃度 + HB濃度) = 1 / (1 + HB / HBO2)

實驗發現攜氧血紅蛋白(HBO2)與未攜氧血紅蛋白(HB)在紅光(波長 660nm波段)與近紅外光(波長 940nm波段)的摩爾消光係數(molar extinction coefficient)會有明顯的差異, 在紅光波段觀察, 當打到 HB 時, 因吸收了較多紅光, 出來時剩下微弱紅光而呈現暗紅色, 而打到 HBO2 則多數穿透過去, 因此呈現的是鮮(亮)紅色, 至於發射紅外光時則呈現相反的狀況, HBO2 反而因吸收較多紅外光譜, 出來的則是較弱(暗)紅外光,用示波器觀察就會發現, HB 與 HBO2 會隨著心跳(cardiac cycle)而產生週期性脈動信號(包含著直流與交流信號, 當輸入信號越強, 交直流也會等比例放大?).信號強度也隨著波長各異, 為了拉開差異解析出紅光與紅外光, 同時校正輸入信號準位(從 SaO2 的定義來看, 只要能測出 HB/HBO2 相對濃度, 就能得出 SaO2, 因為 HB 較吸紅光, 而 HBO2 比較吸收紅外光, 因此看起來測量紅光吸收率/紅外光吸收率, 其值必定與 HB/HBO2 濃度比成一定的比例關係 ?), 因此定義:

                         R = 測試紅光吸收率/測試紅外光吸收率  %

                         測試光譜吸收率 = 量測交流準位/量測直流準位

                         準位則是量測均方根 RMS (root mean square)

                        直流準位: 可以量波峰準位: ?

                        交流準位: 波峰準位 - 波谷準位 ?

利用上述參數的定義,使用標準血氧飽和度(0% ~ 100%), 用紅光及紅外光打進去分別量測光吸收率換算成 R 值 %, 作圖後會發現(例如將自變數 x 當作血氧飽和度 %,  因變數 y 則 是 R 值 %),大多數測試點會散落在負相關的直線方程式附近(這可以從 SaO2 的定義看出其成反比的關係式, 或是經驗值來看因 HB 較吸收紅光, 而 HBO2 比較吸收紅外光的方向來理解), 因此可以定義一個 SpO2 的直線方程:

                         SpO2: f(R) =  m * R + b, 其中 m 係數是斜率常數, b 係數是 bias 常數

實驗數據似乎會得到 m = -25 ? 及 b = 110 ?, 也許隨儀器參數選用也會有所不同, 這種用線性回歸分析法算出係數(m, b) 去對應標準 SaO2 數值就稱為 SpO2 ? 經上述數學推導及實驗校正就能得到精確結果(因為直線方程式是 1 對 1 的關係式, 量出 R % 就相當於測得血氧飽和度 %), 俗稱的血氧儀(Pulse oximetry)就是一種可以解析 SpO2 的工具,他需要的是發射兩種光譜(紅光及紅外光, 常用的遙控器內便有一棵紅外線 LED 發射器, 用手機照相機就能看到所發出的光強度, 面板上也會搭一棵紅光 LED 當作按鍵指示器, 可以說非常容易取得)再搭配一個接收感測器(像是 TSL235, 線性光譜轉換器)及信號運算放大器(OP)取得這兩種光譜的信號強度.

光吸收度(量)是一個相對數值(實際上沒有單位,只是一個純量 scalar), 用來度量輸出與輸入的相對衰減值, 或者換算為差異值. 通常用 log 來衡量,  數學上光吸收度 A 就能定義為:

                       A = - log(O/I) = log(I/O) = log(I) -  log(O) , 其中 I 是入射強度, O 是出射強度

根據比爾定律, 光被吸收量 absorbance = ε * c *d, 其中 c 是摩爾濃度單位, d 是光線經過介質的長度, ε 是消光係數或稱光衰係數 absorptivity, 因此輸入與輸出的關係式就可以推導出來:

                       - log(O/I)  = εcd

                       O = I * exp(-εcd)

備註: 文章內的問號 ? , 是還不理解是否正確.


2021年5月27日 星期四

Android 簡單利用 ZXing 產生 QR code, 並將它解碼

 1.用 AndroidStudio 產生樣板程式, 將 ZXing 程式庫加入 build.gradle, 並按下同步鈕,像這樣:
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.5.0'
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    implementation 'com.google.zxing:core:3.4.1'
}

2. 修改 activity_main.xml 內容,加入 ImageView 及 Button 像這樣:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black"
    tools:context=".MainActivity">
    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="visible"
        tools:ignore="MissingConstraints" />
    <Button
        android:id="@+id/bgButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="left|center_vertical"
        android:textAllCaps="false"
        android:background="@android:color/transparent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <TextView
        android:id="@+id/sample_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> 

3.修改主程式 MainActivity.kt , 像這樣:

package com.example.zxex1
import android.content.pm.ActivityInfo
import android.graphics.*
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.google.zxing.*
import com.google.zxing.common.HybridBinarizer
class MainActivity : AppCompatActivity() {
    private val qrBitmap = {text: String, square: Int ->
        val intArray = IntArray(square * square)
        try {//產生 QR code
            val bCode = MultiFormatWriter().encode(text,
                BarcodeFormat.QR_CODE,
                square,
                square,
                mapOf(EncodeHintType.CHARACTER_SET to "UTF8")
            )
            var k = 0
            for (row in 0 until square) {
                for (col in 0 until square) {
                    if (! bCode[row, col]) intArray[k + col] = Color.WHITE
                }
                k += square // next row
            }
        } catch (e:IllegalArgumentException) {  }
        Bitmap.createBitmap(intArray,
            square,
            square,
            Bitmap.Config.ARGB_8888
        )
    }
    private val painter = Paint() .apply {
        textAlign   = Paint.Align.CENTER
        style       = Paint.Style.STROKE
        color       = Color.RED
        textSize    = 24f
        strokeWidth = 4f
        isAntiAlias = true
    }
    private val shrinkSize = 640
    private val bgGround = Bitmap.createBitmap (
            shrinkSize,
            shrinkSize,
            Bitmap.Config.ARGB_8888
        )  .apply {            
            val centerX= width.toFloat() / 2
            val centerY= height.toFloat() / 2
            val left = centerX / 2     - 4// = 1 unit = 1/4
            val top  = centerY / 2     - 4// = 1 unit = 1/4
            val right= centerX * 3 / 2 + 4// = 3 unit = 3/4
            val down = centerY * 3 / 2 + 4// = 3 unit = 3/4
            Canvas(this).drawRect(left, top, right, down, painter)
        }
    private lateinit var bgButton:Button
    private lateinit var bgImageView:ImageView
    private fun testQRcode(code: String = "中文也可以") {
        try {
            val sqrSize= shrinkSize/2
            val sRGB = IntArray(sqrSize * sqrSize)
            val qrCode = qrBitmap(code, sqrSize) .apply { // 正方形
                getPixels(sRGB, 0, sqrSize, 0, 0, sqrSize, sqrSize)
            }// 解析 QR code
            MultiFormatReader()
                .decode (BinaryBitmap (HybridBinarizer (RGBLuminanceSource (
                    sqrSize, sqrSize, sRGB
                )))) .text ?. also { qrText -> // 成功解出文字
                Bitmap.createBitmap(bgGround) .apply {
                    val x = width.toFloat()/4
                    val y = height.toFloat()/4
                    Canvas(this).drawBitmap(qrCode, x, y, painter)
                    bgImageView.setImageBitmap(this)// 顯示合成照
                }
                bgButton.apply {
                    setTextColor(Color.GREEN)
                    text = qrText
                }
            }            
        } catch (e: Exception) {// QR code not found exception
            bgImageView.setImageBitmap(bgGround)
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
        if (savedInstanceState == null) {
            findViewById<TextView>(R.id.sample_text).text = "Hello"
            bgImageView = findViewById<ImageView>(R.id.imageView)
            bgButton = findViewById<Button>(R.id.bgButton).apply{
                background = GradientDrawable() .apply {
                    cornerRadius = 24f
                    setColor(Color.GREEN)
                }
                isAllCaps = false
                textSize = 24f
                val scanHint= "掃描 QR code"
                var id = 0
                text = scanHint
                setTextColor(Color.RED)
                setOnClickListener { _->
                    text = scanHint
                    setTextColor(Color.RED)
                    testQRcode("數字(${id ++})")
                }
            }
        }
        testQRcode()
    }
    override fun onResume(){ super.onResume()  }
    override fun onPause() {  super.onPause() }
}

4. 編譯並上傳到手機上執行看看

用 python 解簡單的常微分方程式

# sudo apt install python3-pip # python3 -m venv venv # cd venv # . bin/activate # pip3 install numpy matplotlib import numpy as np import m...