欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JS前端使用canvas搞一個手勢識別

 更新時間:2022年08月02日 15:26:05   作者:尤水就下  
這篇文章主要為大家介紹了JS前端使用canvas搞一個手勢識別的實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

前言

最近在看一些關(guān)于圖形學(xué)的東西,寫了個一筆畫手勢識別的小 demo,效果大概是下面這個樣子:

如果你是初次看過肯定會覺得很有意思??。哈哈,話不多說,讓我們直接開擼吧。

這里可以先花幾秒鐘想一下你會怎么做???帶著問題往下看能夠記得更牢固,比如你可能最關(guān)心的就是怎么識別怎么對比。這里先提前貼上項目 demo 地址,有需要的自取。另外這里并不會涉及什么人工智能、AI識別、深度學(xué)習(xí)啥的,所以請放心食用??。

具體步驟

發(fā)車?yán)?????????

第一步:手勢繪制

既然要識別那肯定得先有手勢啦,所以第一步要做的就是手勢繪制,這一步相對來說比較簡單,學(xué)習(xí)過 canvas 的同學(xué)應(yīng)該有看過畫板的實現(xiàn),這個也是一樣的,監(jiān)聽 canvas 上的鼠標(biāo)事件,然后在移動的時候?qū)⑹髽?biāo)坐標(biāo)點用線段相連即可,不同的是我們在繪制過程中還順便把每個坐標(biāo)點都畫了一下,核心代碼如下(可跳過):

handleMousemove(e: MouseEvent) {
    if (!this.isMove) return;
    const curPoint = this.getCanvasPos(e);
    const lastPoint = this.inputPoints[this.inputPoints.length - 1];
    // 畫線段
    CanvasUtils.drawLine(this.ctx2d, lastPoint[0], lastPoint[1], curPoint[0], curPoint[1], 'blue', 3);
    // 畫坐標(biāo)點
    CanvasUtils.drawCircle(this.ctx2d, curPoint[0], curPoint[1], 5);
    // 如果覺得原始點的數(shù)量太多,可以節(jié)流
    this.inputPoints.push(curPoint);
}

畫完之后大概是下面這個樣子:

從上圖可以看到繪制出來的紅點并不均勻,因為一筆畫過程中的手速不一樣,疏密程度也就不一樣,所以為了避免這個因素的影響,我們需要重新取個樣。

第二步:重新取樣

不同場景的取樣方式也有所不同。這里我們簡單的選擇等分線條取樣即可,也就是先計算出整個手勢的長度(所有線段長度相加),然后 n 等分取點(隨便幾等分,看效果調(diào)節(jié),不用糾結(jié))。

注意我們并沒有改變原始坐標(biāo)點的信息,手勢的繪制還是要按照原來的點繪制,所以需要加一個變量來存儲新采樣的點(后面的計算全都是用新的取樣點來計算)。這個計算還是有點小麻煩的,所以我準(zhǔn)備了一張圖方便大家理解????:

然后就是具體的代碼實現(xiàn)(大概懂了可跳過):

export type Point = [number, number];
static resample(inputPoints: Point[], sampleCount: number): Point[] {
    const len = GeoUtils.getLength(inputPoints);
    const unit = len / (sampleCount - 1);
    const outputPoints: Point[] = [[...inputPoints[0]]];
    let curLen = 0;
    let prevPoint = inputPoints[0];
    for (let i = 1; i < inputPoints.length; i++) {
        const curPoint = inputPoints[i];
        let dx = curPoint[0] - prevPoint[0];
        let dy = curPoint[1] - prevPoint[1];
        let tempLen = GeoUtils.getLength([prevPoint, curPoint]);
        while (curLen + tempLen >= unit) {
            const ds = unit - curLen;
            const ratio = ds / tempLen;
            const newPoint: Point = [prevPoint[0] + dx * ratio, prevPoint[1] + dy * ratio];
            outputPoints.push(newPoint);
            curLen = 0;
            prevPoint = newPoint;
            dx = curPoint[0] - prevPoint[0];
            dy = curPoint[1] - prevPoint[1];
            tempLen = GeoUtils.getLength([prevPoint, curPoint]);
        }
        prevPoint = curPoint;
        curLen += tempLen;
    }
    while (outputPoints.length < sampleCount) {
        outputPoints.push([...prevPoint]);
    }
    return outputPoints;
}

