摘要:萬(wàn)萬(wàn)沒(méi)想到,在圣誕節(jié)前夕,女神居然答應(yīng)了在下的約會(huì)請(qǐng)求。想在下正如在座的一些看官一樣,雖玉樹(shù)臨風(fēng)風(fēng)流倜儻,卻總因猜不透女孩的心思,一不留神就落得個(gè)母胎單身。在內(nèi)部將張量表示為基本數(shù)據(jù)類型的維數(shù)組。
本文將結(jié)合移動(dòng)設(shè)備攝像能力與 TensorFlow.js,在瀏覽器里實(shí)現(xiàn)一個(gè)實(shí)時(shí)的人臉情緒分類器。鑒于文章的故事背景較長(zhǎng),對(duì)實(shí)現(xiàn)本身更有興趣的同學(xué)可直接跳轉(zhuǎn)至技術(shù)方案概述。
DEMO 試玩
前言看遍了 25 載的雪月沒(méi)風(fēng)花,本旺早已不悲不喜。萬(wàn)萬(wàn)沒(méi)想到,在圣誕節(jié)前夕,女神居然答應(yīng)了在下的約會(huì)請(qǐng)求。可是面對(duì)這么個(gè)大好機(jī)會(huì),本前端工程獅竟突然慌張起來(lái)。想在下正如在座的一些看官一樣,雖玉樹(shù)臨風(fēng)、風(fēng)流倜儻,卻總因猜不透女孩的心思,一不留神就落得個(gè)母胎單身。如今已是 8102 年,像我等這么優(yōu)秀的少年若再不脫單,黨和人民那都是一萬(wàn)個(gè)不同意!痛定思痛,在下這就要發(fā)揮自己的技術(shù)優(yōu)勢(shì),將察「顏」觀色的技能樹(shù)點(diǎn)滿,做一個(gè)洞悉女神喜怒哀愁的優(yōu)秀少年,決勝圣誕之巔!
正題開(kāi)始 需求分析我們前端工程師終于在 2018 年迎來(lái)了 TensorFlow.js,這就意味著,就算算法學(xué)的再弱雞,又不會(huì) py 交易,我們也能靠著 js 跟著算法的同學(xué)們學(xué)上個(gè)一招半式。如果我們能夠在約會(huì)期間,通過(guò)正規(guī)渠道獲得女神的照片,是不是就能用算法分析分析女神看到在下的時(shí)候,是開(kāi)心還是...不,一定是開(kāi)心的!
可是,約會(huì)的戰(zhàn)場(chǎng)瞬息萬(wàn)變,我們總不能拍了照就放手機(jī)里,約完會(huì)回到靜悄悄的家,再跑代碼分析吧,那可就 「too young too late」 了!時(shí)間就是生命,如果不能當(dāng)場(chǎng)知道女神的心情,我們還不如給自己 -1s!
因此,我們的目標(biāo)就是能夠在手機(jī)上,實(shí)時(shí)看到這樣的效果(嘛,有些簡(jiǎn)陋,不過(guò)本文將專注于功能實(shí)現(xiàn),哈哈):
技術(shù)方案概述很簡(jiǎn)單,我們需要的就兩點(diǎn),圖像采集 & 模型應(yīng)用,至于結(jié)果怎么展示,嗨呀,作為一個(gè)前端工程師,render 就是家常便飯呀。對(duì)于前端的同學(xué)來(lái)說(shuō),唯一可能不熟悉的也就是算法模型怎么用;對(duì)于算法的同學(xué)來(lái)說(shuō),唯一可能不熟悉的也就是移動(dòng)設(shè)備怎么使用攝像頭。
我們的流程即如下圖所示(下文會(huì)針對(duì)計(jì)算速度的問(wèn)題進(jìn)行優(yōu)化):
下面,我們就根據(jù)這個(gè)流程圖來(lái)梳理下如何實(shí)現(xiàn)吧!
核心一:圖像采集與展示 圖像采集我們?nèi)绾问褂?strong>移動(dòng)設(shè)備進(jìn)行圖像或者視頻流的采集呢?這就需要借助 WebRTC 了。WebRTC,即網(wǎng)頁(yè)即時(shí)通信(Web Real-Time Communication),是一個(gè)支持網(wǎng)頁(yè)瀏覽器進(jìn)行實(shí)時(shí)語(yǔ)音對(duì)話或視頻對(duì)話的 API。它于 2011 年 6 月 1 日開(kāi)源,并在 Google、Mozilla、Opera 支持下被納入萬(wàn)維網(wǎng)聯(lián)盟的 W3C 推薦標(biāo)準(zhǔn)。
拉起攝像頭并獲取采集到的視頻流,這正是我們需要使用到的由 WebRTC 提供的能力,而核心的 API 就是 navigator.mediaDevices.getUserMedia。
該方法的兼容性如下,可以看到,對(duì)于常見(jiàn)的手機(jī)來(lái)說(shuō),還是可以較好支持的。不過(guò),不同手機(jī)、系統(tǒng)種類與版本、瀏覽器種類與版本可能還是存在一些差異。如果想要更好的做兼容的話,可以考慮使用 Adapter.js 來(lái)做 shim,它可以讓我們的 App 與 Api 的差異相隔離。此外,在這里可以看到一些有趣的例子。具體 Adapter.js 的實(shí)現(xiàn)可以自行查閱。
那么這個(gè)方法是如何使用的呢?我們可以通過(guò) MDN 來(lái)查閱一下。MediaDevices getUserMedia() 會(huì)向用戶申請(qǐng)權(quán)限,使用媒體輸入,獲得具有指定類型的 MediaStream(如音頻流、視頻流),并且會(huì) resolve 一個(gè) MediaStream 對(duì)象,如果沒(méi)有權(quán)限或沒(méi)有匹配的媒體,會(huì)報(bào)出相應(yīng)異常:
navigator.mediaDevices.getUserMedia(constraints) .then(function(stream) { /* use the stream */ }) .catch(function(err) { /* handle the error */ });
因此,我們可以在入口文件統(tǒng)一這樣做:
class App extends Component { constructor(props) { super(props); // ... this.stream = null; this.video = null; // ... } componentDidMount() { // ... this.startMedia(); } startMedia = () => { const constraints = { audio: false, video: true, }; navigator.mediaDevices.getUserMedia(constraints) .then(this.handleSuccess) .catch(this.handleError); } handleSuccess = (stream) => { this.stream = stream; // 獲取視頻流 this.video.srcObject = stream; // 傳給 video } handleError = (error) => { console.log("navigator.getUserMedia error: ", error); } // ... }實(shí)時(shí)展示
為什么需要 this.video 呢,我們不僅要展示拍攝到的視頻流,還要能直觀的將女神的面部神情標(biāo)記出來(lái),因此需要通過(guò) canvas 來(lái)同時(shí)實(shí)現(xiàn)展示視頻流和繪制基本圖形這兩點(diǎn),而連接這兩點(diǎn)的方法如下:
canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height);
當(dāng)然,我們并不需要在視圖中真的提供一個(gè) video DOM,而是由 App 維護(hù)在實(shí)例內(nèi)部即可。canvas.width 和 canvas.height 需要考慮移動(dòng)端設(shè)備的尺寸,這里略去不表。
而繪制矩形框與文字信息則非常簡(jiǎn)單,我們只需要拿到算法模型計(jì)算出的位置信息即可:
export const drawBox = ({ ctx, x, y, w, h, emoji }) => { ctx.strokeStyle = EmojiToColor[emoji]; ctx.lineWidth = "4"; ctx.strokeRect(x, y, w, h); } export const drawText = ({ ctx, x, y, text }) => { const padding = 4 ctx.fillStyle = "#ff6347" ctx.font = "16px" ctx.textBaseline = "top" ctx.fillText(text, x + padding, y + padding) }核心二:模型預(yù)測(cè)
在這里,我們需要將問(wèn)題進(jìn)行拆解。鑒于本文所說(shuō)的「識(shí)別女神表情背后的情緒」屬于圖像分類問(wèn)題,那么這個(gè)問(wèn)題就需要我們完成兩件事:
從圖像中提取出人臉部分的圖像;
將提取出的圖像塊作為輸入交給模型進(jìn)行分類計(jì)算。
下面我們來(lái)圍繞這兩點(diǎn)逐步討論。
人臉提取我們將借助 face-api.js 來(lái)處理。face-api.js 是基于 tensorflow.js 核心 API (@tensorflow/tfjs-core) 來(lái)實(shí)現(xiàn)的在瀏覽器環(huán)境中使用的面部檢測(cè)與識(shí)別庫(kù),本身就提供了
SSD Mobilenet V1、Tiny Yolo V2、MTCNN 這三種非常輕量的、適合移動(dòng)設(shè)備使用的模型。很好理解的是效果自然是打了不少折扣,這些模型都從模型大小、計(jì)算復(fù)雜度、機(jī)器功耗等多方面做了精簡(jiǎn),盡管有些專門用來(lái)計(jì)算的移動(dòng)設(shè)備還是可以駕馭完整模型的,但我們一般的手機(jī)是肯定沒(méi)有卡的,自然只能使用 Mobile 版的模型。
這里我們將使用 MTCNN。我們可以小瞄一眼模型的設(shè)計(jì),如下圖所示??梢钥吹剑覀兊膱D像幀會(huì)被轉(zhuǎn)換成不同 size 的張量傳入不同的 net,并做了一堆 Max-pooling,最后同時(shí)完成人臉?lè)诸悺b box 的回歸與 landmark 的定位。大致就是說(shuō),輸入一張圖像,我們可以得到圖像中所有人臉的類別、檢測(cè)框的位置信息和比如眼睛、鼻子、嘴唇的更細(xì)節(jié)的位置信息。
當(dāng)然,當(dāng)我們使用了 face-api.js 時(shí)就不需要太仔細(xì)的去考慮這些,它做了較多的抽象與封裝,甚至非常兇殘的對(duì)前端同學(xué)屏蔽了張量的概念,你只需要取到一個(gè) img DOM,是的,一個(gè)已經(jīng)加載好 src 的 img DOM 作為封裝方法的輸入(加載 img 就是一個(gè) Promise 咯),其內(nèi)部會(huì)自己轉(zhuǎn)換成需要的張量。通過(guò)下面的代碼,我們可以將視頻幀中的人臉提取出來(lái)。
export class FaceExtractor { constructor(path = MODEL_PATH, params = PARAMS) { this.path = path; this.params = params; } async load() { this.model = new faceapi.Mtcnn(); await this.model.load(this.path); } async findAndExtractFaces(img) { // ...一些基本判空保證在加載好后使用 const input = await faceapi.toNetInput(img, false, true); const results = await this.model.forward(input, this.params); const detections = results.map(r => r.faceDetection); const faces = await faceapi.extractFaces(input.inputs[0], detections); return { detections, faces }; } }情緒分類
好了,終于到了核心功能了!一個(gè)「好」習(xí)慣是,扒一扒 GitHub 看看有沒(méi)有開(kāi)源代碼可以參考一下,如果你是大佬請(qǐng)當(dāng)我沒(méi)說(shuō)。這里我們將使用一個(gè)實(shí)時(shí)面部檢測(cè)和情緒分類模型來(lái)完成我們的核心功能,這個(gè)模型可以區(qū)分開(kāi)心、生氣、難過(guò)、惡心、沒(méi)表情等。
對(duì)于在瀏覽器中使用 TensorFlow.js 而言,很多時(shí)候我們更多的是應(yīng)用現(xiàn)有模型,通過(guò) tfjs-converter 來(lái)將已有的 TensorFlow 的模型、Keras 的模型轉(zhuǎn)換成 tfjs 可以使用的模型。值得一提的是,手機(jī)本身集成了很多的傳感器,可以采集到很多的數(shù)據(jù),相信未來(lái)一定有 tfjs 發(fā)揮的空間。具體轉(zhuǎn)換方法可參考文檔,我們繼續(xù)往下講。
那么我們可以像使用 face-api.js 一樣將 img DOM 傳入模型嗎?不行,事實(shí)上,我們使用的模型的輸入并不是隨意的圖像,而是需要轉(zhuǎn)換到指定大小、并只保留灰度圖的張量。因此在繼續(xù)之前,我們需要對(duì)原圖像進(jìn)行一些預(yù)處理。
哈哈,躲得了初一躲不過(guò)十五,我們還是來(lái)了解下什么是張量吧!TensorFlow 的官網(wǎng)是這么解釋的:
張量是對(duì)矢量和矩陣向潛在的更高維度的泛化。TensorFlow 在內(nèi)部將張量表示為基本數(shù)據(jù)類型的 n 維數(shù)組。
算了沒(méi)關(guān)系,我們畫(huà)個(gè)圖來(lái)理解張量是什么樣的:
因此,我們可將其簡(jiǎn)單理解為更高維的矩陣,并且存儲(chǔ)的時(shí)候就是個(gè)數(shù)組套數(shù)組。當(dāng)然,我們通常使用的 RGB 圖像有三個(gè)通道,那是不是就是說(shuō)我們的圖像數(shù)據(jù)就是三維張量(寬、高、通道)了呢?也不是,在 TensorFlow 里,第一維通常是 n,具體來(lái)說(shuō)就是圖像個(gè)數(shù)(更準(zhǔn)確的說(shuō)法是 batch),因此一個(gè)圖像張量的 shape 一般是 [n, height, width, channel],也即四維張量。
那么我們要怎么對(duì)圖像進(jìn)行預(yù)處理呢?首先我們將分布在 [0, 255] 的像素值中心化到 [-127.5, 127.5],然后標(biāo)準(zhǔn)化到 [-1, 1]即可。
const NORM_OFFSET = tf.scalar(127.5); export const normImg = (img, size) => { // 轉(zhuǎn)換成張量 const imgTensor = tf.fromPixels(img); // 從 [0, 255] 標(biāo)準(zhǔn)化到 [-1, 1]. const normalized = imgTensor .toFloat() .sub(NORM_OFFSET) // 中心化 .div(NORM_OFFSET); // 標(biāo)準(zhǔn)化 const { shape } = imgTensor; if (shape[0] === size && shape[1] === size) { return normalized; } // 按照指定大小調(diào)整 const alignCorners = true; return tf.image.resizeBilinear(normalized, [size, size], alignCorners); }
然后將圖像轉(zhuǎn)成灰度圖:
export const rgbToGray = async imgTensor => { const minTensor = imgTensor.min() const maxTensor = imgTensor.max() const min = (await minTensor.data())[0] const max = (await maxTensor.data())[0] minTensor.dispose() maxTensor.dispose() // 灰度圖則需要標(biāo)準(zhǔn)化到 [0, 1],按照像素值的區(qū)間來(lái)標(biāo)準(zhǔn)化 const normalized = imgTensor.sub(tf.scalar(min)).div(tf.scalar(max - min)) // 灰度值取 RGB 的平均值 let grayscale = normalized.mean(2) // 擴(kuò)展通道維度來(lái)獲取正確的張量形狀 (h, w, 1) return grayscale.expandDims(2) }
這樣一來(lái),我們的輸入就從 3 通道的彩色圖片變成了只有 1 個(gè)通道的黑白圖。
注意,我們這里所做的預(yù)處理比較簡(jiǎn)單,一方面我們?cè)诒苊馊ダ斫庖恍┘?xì)節(jié)問(wèn)題,另一方面也是因?yàn)槲覀兪窃谑褂靡呀?jīng)訓(xùn)練好的模型,不需要做一些復(fù)雜的預(yù)處理來(lái)改善訓(xùn)練的效果。
準(zhǔn)備好圖像后,我們需要開(kāi)始準(zhǔn)備模型了!我們的模型主要需要暴露加載模型的方法 load 和對(duì)圖像進(jìn)行分類的 classify 這兩個(gè)方法。加載模型非常簡(jiǎn)單,只需要調(diào)用 tf.loadModel 即可,需要注意的是,加載模型是一個(gè)異步過(guò)程。我們使用 create-react-app 構(gòu)建的項(xiàng)目,封裝的 Webpack 配置已經(jīng)支持了 async-await 的方法。
class Model { constructor({ path, imageSize, classes, isGrayscale = false }) { this.path = path this.imageSize = imageSize this.classes = classes this.isGrayscale = isGrayscale } async load() { this.model = await tf.loadModel(this.path) // 預(yù)熱一下 const inShape = this.model.inputs[0].shape.slice(1) const result = tf.tidy(() => this.model.predict(tf.zeros([1, ...inShape]))) await result.data() result.dispose() } async imgToInputs(img) { // 轉(zhuǎn)換成張量并 resize let norm = await prepImg(img, this.imageSize) // 轉(zhuǎn)換成灰度圖輸入 norm = await rgbToGrayscale(norm) // 這就是所說(shuō)的設(shè)置 batch 為 1 return norm.reshape([1, ...norm.shape]) } async classify(img, topK = 10) { const inputs = await this.imgToInputs(img) const logits = this.model.predict(inputs) const classes = await this.getTopKClasses(logits, topK) return classes } async getTopKClasses(logits, topK = 10) { const values = await logits.data() let predictionList = [] for (let i = 0; i < values.length; i++) { predictionList.push({ value: values[i], index: i }) } predictionList = predictionList .sort((a, b) => b.value - a.value) .slice(0, topK) return predictionList.map(x => { return { label: this.classes[x.index], value: x.value } }) } } export default Model
我們可以看到,我們的模型返回的是一個(gè)叫 logits 的量,而為了知道分類的結(jié)果,我們又做了 getTopKClasses 的操作。這可能會(huì)使得較少了解這塊的同學(xué)有些困惑。實(shí)際上,對(duì)于一個(gè)分類模型而言,我們返回的結(jié)果并不是一個(gè)特定的類,而是對(duì)各個(gè) class 的概率分布,舉個(gè)例子:
// 示意用 const classifyResult = [0.1, 0.2, 0.25, 0.15, 0.3];
也就是說(shuō),我們分類的結(jié)果其實(shí)并不是說(shuō)圖像中的東西「一定是人或者狗」,而是「可能是人或者可能是狗」。以上面的示意代碼為例,如果我們的 label 對(duì)應(yīng)的是 ["女人", "男人", "大狗子", "小狗子", "二哈"],那么上述的結(jié)果其實(shí)應(yīng)該理解為:圖像中的物體 25% 的可能性為大狗子,20% 的可能性為一個(gè)男人。
因此,我們需要做 getTopKClasses,根據(jù)我們的場(chǎng)景我們只關(guān)心最可能的情緒,那么我們也就會(huì)取 top1 的概率分布值,從而知道最可能的預(yù)測(cè)結(jié)果。
怎么樣,tfjs 封裝后的高級(jí)方法是不是在語(yǔ)義上較為清晰呢?
最終我們將上文提到的人臉提取功能與情緒分類模型整合到一起,并加上一些基本的 canvas 繪制:
// 略有調(diào)整 analyzeFaces = async (img) => { // ... const faceResults = await this.faceExtractor.findAndExtractFaces(img); const { detections, faces } = faceResults; // 對(duì)提取到的每一個(gè)人臉進(jìn)行分類 let emotions = await Promise.all( faces.map(async face => await this.emotionModel.classify(face)) ); // ... } drawDetections = () => { const { detections, emotions } = this.state; if (!detections.length) return; const { width, height } = this.canvas; const ctx = this.canvas.getContext("2d"); const detectionsResized = detections.map(d => d.forSize(width, height)); detectionsResized.forEach((det, i) => { const { x, y } = det.box const { emoji, name } = emotions[i][0].label; drawBox({ ctx, ...det.box, emoji }); drawText({ ctx, x, y, text: emoji, name }); }); }
大功告成!
實(shí)時(shí)性優(yōu)化事實(shí)上,我們還應(yīng)該考慮的一個(gè)問(wèn)題是實(shí)時(shí)性。事實(shí)上,我們的計(jì)算過(guò)程用到了兩個(gè)模型,即便已經(jīng)是針對(duì)移動(dòng)設(shè)備做了優(yōu)化的精簡(jiǎn)模型,但仍然會(huì)存在性能問(wèn)題。如果我們?cè)诮M織代碼的時(shí)候以阻塞的方式進(jìn)行預(yù)測(cè),那么就會(huì)出現(xiàn)一幀一幀的卡頓,女神的微笑也會(huì)變得抖動(dòng)和僵硬。
因此,我們要考慮做一些優(yōu)化,來(lái)更好地畫(huà)出效果。
筆者這里利用一個(gè) flag 來(lái)標(biāo)記當(dāng)前是否有正在進(jìn)行的模型計(jì)算,如果有,則進(jìn)入下一個(gè)事件循環(huán),否則則進(jìn)入模型計(jì)算的異步操作。同時(shí),每一個(gè)事件循環(huán)都會(huì)執(zhí)行 canvas 操作,從而保證標(biāo)記框總是會(huì)展示出來(lái),且每次展示的其實(shí)都是緩存在 state 中的前一次模型計(jì)算結(jié)果。這種操作是具有合理性的,因?yàn)槿四樀囊苿?dòng)通常是連續(xù)的(如果不連續(xù)這個(gè)世界可能要重新審視一下),這種處理方法能較好的對(duì)結(jié)果進(jìn)行展示,且不會(huì)因?yàn)槟P陀?jì)算而阻塞,導(dǎo)致卡頓,本質(zhì)上是一種離散化采樣的技巧吧。
handleSnapshot = async () => { // ... 一些 canvas 準(zhǔn)備操作 canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height); this.drawDetections(); // 繪制 state 中維護(hù)的結(jié)果 // 利用 flag 判斷是否有正在進(jìn)行的模型預(yù)測(cè) if (!this.isForwarding) { this.isForwarding = true; const imgSrc = await getImg(canvas.toDataURL("image/png")); this.analyzeFaces(imgSrc); } const that = this; setTimeout(() => { that.handleSnapshot(); }, 10); } analyzeFaces = async (img) => { // ...其他操作 const faceResults = await this.models.face.findAndExtractFaces(img); const { detections, faces } = faceResults; let emotions = await Promise.all( faces.map(async face => await this.models.emotion.classify(face)) ); this.setState( { loading: false, detections, faces, emotions }, () => { // 獲取到新的預(yù)測(cè)值后,將 flag 置為 false,以再次進(jìn)行預(yù)測(cè) this.isForwarding = false; } ); }效果展示
我們來(lái)在女神這試驗(yàn)下效果看看:
嗯,馬馬虎虎吧!雖然有時(shí)候還是會(huì)把笑容識(shí)別成沒(méi)什么表情,咦,是不是 Gakki 演技還是有點(diǎn)…好了好了,時(shí)間緊迫,趕緊帶上武器準(zhǔn)備赴約吧。穿上一身帥氣格子衫,打扮成程序員模樣~
結(jié)尾約會(huì)當(dāng)晚,吃著火鍋唱著歌,在下與女神相談甚歡。正當(dāng)氣氛逐漸曖昧,話題開(kāi)始深入到感情方面時(shí),我自然的問(wèn)起女神的理想型。萬(wàn)萬(wàn)沒(méi)想到,女神突然說(shuō)了這樣的話:
那一刻我想起了 Eason 的歌:
Lonely Lonely christmas參考
Merry Merry christmas
寫了卡片能寄給誰(shuí)
心碎的像街上的紙屑
https://developer.mozilla.org...
https://github.com/webrtc/ada...
https://github.com/justadudew...
https://github.com/tensorflow...
[Zhang K, Zhang Z, Li Z, et al. Joint face detection and alignment using multitask cascaded convolutional networks[J]. IEEE Signal Processing Letters, 2016, 23(10): 1499-1503.][7]
文章可隨意轉(zhuǎn)載,但請(qǐng)保留此 原文鏈接。非常歡迎有激情的你加入 ES2049 Studio,簡(jiǎn)歷請(qǐng)發(fā)送至 caijun.hcj(at)alibaba-inc.com 。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/100361.html
摘要:差分編碼的目的,就是盡可能的將圖片數(shù)據(jù)值轉(zhuǎn)換成一組重復(fù)的低的值,這樣的值更容易被壓縮。最后還要注意的是,差分編碼處理的是每一個(gè)的像素點(diǎn)中每條顏色通道的值,紅綠藍(lán)透明四個(gè)顏色通道的值分別進(jìn)行處理。 背景 今天凌晨一點(diǎn),突然有個(gè)人加我的qq,一看竟然是十年前被我刪掉的初戀。。。。 因?yàn)橹霸趒q空間有太多的互動(dòng),所以qq推薦好友里面經(jīng)常推薦我倆互相認(rèn)識(shí)。。。。謎之尷尬 showImg(ht...
摘要:差分編碼的目的,就是盡可能的將圖片數(shù)據(jù)值轉(zhuǎn)換成一組重復(fù)的低的值,這樣的值更容易被壓縮。最后還要注意的是,差分編碼處理的是每一個(gè)的像素點(diǎn)中每條顏色通道的值,紅綠藍(lán)透明四個(gè)顏色通道的值分別進(jìn)行處理。 背景 今天凌晨一點(diǎn),突然有個(gè)人加我的qq,一看竟然是十年前被我刪掉的初戀。。。。 因?yàn)橹霸趒q空間有太多的互動(dòng),所以qq推薦好友里面經(jīng)常推薦我倆互相認(rèn)識(shí)。。。。謎之尷尬 showImg(ht...
摘要:差分編碼的目的,就是盡可能的將圖片數(shù)據(jù)值轉(zhuǎn)換成一組重復(fù)的低的值,這樣的值更容易被壓縮。最后還要注意的是,差分編碼處理的是每一個(gè)的像素點(diǎn)中每條顏色通道的值,紅綠藍(lán)透明四個(gè)顏色通道的值分別進(jìn)行處理。 背景 今天凌晨一點(diǎn),突然有個(gè)人加我的qq,一看竟然是十年前被我刪掉的初戀。。。。 因?yàn)橹霸趒q空間有太多的互動(dòng),所以qq推薦好友里面經(jīng)常推薦我倆互相認(rèn)識(shí)。。。。謎之尷尬 showImg(ht...
摘要:小明追女神的故事小明遇到了他的女神,打算送一朵鮮花來(lái)表白。剛好小明打聽(tīng)到他和有一個(gè)共同的好友,于是小明決定讓來(lái)替自己來(lái)完成這件事。 1.單例模式 單例模式的核心:(1)確保只有一個(gè)實(shí)例(2)提供全局訪問(wèn) 用代理實(shí)現(xiàn)單例模式: var ProxySingletonCreateDiv = (function(){ var instance; return f...
閱讀 1633·2021-11-17 09:33
閱讀 1330·2021-10-11 10:59
閱讀 2971·2021-09-30 09:48
閱讀 1976·2021-09-30 09:47
閱讀 3097·2019-08-30 15:55
閱讀 2400·2019-08-30 15:54
閱讀 1549·2019-08-29 15:25
閱讀 1710·2019-08-29 10:57