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. 編譯並上傳到手機上執行看看

2021年5月7日 星期五

Andoid 使用 kotlin 的 coroutine 實現簡單的 http 伺服器

程式複製到  MainActivity.kt:

package com.example.simplehttp
import android.os.Bundle
import android.net.wifi.WifiManager
import androidx.appcompat.app.AppCompatActivity
import java.net.ServerSocket
import java.net.Socket
import java.net.SocketTimeoutException
import java.io.IOException
import java.io.PrintWriter
import java.util.*
import kotlinx.coroutines.*
typealias _ReplyURI = (Socket) -> Unit
class MainActivity : AppCompatActivity() {
    private val patternBegin = "GET "
    private val patternEnd = " HTTP/1.1"
    private val hformat = "HTTP/1.0 200 OK\nServer: Simple\nContent-Type: %s\nContent-Length: %11d\nConnection: close\n\n"
    private val socketPrintHTML:(Socket, String) -> Unit = {sfd, html ->
        try {
            val writer = PrintWriter(sfd.getOutputStream(), true)
            writer.printf(hformat, "text/html", html.length)
            writer.printf("%s", html)
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }
    data class OneResponse(val uri:String, val lambda: _ReplyURI)
    inner class ToResponse(val fd: Socket) {
        private val field = ArrayList<OneResponse>()
        private val lambdaNull: _ReplyURI = { socketPrintHTML(it, "is lambda Null ok?") }
        val execute:(String) -> Unit = { uri ->
            if(! fd.isClosed) {
                val matchURI = uri.toLowerCase()
                for(i in field.indices) if(field[i].uri == matchURI) {
                    field[i].lambda(fd)
                }
                socketPrintHTML(fd, "not found $uri")
            }
        }
        fun toReply(uri:String, lambda:_ReplyURI? = null) {
            field.add(OneResponse(
                uri.toLowerCase(),
                lambda ?: lambdaNull
            ))
        }
    }
    private fun uhttpRequest(res:ToResponse) {
        val rxSize = 1024
        val rxBuffer = ByteArray(rxSize)
        val len = res.fd.getInputStream().read(rxBuffer, 0, rxSize - 1)
        if(len > 0) {
            rxBuffer[len] = 0
            val str = String(rxBuffer)
            if (str.startsWith(patternBegin) && str.contains(patternEnd)) res.
                execute( str.substring( patternBegin.length, str.indexOf(patternEnd) ))
        }
    }
    private lateinit var serverJob: Job
    private var portTCP: Int = 8080
    private var snapURI: String = "/"
    private  fun uListen() {
        var streamingLock = false
        val tcpServer = ServerSocket(portTCP).apply {
            soTimeout = 40
        }
        while (serverJob.isActive) {
            try {
                val socketfd = tcpServer.accept()
                val req = ToResponse(socketfd). apply {
                    toReply("/Hello1")
                    toReply("/Hello2") { sfd ->
                        socketPrintHTML(sfd, "You are welcome")
                    }
                    toReply(snapURI) { sfd -> runBlocking {
                        while(streamingLock) delay(100)
                        streamingLock = true //...
                            socketPrintHTML(sfd, "Coroutine runBlocking ok")                        
                        streamingLock = false
                    }}
                }
                GlobalScope.launch(Dispatchers.IO) {
                    uhttpRequest(req)
                    socketfd.close()
                }
            } catch (e: SocketTimeoutException) {                
            }
        }
        tcpServer.close()
    }    
    override fun onResume() {        
        val info = (applicationContext.
            getSystemService(WIFI_SERVICE) as WifiManager).connectionInfo
        val a = info.ipAddress        and 0xff
        val b = info.ipAddress shr  8 and 0xff
        val c = info.ipAddress shr 16 and 0xff
        val d = info.ipAddress shr 24 and 0xff
        title = "http://$a.$b.$c.$d:${portTCP}${snapURI}"
        serverJob = GlobalScope.launch(Dispatchers.IO) {
            uListen()            
        }
    }    
    override fun onPause() {
        runBlocking {
            serverJob.cancelAndJoin()
        }
        super.onPause()
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if (savedInstanceState == null) {
        }
    }
}

簡單 c 程式碼, 根據五行八卦相生相剋推斷吉凶

#include "stdio.h" // 五行: //               木2 //      水1           火3 //         金0     土4 // // 先天八卦 vs 五行 //                    ...