重新采樣之后大概是下面這個效果:

要注意如果你采用了 n 等分,那么所有的手勢都應(yīng)該是 n 等分的,不能改變,否則難以比較。另外我們順便把手勢的中心點算了出來(就是簡單的把每個采樣點坐標(biāo)相加取平均值),并且將手勢的起始點(最后一個點也行)與中心點相連,這個你可以粗淺的認(rèn)為它表示的是這個手勢的大致方向,不理解可以先跳過,后續(xù)會講到。

第二步:平移

其實你要比較任何東西,都是要量化成數(shù)字來比較的,而不是通過感覺。 不能說我覺的兩個手勢長得像它就像,那只是人工沒有智能,所以我們要怎么解決這個問題呢?我們需要定一個標(biāo)準(zhǔn),讓所有手勢都在同一個模子下進行比較(就好像你要找個對象,不得有個衡量標(biāo)準(zhǔn)嗎),比如都變成同樣的大小、同樣的方向。

不然你想想如果我豎著寫了一個很大的3和橫著寫了一個很小的3,它們要怎么比較。所以接下來我們要做的就是把手勢標(biāo)準(zhǔn)化(其實每幅示例圖中的虛線框就是我們的架子),為后續(xù)的比較打好基礎(chǔ),為此就需要經(jīng)歷平移、旋轉(zhuǎn)、縮放這幾個步驟。

關(guān)于平移,剛才我們已經(jīng)計算過手勢的中心點,現(xiàn)在只需要把它移動到畫布中心即可,簡單算下平移距離,然后對所有新的采樣點做平移操作即可,示例代碼如下:

// 對每個坐標(biāo)點進行平移
static translate(points: Point[], dx: number, dy: number) {
    points.forEach((p) => {
        p[0] += dx;
        p[1] += dy;
    });
}

效果如下:

要注意我們在繪制的時候需要將畫布左上角的原點移到到畫布中間,這樣做能夠極大的方便計算,包括接下來的旋轉(zhuǎn)和縮放也是在平移坐標(biāo)系的基礎(chǔ)上。

第三步:旋轉(zhuǎn)

細心的同學(xué)會發(fā)現(xiàn)除了中間的虛線框,我們還把整個畫布八等分了,這是為什么呢?其實上文中有提到,是因為手勢具有方向性,比如 丨和 /,這兩種手勢本應(yīng)該很相近,但是方向不同,所以就需要進行一定的旋轉(zhuǎn)。

而這里的八條等分線就是我們要靠近的方向(幾等分也是你自己隨意取的),于是乎我們可以簡單地算下手勢方向(圖中的綠線)離哪條等分線近就往哪邊旋轉(zhuǎn),然后把所有的點都進行旋轉(zhuǎn)變換即可,代碼如下(可跳過):

// 計算需要旋轉(zhuǎn)到最近輔助線的弧度,center 為中心點,startPoint 為手勢起始點,sublineCount 為坐標(biāo)等分?jǐn)?shù)量
static computeRadianToSubline(center: Point, startPoint: Point, sublineCount: number): number {
    const dy = startPoint[1] - center[1];
    const dx = startPoint[0] - center[0];
    let radian = Math.atan2(dy, dx);
    if (radian < 0) radian += TWO_PI;
    const unitRadian = TWO_PI / sublineCount;
    const targetRadian = Math.round(radian / unitRadian) * unitRadian;
    radian -= targetRadian;
    return radian;
}
// 對每個坐標(biāo)點進行旋轉(zhuǎn)
static rotate(points: Point[], radian: number) {
    const sin = Math.sin(radian);
    const cos = Math.cos(radian);
    points.forEach((p) => {
        let [x, y] = p;
        p[0] = cos * x - sin * y;
        p[1] = sin * x + cos * y;
    });
}

