"use strict"
* @深度学习的全局状态变量及通用方法
*/
* @库导入
*/
import * as tf from "@tensorflow/tfjs"
import * as tfvis from "@tensorflow/tfjs-vis"
import * as XLSX from "xlsx"
import { shallowRef } from "vue"
* @全局状态变量
* tfBackendRef tf后端加载状态
*/
* @tf后端加载状态
* @const { ShallowRef }
*/
export const tfBackendRef = shallowRef(null)
* @全局通用方法
* goToPage() 前往新页面
* paramQuery() 把传参对象解析成url传参字符串格式,未导出
* findDom() 获取某个DOM元素对象
* wait() 等待,未导出
* downloadFile() 下载文件
* arrayTranspose() 数组转置
* showVisor() 打开图表栏
* downloadJson() 将JSON对象下载为js文件。(可能会废弃)
* downloadTrainHistoryToXlsx() 将训练数据下载为excel文件
* showModelSummary() 可视化模型概要
* showModelLayers() 可视化模型各层训练结果
* aoaTotfvisChart() 用tfvis渲染Aoa数据为图表的封装方法
* downloadExampleFile() 下载示例波谱文件
* readXlsxFile() 读取XLSX文件为工作簿对象
* sheetToDataArr() 从工作表提取原始数据的方法
* arrRangeAve() 数组从小到大序列的某个范围内的均值,未导出
* minToZero() 零位校正
* maxToOne() 归一化,即上限归一
*/
* @goToPage 前往新页面
* @function
* @param { string } localPage 本地页面的url路径及传参。可以是相对路径,即最后一个节点;也可以是绝对路径,即pages开始。
* @param { object } [param] 额外传参,会加到localPage里进行传递。必须是封装的object对象。
* @param { boolean } [ifNavigate] 额外传参,是否必须保留当前页面。默认为true。
*/
export function goToPage(localPage, param, ifNavigate) { try {
if (param && (typeof param === "object")) {
const paramQueryString = paramQuery(param)
const regex = /\?/
if (regex.test(localPage)) {
localPage = `${localPage}&${paramQueryString}`
} else {
localPage = `${localPage}?${paramQueryString}`
}
}
if (ifNavigate === false) {
uni.redirectTo({ url: localPage })
} else {
uni.navigateTo({ url: localPage })
}
} catch (error) {
console.error("goToPage()报错:", error)
}}
* @paramQuery 把传参对象解析成url传参字符串格式
* main模块全局方法调用的内部方法。
* @function
* @param { object } paramObject 本地页面的传参,必须是object对象。
* @return { string } 返回的字符串。
*/
function paramQuery(paramObject) { try {
const keys = Object.keys(paramObject)
const paramStrings = keys.map((key) => {
return `${ encodeURIComponent(key) }=${ encodeURIComponent(paramObject[key]) }`
})
const paramQuery = paramStrings.join("&")
return paramQuery
} catch (error) {
console.error("paramQuery()报错:", error)
}}
* @findDom 获取某个DOM元素对象
* uni-app会对<video>、<canvas>进行封装,所以需要找到DOM。
* @function async
* @param { string } parentElementId 父元素的ID。
* @param { string } elementTagName 要获取的元素的标签名。
* @returns { Promise<HTMLElement> } 返回元素对象。
*/
export async function findDom(parentElementId, elementTagName) { try {
let parentElement = document.getElementById(parentElementId)
while (!parentElement) {
await wait(100)
parentElement = document.getElementById(parentElementId)
}
let element = parentElement.getElementsByTagName(elementTagName)[0]
while (!element) {
await wait(100)
element = parentElement.getElementsByTagName(elementTagName)[0]
}
return element
} catch (error) {
console.error("findDom()报错:", error)
}}
* @wait 等待方法
* @function async 不导出
* @param { number } ms 等待时间,单位是毫秒
* @returns { Promise<void> } 单纯的等待
*/
export async function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
* @downloadFile 下载文件
* @function
* @param { ArrayBuffer } dataObj Buffer格式的数据对象。
* @param { String } fileName 文件名(含扩展名)。
*/
export function downloadFile(dataObj, fileName) { try {
const dataBlob = new Blob([dataObj], { type: "application/octet-stream" })
let downloadLink = document.getElementById("just-for-download")
if (!downloadLink) {
downloadLink = document.createElement("a")
downloadLink.setAttribute("id", "justForDownload")
downloadLink.style.display = "none"
downloadLink.download = fileName
}
downloadLink.href = URL.createObjectURL(dataBlob)
downloadLink.click()
} catch (error) {
console.error("downloadFile()报错: ", error)
}}
* @arrayTranspose 数组转置
* @function
* @param { Array[] } aoa AOA二维数组。
* @returns { Array[] } 转置后的AOA二维数组。
*/
export function arrayTranspose(aoa) { try {
const rowNumber = aoa.length
const colNumber = aoa[0].length
const transposedAoa = []
for (let j = 0; j < colNumber; j++) {
transposedAoa[j] = []
for (let i = 0; i < rowNumber; i++) {
transposedAoa[j][i] = aoa[i][j]
}
}
return transposedAoa
} catch (error) {
console.error("arrayTranspose()报错: ", error)
}}
* @showVisor 打开图表栏
* @function
*/
export function showVisor() { try {
if (!tfvis.visor().isOpen()) {
tfvis.visor().open()
}
} catch (error) {
console.error("showVisor()报错:", error)
}}
* @downloadJson 将JSON对象下载为js文件
* @function
* @param { JSON } datasetJson 数据集对象。
* @param { String } datasetName 数据集的名称。
* @note 数据集对象必须得是JSON化的。
*/
export function downloadJson(datasetJson, datasetName) { try {
let datasetStr = `export const ${ datasetName } = ${ JSON.stringify(datasetJson) }`
downloadFile(datasetStr, "export-dataset.js")
} catch (error) {
console.error("downloadJson()报错:", error)
}}
* @downloadTrainHistoryToXlsx 将训练数据下载为excel文件
* @function
* @note 前端网页方法,node不适用。
* @param { tf.History } trainHistory 训练数据记录。
*/
export function downloadTrainHistoryToXlsx(trainHistory) { try {
let historyJson = []
for (let i = 0; i < trainHistory.params.epochs; i++) {
let uniHistoryJson = { "epoch": i + 1 }
for (let key of trainHistory.params.metrics) {
uniHistoryJson[key] = trainHistory.history[key][i]
}
historyJson.push(uniHistoryJson)
}
const sheet = XLSX.utils.json_to_sheet(historyJson)
let workbook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workbook, sheet, "tfjs-data")
const xlsxObj = XLSX.write(workbook, { type: "buffer" })
const xlsxBlob = new Blob([xlsxObj], { type: "application/octet-stream" })
let downloadLink = document.getElementById("justForDownload")
if (!downloadLink) {
downloadLink = document.createElement("a")
downloadLink.setAttribute("id", "justForDownload")
downloadLink.style.display = "none"
downloadLink.download = "train-log.xlsx"
}
downloadLink.href = URL.createObjectURL(xlsxBlob)
downloadLink.click()
} catch (error) {
console.error("downloadTrainHistoryToXlsx()报错: ", error)
}}
* @showModelSummary 可视化模型概要
* @function
* @param { tf.LayersModel } model 模型。
* @param { string } tableName 表格名(表头)。
*/
export function showModelSummary(model, tableName) { try {
tfvis.show.modelSummary(
{
tab: "模型概要",
name: tableName,
},
model
)
} catch (error) {
console.error("showModelSummary()报错: ", error)
}}
* @showModelLayers 可视化模型各层参数
* @function
* @param { tf.LayersModel } model 模型。
* @param { string } tableTab 表格标签。
*/
export function showModelLayers(model, tableTab) { try {
for (let key in model.layers) {
if (
model.layers[key].kernel
|| (model.layers[key].cell && model.layers[key].cell.kernel)
) {
tfvis.show.layer(
{
tab: tableTab,
name: `${ model.layers[key].name }层参数`,
},
model.layers[key]
)
}
}
} catch (error) {
console.error("showModelLayers()报错: ", error)
}}
* @aoaTotfvisChart 用tfvis渲染Aoa数据为图表的封装方法
* 直接给x和y的数组即可
* @function
* @param { String } chartKind 图表类别,有"linechart"、"scatterplot"
* @param {{
* tab: String,
* name: String,
* }} container 容器对象
* @param {{
* x?: Number[],
* y: Number[],
* }[] } dataAoa 用于生成图表的AOA处理的数组
* @param { String[] } [serieTagArr] 序列名称数组
* @param { Object } [visOptions] 可视化选项
* @example
* ```js
* // 绘图预览
* aoaTotfvisChart(
* // 图表类型
* "scatterplot",
* // 图表容器
* { tab: "测试数据", name: "原始数据" },
* // 图表数据
* [
* { x: x, y: y },
* { x: x, y: corrected },
* { x: x, y: baseline },
* ],
* // 数据序列标签
* ["原始数据", "校正", "基线"],
* )
* ```
*/
export function aoaTotfvisChart(chartKind, container, dataAoa, serieTagArr, visOptions) { try {
const dataJsonAoa = []
const visOptionObj = {
zoomToFit: (visOptions && visOptions.zoomToFit) ? visOptions.zoomToFit : true,
...visOptions
}
for (let i = 0; i < dataAoa.length; i++) {
const dataJsonArr = []
for (let j = 0; j < dataAoa[i].y.length; j++) {
dataJsonArr.push({
x: (dataAoa[i].x) ? dataAoa[i].x[j] : undefined,
y: dataAoa[i].y[j],
})
}
dataJsonAoa.push(dataJsonArr)
}
if (chartKind == "linechart") {
tfvis.render.linechart(
container,
{
values: dataJsonAoa,
series: serieTagArr || undefined,
},
visOptionObj
)
} else if (chartKind == "scatterplot") {
tfvis.render.scatterplot(
container,
{
values: dataJsonAoa,
series: serieTagArr || undefined,
},
visOptionObj
)
}
} catch (error) {
console.error("aoaTotfvisChart()报错: ", error)
}}
* @downloadExampleFile 下载示例波谱文件
* 需要从gsd-data-aoa-example.js文件读取gsdDataAoaExample示例文件对象
* 需要XLSX库
* 需要downloadFile()方法
*/
* @readXlsxFile 读取XLSX文件为工作簿对象
* @function async
* @param { File } xlsxFile XLSX文件对象
* @returns { Promise<XLSX.WorkBook> } 工作簿对象
*/
export async function readXlsxFile(xlsxFile) { try {
const dataBuffer = await xlsxFile.arrayBuffer()
return XLSX.read(dataBuffer, { type: "buffer" })
} catch (error) {
console.error("readXlsxFile()报错: ", error)
}}
* @sheetToDataArr 从工作表提取原始数据的方法
* 含检查、修复、XY配对等
* @function
* @param { XLSX.WorkSheet } workSheet 工作簿表格对象,需要有header行
* @returns {{
* x: Number[],
* y: Number[],
* header: String | Number
* }[] | null } 可用于处理的数组,或无返回,即模态框报错
*/
export function sheetToDataArr(workSheet) { try {
const sheetDataAoa = XLSX.utils.sheet_to_json(workSheet, {
header: 1,
blankrows: false
})
const rawDataHeaderArr = (sheetDataAoa[0][0] === "X") ?
sheetDataAoa[0] : sheetDataAoa[1]
const rawDataAoa = (sheetDataAoa[0][0] === "X") ?
arrayTranspose(sheetDataAoa.slice(1)) : arrayTranspose(sheetDataAoa.slice(2))
const dataJsonArr = []
const rawDataHeaderArrTrimed = rawDataHeaderArr.map((rawDataHeaderArr) => {
if (typeof rawDataHeaderArr === "string") {
return rawDataHeaderArr.trim()
} else {
return rawDataHeaderArr
}
})
let xLabelArr = []
for (let i = 0; i < rawDataAoa.length; i++) {
rawDataAoa[i] = rawDataAoa[i].filter((rawDatum) => (
(rawDatum !== undefined) && (rawDatum !== null) && (rawDatum !== NaN)
))
for (let j = 0; j < rawDataAoa[i].length; j++) {
if (typeof rawDataAoa[i][j] !== "number") {
if (typeof rawDataAoa[i][j] === "string") {
rawDataAoa[i][j] = rawDataAoa[i][j].trim()
}
rawDataAoa[i][j] = Number(rawDataAoa[i][j])
if (rawDataAoa[i][j] === NaN) {
uni.showModal({
showCancel: false,
title: "数据错误",
content: `表格文件第 ${ i + 1 } 列第 ${ j + 3 } 行数据不是有效数字`
})
return
}
}
}
if (rawDataHeaderArrTrimed[i] === "X" || rawDataHeaderArrTrimed[i] === "x") {
if (rawDataAoa[i].length < 9) {
uni.showModal({
showCancel: false,
title: "数据错误",
content: `表格文件第 ${ i + 1 } 列的 X 数据过短,无法拟合`
})
return
} else {
let up = false
let down = false
for (let j = 1; j < rawDataAoa[i].length; j++) {
if (rawDataAoa[i][j - 1] <= rawDataAoa[i][j]) {
up = true
} else if (rawDataAoa[i][j - 1] >= rawDataAoa[i][j]) {
down = true
}
if (down && up) {
uni.showModal({
showCancel: false,
title: "数据错误",
content: `表格文件第 ${ i + 1 } 列的 X 数据不单调`
})
return
}
}
}
xLabelArr = rawDataAoa[i]
} else {
if (xLabelArr.length !== rawDataAoa[i].length) {
uni.showModal({
showCancel: false,
title: "数据错误",
content: `表格文件第 ${ i + 1 } 列数据和其X轴数据数量不一致`
})
return
} else {
dataJsonArr.push({
x: xLabelArr,
y: rawDataAoa[i],
header: rawDataHeaderArrTrimed[i]
})
}
}
}
return dataJsonArr
} catch (error) {
console.error("solveRawData()报错: ", error)
}}
* @arrRangeAve 数组从小到大序列的某个范围内的均值
* @function
* @param { Number[] } arr 完整的数组数据。
* @param { Number } relativeFrom 初始比例,相对值。
* @param { Number } relativeTo 结尾比例,相对值。
* @returns { Number } 返回均值。
*/
function arrRangeAve(arr, relativeFrom, relativeTo) {
const arrDeepCopy = [...arr]
arrDeepCopy.sort((item1, item2) => {
return (item1 - item2)
})
const from = Math.round((arrDeepCopy.length - 1) * relativeFrom)
const to = Math.round((arrDeepCopy.length - 1) * relativeTo)
const arrRange = arrDeepCopy.slice(from, to + 1)
let sum = 0
for (let i = 0; i < arrRange.length; i++) {
sum = sum + arrRange[i]
}
const ave = sum / arrRange.length
return ave
}
* @minToZero 零位校正
* 对于倒置峰,则是上限归1
* 直接改数据,不返回结果
* @function
* @param { Number[] } dataJsonY 单组数据的Y数组。
* @param { Boolean } isMinZoom 是否缩放。
* @param { Boolean } isMaxCriteria 正置or倒置。默认正置。
*/
export function minToZero(dataJsonY, isMinZoom = true, isMaxCriteria = true) { try {
if (isMaxCriteria) {
const yMin = arrRangeAve(dataJsonY, 0.00, 0.01)
if ((yMin > 0) && isMinZoom) {
for (let i = 0; i < dataJsonY.length; i++) {
dataJsonY[i] = dataJsonY[i] / yMin - 1
}
} else {
for (let i = 0; i < dataJsonY.length; i++) {
dataJsonY[i] = dataJsonY[i] - yMin
}
}
} else {
const yMax = arrRangeAve(dataJsonY, 0.99, 1.00)
if (yMax < 1 && isMinZoom) {
for (let i = 0; i < dataJsonY.length; i++) {
dataJsonY[i] = 1 - ((1 - dataJsonY[i]) / (1 - yMax))
}
} else if (yMax > 1 && yMax < 100 && isMinZoom) {
for (let i = 0; i < dataJsonY.length; i++) {
dataJsonY[i] = 100 - ((100 - dataJsonY[i]) / (100 - yMax))
}
} else {
for (let i = 0; i < dataJsonY.length; i++) {
dataJsonY[i] = dataJsonY[i] - yMax + 1
}
}
}
} catch (error) {
console.error("minToZero()报错: ", error)
}}
* @maxToOne 归一化,即上限归一
* 直接改数据,不返回
* @function
* @param { Number[] } dataJsonY 单组数据的Y数组对象。
* @param { Boolean } isMaxCriteria 正置or倒置。
*/
export function maxToOne(dataJsonY, isMaxCriteria = true) { try {
if (isMaxCriteria) {
const yMax = arrRangeAve(dataJsonY, 0.99, 1.00)
for (let i = 0; i < dataJsonY.length; i++) {
dataJsonY[i] = dataJsonY[i] / yMax
}
} else {
const yMax = arrRangeAve(dataJsonY, 0.99, 1.00)
const yMin = arrRangeAve(dataJsonY, 0.00, 0.01)
for (let i = 0; i < dataJsonY.length; i++) {
dataJsonY[i] = 1 - ((yMax - dataJsonY[i]) / (yMax - yMin))
}
}
} catch (error) {
console.error("maxToOne()报错: ", error)
}}