很多同學(xué)可能會覺得旋轉(zhuǎn)比平移難,其實很簡單的,你只需要知道一個點是怎么旋轉(zhuǎn)的就行了(線段的旋轉(zhuǎn)就是兩個端點的旋轉(zhuǎn),多邊形的旋轉(zhuǎn)就是多個頂點的旋轉(zhuǎn)),這里我畫了張推導(dǎo)圖方便大家理解(不感興趣也可以跳過):

然后看下這步的效果圖:

第四步:縮放

我們每次繪制的手勢是有大有小的,所以這里需要統(tǒng)一成一個大小,也就是做個縮放。

比如我們要把一個 600*600 的手勢放進一個 100*100 的容器中(也就是圖中的虛線框),那就要縮小 6 倍。

那具體要怎么求呢?首先我們要求出手勢的包圍盒大小,這里采用AABB模型(還有OBB、球模型等)。

那什么是 AABB 包圍盒呢,這個賊簡單,就是找出所有采樣點的最大最小 x、y 值即可,就像下面這樣:

現(xiàn)在只要用容器長度除以 AABB 的最長邊,得到的就是縮放倍數(shù)。然后同樣的,遍歷所有點進行縮放操作,具體代碼如下:

// 再次提醒下因為我們已經(jīng)把坐標(biāo)系移到了畫布中央,畫布中心和手勢中心是重合的,所以直接乘以縮放倍速就可以了
static scale(points: Point[], scale: number) {
    points.forEach((p) => {
        let [x, y] = p;
        p[0] = x * scale;
        p[1] = y * scale;
    });
}

效果圖如下:

注意不是說縮放之后的圖形一定要在虛線框里面,而是縮放之后的圖形大小和虛線框差不多。

第五步:手勢錄入

這個就是簡單的保存數(shù)據(jù),一共可分為兩步:

  • 縮略圖:動態(tài)地創(chuàng)建一個 canvas 來繪制手勢,再通過 drawImage 繪制到畫布上,這個其實和第一步是一樣的,只不過圖變小了。用原始點或采樣點畫都可以(原始點比較精確),畢竟是縮略圖,看不出來太大差別。
  • 保存數(shù)據(jù):采樣坐標(biāo)點肯定是要保存的,畢竟我們辛辛苦苦標(biāo)準(zhǔn)化了這么久,其它的想保存啥就保存啥。

第六步:比較(重點)

假設(shè)我們已經(jīng)有了兩個標(biāo)準(zhǔn)化后的手勢,那怎樣才能知道他們相似呢?如果你沒看過相關(guān)知識,大概率是不懂的,我。也是??。。。同樣的,這里也可以停下來思考幾秒種??。。。 ok,其實手勢相似與否可以轉(zhuǎn)成兩組采樣點是否足夠靠近的問題,一種直觀的解法就是計算兩組采樣點之間的距離,看是否小于某個閾值,類似下面這樣:

不懂的話想成一個采樣點就好理解了(就變成了求兩點距離??),具體代碼如下:

static squaredEuclideanDistance(points1, points2) {
    let squaredDistance = 0;
    const count = points1.length;
    for (let i = 0; i < count; i++) {
        const p1 = points1[i];
        const p2 = points2[i];
        const dx = p1[0] - p2[0];
        const dy = p1[1] - p2[1];
        squaredDistance += dx * dx + dy * dy;
    }
    return squaredDistance;
}

其實上面這種方法有個高大上的名字,叫歐氏距離(好了好了,別裝了??)。

但是對于我們這種場景有個更好的相似度算法(算法?溜了溜了!),所以接下來我們來介紹一個余弦相似度的概念(不難的,我都畫了圖的,包看包會):

如果上圖采用的是歐氏距離比較,顯然 AC 距離更近更相似。

如果用余弦值比較,那顯然是 AB 更相似。

這是因為歐氏距離得到的是絕對差異,余弦相似度比較的是相對差異(仔細品品??)。

那為什么夾角的大小可以判定兩點的相似度呢?

其實這個方法主要判定的是兩點方向的相似度,你可以看到即便向量B很長,但是不影響它的方向朝向,所以B的目標(biāo)朝向和A更相近,這個用力學(xué)的知識會比較好理解一點,看下下面這張圖:

夾角越小,發(fā)力的方向才越一致,我們才能拉動一個物體(我們就是有相同目標(biāo)的一類人,也就是相似)。那這么多個點我們怎么算余弦相似度呢?

回頭看看剛才求夾角的公式,既然只和方向相關(guān),而和向量A、向量B長度無關(guān),那么我們一般可以把A、B變成單位向量(就是向量除以它們自身的長度),這樣A、B的模長就為1,于是余弦值就可以變成這樣:

是不是突然簡單了不少,接下來我們就想辦法把手勢變成向量就行(就是變成一個很長的數(shù)組),這里看圖理解會方便些:

我們可以把轉(zhuǎn)變后的一維數(shù)組叫做這個手勢的特征,并當(dāng)做數(shù)據(jù)保存下來,下次比較的時候直接把這個數(shù)組拿出來算余弦值即可。

// 計算余弦相似度
static calcCosDistance(vector1: number[], vector2: number[]): number {
    let similarity = 0;
    vector1.forEach((v1, i) => {
        const v2 = vector2[i];
        similarity += v1 * v2;
    });
    return similarity; // 相似度介于 -1~1
}

余弦相似度在很多場合都是有用到的,比如文章相似度中詞向量的應(yīng)用(扯遠了),所以這里簡要回顧一下它的具體思路:

  • 想辦法把原始數(shù)據(jù)轉(zhuǎn)換成長度相同的一維數(shù)組[a, b, c, ..., n],(雖然是一維數(shù)組,但是是 n 維向量,不理解沒關(guān)系)。
  • 遍歷現(xiàn)有數(shù)據(jù),分別求出對應(yīng)的余弦值,找出相似度值最高的那一個。

注意事項

  • 手勢具有方向性:我們可以識別|/,因為他們經(jīng)過旋轉(zhuǎn)都靠近y軸,但是|就不行了,一個是 y 軸一個是 x 軸。所以如果我們要想把|識別成一個東西可以這樣子搞,把|多旋轉(zhuǎn)幾個角度,在每個角度都判斷一下是否相似。
  • 手勢的寬高比會影響結(jié)果:比如你畫一個正方形和一個長長扁扁的矩形是不相似的。
  • 采樣點的數(shù)量:過多過少都不行,過多效率低,對圖形一致性要求也高,反之同理。
  • 手勢的復(fù)雜度:圖形的識別率和圖形的復(fù)雜性沒有太大關(guān)系。簡單的圖形由于特征不明顯,容易出錯,比如多邊型和圓。復(fù)雜的圖形,采樣點就容易被稀釋,得到的特征比較粗。
  • 應(yīng)用場景:大家可以自己想想這個東西除了用在手勢還能用在那里?這里舉個例子,比如數(shù)學(xué)老師在遠程上課、寫板書的時候,經(jīng)常需要徒手畫圓或者畫正方形,這里我們就可以幫其自動校正,如果畫的像一個圓就自動重新生成一個正圓,也許描述的比較蒼白,所以大家可以自行腦補一下畫面??。

比較的基本套路(可跳過)

這里簡單補充下比較兩個東西是否相似的一般套路,也就兩大步:

特征提?。ň褪翘幚頂?shù)據(jù)的過程):

不管是什么東西,都有對應(yīng)的原始數(shù)據(jù),我們要做的就是將其(經(jīng)過層層處理)轉(zhuǎn)換成同一個框架維度下(也就是標(biāo)準(zhǔn)化),通常就是將原始數(shù)據(jù)轉(zhuǎn)換成長度相同的一維數(shù)組(再次強調(diào)雖然是一維數(shù)組,但其實是 n 維向量)。

算法識別(就是比較數(shù)據(jù)的過程):

  • 通過某種算法(比如上面提到的歐氏距離和余弦相似度)進行逐一對比。
  • 類似的還有網(wǎng)格識別(先把圖片馬賽克化,像素粒度就變粗了,然后根據(jù)像素顏色差值進行比較,這個方法是適用于以縮略圖找原圖)、方向識別(比如只要手勢順序是先向右再向下再向左再向上就認(rèn)為是矩形)等。
  • 顯然不同的特征和算法就造成了結(jié)果的千差萬別(效率啊、準(zhǔn)確率等,還有薪資待遇???),優(yōu)化的手段也是百花齊放,所以也就沒有通用的算法,只有適合的算法,因地制宜。

我們以一個極其簡單的推薦算法為例,推薦算法的問題在某種程度上可以轉(zhuǎn)換成兩個人的喜好相似程度:

喜好干飯摸魚睡覺就是玩...
甲(咸魚)????...
乙(翻身)????...
????...

這個和我們的手勢識別不能說是很像,只能說是一模一樣,在已有的手勢中(甲乙)找一個和我(喜好)相似度較高的,每一行其實就是一系列采樣點,最終可以簡單的推斷出我(可能)是條咸魚??,還喜歡摸魚。

又比如你打算買一臺電腦,那大概率是先看下周圍的人用什么,然后你就買什么,從眾本質(zhì)上也是一種相似(大眾的選擇就是方向),近朱者赤近墨者黑嘛。如果你說很獨立自主,自己想買啥買啥,那也是對的,畢竟這玩意怎么搞都搞不到 100%。

關(guān)于多筆畫(可跳過)

我們本文學(xué)的是單筆畫,現(xiàn)在你可以稍微想下,如果是多筆畫應(yīng)該怎么搞?這里還是可以短暫的思考幾秒種??。。。

  • 對于簡單的一筆畫來說上面的識別效果是很不錯的,不論是效率和準(zhǔn)確率,但如果是多筆畫,那就復(fù)雜起來了,比如漢字的識別(想想就頭大??)。
  • 這里就介紹一個簡單的識別方法,就是把多筆畫拆成單筆畫,通過本文的學(xué)習(xí)你可以求出每個單筆畫的相似度,然后簡單求和就可以得到整個字體的相似度,最后取相似度最高的即可(就這??)。

舉個具象點的例子,比如這個字(這里僅僅是例子哈,不完全是這樣):

提取每個漢字的筆畫特征,一般可以采集起始點、終點和中間的轉(zhuǎn)折點。數(shù)據(jù)大概長下面這個樣子:

  • 處理數(shù)據(jù)(標(biāo)準(zhǔn)化的過程,比如把每個字移到畫布中心,縮放成一樣的大?。?/li>
  • 比較數(shù)據(jù)(選個算法,這里就是先判斷下筆畫數(shù),再簡單的將單筆相似度相加求和) 這就完了?當(dāng)然還差得遠呢,問題一抓一大把。比如:
  • 由于存在連筆的情況,一筆可能寫成兩筆,所以我們應(yīng)該允許筆畫的誤差在 2 左右,但是在最終排序時,筆畫數(shù)越接近的,優(yōu)先級越高。
  • 每一筆當(dāng)中至少包含起點和終點,中間可能有幾個拐點,如果比較的時候單筆的坐標(biāo)點數(shù)量不同該怎么處理?一種方式是進行插值計算,另一種方式是取最初的采樣點信息。

采用上述的方式如果我寫了個字是不是好像也能識別出來,大體都是一橫一豎,有沒有什么辦法可以避免呢?當(dāng)然是有的,現(xiàn)在我們每一筆保存的不再是點的坐標(biāo),而是該點與前一個點連線的角度,如果是每一筆的起始點,就拿上一筆的終點作為前一個點,說起來比較抽象,所以我又畫了張圖????(很簡單的一張圖,不要被嚇到??):

大家想想如果是字,在上圖的第二個角度(綠2)中是不是就可以明顯區(qū)分開了。另外我們只保存了兩兩點之間的角度,還省了不少空間呢。

看起來好像沒問題了?不,還是差得遠呢。你想想要是筆畫順序不對咋整。還是以為例,我先寫豎再寫橫咋整。啊這。。。其實還有其他識別方法,比如把文字按坐標(biāo)軸切分成四塊,分四段校驗,這就不深入了,點到即止(畢竟就懂點皮毛)。

小結(jié)

以上就是手勢識別的大致思路,雖然看起來是挺高大上的一個東西,但是讀完之后應(yīng)該覺得。不。。算難吧。。。有些東西不是你不會只是你不知道也沒去嘗試下,嘿嘿。最后,再次送上項目地址傳送門,順便附上我 canvas 專欄的另外兩篇實戰(zhàn)文章:

html2canvas 用著有問題?手寫一個就知道為啥了??

??用 canvas 來畫個函數(shù)曲線吧!縱享絲滑

更多關(guān)于JS前端canvas手勢識別的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 文字幻燈片

    文字幻燈片

    文字幻燈片...
    2006-06-06
  • 微信小程序 歡迎界面開發(fā)的實例詳解

    微信小程序 歡迎界面開發(fā)的實例詳解

    這篇文章主要介紹了微信小程序 歡迎界面開發(fā)的實例詳解的相關(guān)資料,這里實現(xiàn)歡迎界面的簡單實例和實現(xiàn)代碼及實現(xiàn)效果圖,需要的朋友可以參考下
    2016-11-11
  • JavaScript實例?ODO?List分析

    JavaScript實例?ODO?List分析

    這篇文章主要介紹了JavaScript實例?ODO?List分析,主要利用JavaScript、css、HTML等實例代碼展開起內(nèi)容的解析,需要的小伙伴可以參考一下
    2022-01-01
  • 微信小程序引用公共js里的方法的實例詳解

    微信小程序引用公共js里的方法的實例詳解

    這篇文章主要介紹了微信小程序引用公共js里的方法的實例詳解的相關(guān)資料,這里提供了實現(xiàn)的方法,希望能幫助到大家,需要的朋友可以參考下
    2017-08-08
  • JavaScript 定時器詳情

    JavaScript 定時器詳情

    這篇文章主要介紹了JavaScript 定時器,在JavaScript中定時器有兩個 setInterval() 與 setTimeout() 分別還有取消定時器的方法,下面來看看文章的詳細介紹
    2021-11-11
  • 微信小程序 教程之小程序配置

    微信小程序 教程之小程序配置

    這篇文章主要介紹了微信小程序 教程之小程序配置的相關(guān)資料,這里對app.json,pages,window等做了詳細介紹,對于初學(xué)開發(fā)微信小程序的朋友,掌握這些還是比較重要的,需要的朋友可以參考下
    2016-10-10
  • 手把手教你從0搭建前端腳手架詳解

    手把手教你從0搭建前端腳手架詳解

    這篇文章主要介紹了手把手教你從0搭建前端腳手架詳解,腳手架就是在啟動的時候詢問一些簡單的問題,并且通過用戶回答的結(jié)果去渲染對應(yīng)的模板文件,需要的朋友可以參考下
    2023-03-03
  • 微信小程序使用navigateTo數(shù)據(jù)傳遞的實例

    微信小程序使用navigateTo數(shù)據(jù)傳遞的實例

    這篇文章主要介紹了微信小程序使用navigateTo數(shù)據(jù)傳遞的實例的相關(guān)資料,希望通過本文能幫助到大家,需要的朋友可以參考下
    2017-09-09
  • 解析Clipboard?API剪貼板操作實例

    解析Clipboard?API剪貼板操作實例

    這篇文章主要為大家介紹了解析Clipboard?API剪貼板操作實例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-09-09
  • 微信小程序 window_x64環(huán)境搭建

    微信小程序 window_x64環(huán)境搭建

    這篇文章主要介紹了微信小程序 window_x64環(huán)境搭建的相關(guān)資料,需要的朋友可以參考下
    2016-09-09

最新評論