2024年8月

开场白

虽然在实际的开发中我们很少去绘制流程图
就算需要,我们也会通过第3方插件去实现
下面我们来简单实现流程图中很小的一部分
手动绘制矩形

绘制一个矩形的思路

我们这里绘制矩形
会使用到canvas.strokeRect(x,y, w, h)方法绘制一个描边矩形
x:矩形起点的 x 轴坐标。
y:矩形起点的 y 轴坐标。
width:矩形的宽度。正值在右边,负值在左边。
height:矩形的高度。正值下降,负值上升。
注意一下w,h这两个参数的正直和负值。
如果w,h是负数,会出现了2个斜着对称的矩形

绘制一个静态矩形

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    html,body{
      /* 去除浏览器内置的margin */
      margin: 0;
      /* 整个页面铺满 */
      height: 100%;
      width: 100%;
    }
  </style>
</head>
<body>
  <canvas id="canvas"></canvas>
</body>
<script>
  // 获取canvas元素
  let canvasEle = document.getElementById('canvas')
  // 获取canvas的上下文
  const ctx = canvasEle.getContext('2d')
  // 设置canvas的大小(宽高) 与屏幕一样宽高
  const screenSize = document.documentElement
  canvasEle.width =  window.screen.availWidth
  canvasEle.height =  window.screen.availHeight
  // 给矩形的设置颜色,设置颜色一定要在绘制之前,否则将会不生效
  ctx.strokeStyle = '#a0a'
  // 绘制一个路径模式是的矩形,起始坐标100,100, 宽300,高280
  ctx.strokeRect(100,100,300,280)
</script>
</html>

手动绘制矩形的基本思路

通过上面我们实现了静态绘制矩形。
如果我们想要手动画一个矩形,需要实现以下几个步骤
1.给canvas注册鼠标按下事件,在按下的时候记录矩形的起始坐标(x,y)
与此同时,还需要在按下时注册鼠标移动事件和抬起事件。
2.在鼠标移动的时候,通过计算得到矩形的宽和高。
计算矩形的宽度和高度时,我们要使用绝对值进行计算。
计算后立即绘制矩形
3.在鼠标抬起时,移除之前注册的鼠标移动事件和抬起事件

手动绘制矩形

<script>
// 获取canvas元素 oCan
let canvasEle = document.getElementById('canvas')
// 获取canvas的上下文
const ctx = canvasEle.getContext('2d')
// 设置canvas的大小(宽高) 与屏幕一样宽高
const screenSize = document.documentElement
canvasEle.width =  window.screen.availWidth
canvasEle.height =  window.screen.availHeight
// 所有的矩形信息
let rectArr = []
// 给canvas注册事件按下事件
canvasEle.addEventListener('mousedown',canvasDownHandler)
function canvasDownHandler(e){
  console.log('按下', e)
  rectArr = [e.clientX,e.clientY ]
  // 按下的时候需要注册移动事件
  canvasEle.addEventListener('mousemove', canvasMoveHandler)
  // 抬起事件
  canvasEle.addEventListener('mouseup', canvasMouseUpHandler)
}
// 移动的时候我们需要记录起始点和结束点,然后就可以绘制矩形了
function canvasMoveHandler(e){
  console.log('我们在移动了', e)
  // 在移动的时候就开始绘制矩形
  drawRect(rectArr[0], rectArr[1], e.clientX, e.clientY)
}

function drawRect(x1,y1,x2,y2){
  // 在计算矩形的宽高时,我们需要使用绝对值来进行计算
  // 此时此刻移动的坐标减去最初按下的坐标就是矩形的宽和高
  // 如果不用绝对值,可能会出现2个矩形
  let rectWidth = Math.abs(x2-x1)
  let rectHeight = Math.abs(y2-y1)
  // 存储矩形的坐标信息
  rectArr = [x1,y1,rectWidth,rectHeight]
  ctx.strokeRect(...rectArr)
}
// 当我们鼠标抬起的时候要移除之前注册移动事件和抬起事件
function canvasMouseUpHandler(){
  canvasEle.removeEventListener('mousemove', canvasMoveHandler)
  canvasEle.removeEventListener('mouseup', canvasMouseUpHandler)
}
</script>


发现问题出现多余的路径

通过上面这张图片,我们虽然绘制手动绘制出了矩形。
但是出现了重复的路径。
我们先分析一下出现重复路径的原因。
我们在每次移动的过程中,都会绘制矩形。
只要我们在绘制前,清空矩形是不是就可以解决这个问题
我们来尝试一下

在绘之前清除多余的路径

function drawRect(x1,y1,x2,y2){
  // 在计算矩形的宽高时,我们需要使用绝对值来进行计算
  // 此时此刻移动的坐标减去最初按下的坐标就是矩形的宽和高
  let rectWidth = Math.abs(x2-x1)
  let rectHeight = Math.abs(y2-y1)
  // 存储矩形的坐标信息
  rectArr = [x1,y1,rectWidth,rectHeight]
  // 在绘制矩形前,我们将矩形清空,然后在绘制,就不会出现多余的路径了
  ctx.clearRect(0,0,  canvasEle.width, canvasEle.height)
  // 重新绘制矩形
  ctx.strokeRect(...rectArr)
}

机智的小伙伴发现问题了?

有些机智的小伙伴发现:
如果我起始点是右下角,终点是左上角。
即:用户从(900, 1000)拖动到(50, 50)这种情况
按照这样的方向绘制矩形,是不是会出现问题呢?
确实会出现问题。
此时绘制的矩形不会随着我们的方向进行绘制。请看下面的图
如何处理这个问题呢?

主角闪亮登场 canvas.rect

canvas.rect(x,y,w,h)该方法创建一个矩形路径
x:矩形起点的 x 轴坐标。
y:矩形起点的 y 轴坐标。
width:矩形的宽度。正值在右边,负值在左边。
height:矩形的高度。正值下降,负值上升。
这个方法是创建一个矩形路径,创建的路径并不会直接绘制在画布上。
需要调用stroke()或fill()才能显示在画布上。

使用canvas.rect 绘制矩形路径

function drawRect(x1,y1,x2,y2){
  let rectWidth = Math.abs(x2-x1)
  let rectHeight = Math.abs(y2-y1)
  // 绘制之前先清空之前实时移动产生的多余的矩形路径
  ctx.clearRect(0,0,  canvasEle.width, canvasEle.height)
  // 开始画线
  ctx.beginPath();  
  // 绘制路径矩形
  ctx.rect(Math.min(x1, x2), Math.min(y1, y2), rectWidth, rectHeight);  
  // 绘制形状的轮廓。
  ctx.stroke(); 
}

canvas.strokeRect与canvas.rect的异同

区别1功能性: canvas.strokeRect:绘制的是边框。canvas.rect创建矩形的路径(不绘立即绘制在矩形上)
区别2即时性: canvas.strokeRect是立即绘制。canvas.rect不是立即绘制,需要调用stroke()或fill()才能绘制
相同点:
1.都是绘制矩形
2.接受的参数相同
3.都是通过strokeStyle(颜色)和lineWidth(线的粗细)等来设置样式

连续绘制多个矩形

function drawRect(x1,y1,x2,y2){
  let rectWidth = Math.abs(x2-x1)
  let rectHeight = Math.abs(y2-y1)
  let endX = Math.min(x1, x2)
  let endY = Math.min(y1, y2)
  // 绘制之前先清空之前实时移动产生的多余的矩形路径
  ctx.clearRect(0,0,  canvasEle.width, canvasEle.height)
  // 绘制之前那些存储在 beforeRectArr 数组中的矩形
  allRectInfoArr = [endX, endY, rectWidth, rectHeight]
  ctx.clearRect(0,0,  canvasEle.width, canvasEle.height)
  beforeRectArr.forEach(element => {
    ctx.beginPath(); 
    ctx.strokeRect(...element)
    ctx.stroke();
  });
  
  // 开始本次路径
  ctx.beginPath(); 
  // 绘制本次的矩形路径
  ctx.rect(...allRectInfoArr); 
  // 开始填充矩形
  ctx.stroke(); 
}
// 当我们鼠标抬起的时候要移除之前注册移动事件和抬起事件
function canvasMouseUpHandler(){
  savaBeforeRect()
  canvasEle.removeEventListener('mousemove', canvasMoveHandler)
  canvasEle.removeEventListener('mouseup', canvasMouseUpHandler)
}
function savaBeforeRect(){
  beforeRectArr.push(allRectInfoArr)
}

全部代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    html,body{
      /* 去除浏览器内置的margin */
      margin: 0;
      /* 整个页面铺满 */
      height: 100%;
      width: 100%;
    }
  </style>
</head>
<body>
  <canvas id="canvas"></canvas>
</body>
<script>
  // 获取canvas元素 oCan
  let canvasEle = document.getElementById('canvas')
  // 获取canvas的上下文
  const ctx = canvasEle.getContext('2d')
  // 设置canvas的大小(宽高) 与屏幕一样宽高
  const screenSize = document.documentElement
  canvasEle.width =  window.screen.availWidth
  canvasEle.height =  window.screen.availHeight
  // 给矩形的设置颜色
  // ctx.strokeStyle = '#a0a'
  // // 绘制一个路径模式是的矩形,起始坐标100,100, 宽300,高280
  // ctx.strokeRect(100,100,400,280)
  // 矩形信息
  let rectArr = []
  // 所有的矩形信息
  allRectInfoArr = []
  let beforeRectArr =[]
  // 给canvas注册事件按下事件
  canvasEle.addEventListener('mousedown',canvasDownHandler)
  function canvasDownHandler(e){
    rectArr = [e.clientX,e.clientY ]
    // 按下的时候需要注册移动事件
    canvasEle.addEventListener('mousemove', canvasMoveHandler)
    // 抬起事件
    canvasEle.addEventListener('mouseup', canvasMouseUpHandler)
  }
  // 移动的时候我们需要记录起始点和结束点,然后就可以绘制矩形了
  function canvasMoveHandler(e){
    // 在移动的时候就开始绘制矩形
    drawRect(rectArr[0], rectArr[1], e.clientX, e.clientY)
  }

  function drawRect(x1,y1,x2,y2){
    let rectWidth = Math.abs(x2-x1)
    let rectHeight = Math.abs(y2-y1)
    let endX = Math.min(x1, x2)
    let endY = Math.min(y1, y2)
    // 绘制之前先清空之前实时移动产生的多余的矩形路径
    ctx.clearRect(0,0,  canvasEle.width, canvasEle.height)
    // 绘制之前那些存储在 beforeRectArr 数组中的矩形
    allRectInfoArr = [endX, endY, rectWidth, rectHeight]
    beforeRectArr.forEach(element => {
      ctx.beginPath(); 
      ctx.strokeRect(...element)
      ctx.stroke();
    });
    
    // 开始本次路径
    ctx.beginPath(); 
    // 绘制本次的矩形路径
    ctx.rect(...allRectInfoArr); 
    // 开始填充矩形
    ctx.stroke(); 
  }
  // 当我们鼠标抬起的时候要移除之前注册移动事件和抬起事件
  function canvasMouseUpHandler(){
    savaBeforeRect()
    canvasEle.removeEventListener('mousemove', canvasMoveHandler)
    canvasEle.removeEventListener('mouseup', canvasMouseUpHandler)
  }

  function savaBeforeRect(){
    beforeRectArr.push(allRectInfoArr)
  }
</script>
</html>

尾声

如果小伙伴觉得我写的不错的话
可以给我点个赞吗?感谢了。
后面会继续写:
如何选中矩形,更改矩形大小。
如何在矩形上添加文字。
如何绘制圆,箭头符号等

前言

实验室信息管理系统,即 LIMS(Laboratory Information Management System),它是由计算机和应用软件组成,能够完成实验室数据和信息的收集、分析、报告和管理。早期的 LIMS 系统大多基于计算机局域网,专门针对一个实验室的整体环境而设计,是一个包括了信号采集设备、数据通讯软件、数据库管理软件在内的高效集成系统。

LIMS 系统以实验室为中心,将实验室的业务流程、环境、人员、仪器设备、标物标液、化学试剂、标准方法、图书资料、文件记录、客户管理等等影响分析的数据因素有机结合起来,采用先进的计算机网络技术,数据库技术和标准化的实验室管理思想,组成一个全面、规范的管理体系,为实现分析数据网上调度、分析数据自动采集、快速分布、信息共享、分析报告无纸化、质量保证体系顺利实施、成本严格控制、人员量化考核、实验室管理水平整体提高等各方面提供技术支持,是连接实验室、采样现场、监管部门及客户信息的信息平台,同时引入先进的数理统计技术,如方差分析、相关和回归分析、显著性检验、累积和控制图、抽样检验等,协助职能部门及时发现和控制影响产品质量的关键因素。

LIMS 系统中面临的痛点问题:

痛点一:

LIMS 系统在报表分析中经常会遇见这样的问题:实验模板的报表行数与列数是固定的,但是检测物和检测方法的数量是不固定的,举个简单的例子,如下图所示,【离子色谱测定原始记录】表中检测方法共有 20 行,检测物共 7 列。

但是在实际的使用中,检测方法数据有 24 行,检测物有 7 列,这个时候就无法满足报表的现有行列模板模式,像上面这个例子数据少的,可以人工手动进行修改,但是当数据过大时(几千甚至几万列数据),人工修改就会很浪费时间,且费时费力。

痛点二:

配置在单元格中的公式也需要自动扩展,公式引用的单元格也需要相对变化:

解决方法

面对这样需求,可以借助葡萄城的
SpreadJS在线表格编辑器
来解决这些痛点问题:

1.首先通过数据绑定完成固定单元格的数据渲染和填报:

2. 面对行列总数超过模板总数这样的问题,实现的思路就是把检测数据根据模板的行列总数进行分割,根据分割的数据,复制现有模板到新的sheet中,将分割的数据放到新的sheet中实现分页,当然实际业务中某些行列

3.公式函数通过api填充实现:

核心代码分享

1.根据模板行列总数,动态处理数据源:

// 根据模板行列总数,动态处理数据源
        processBindData() {
            this.dataSource = JSON.parse(JSON.stringify(data));
            delete this.dataSource.test;
            let test1, test2, test3, test4
            // 实际项目中需要根据模板行列动态判断是否需要分页,本示例中20行,7列
            if (data.test.length > 20 && Object.keys(data.test[0]).length > 7) {
                test1 = data.test.slice(0, 20);
                test1.forEach((item, index) => {
                    test1[index] = pickByIndex(item, 0, 6)
                })
                this.dataSource.test1 = test1;
                test2 = data.test.slice(0, 9).concat(data.test.slice(20, data.test.length));
                test2.forEach((item, index) => {
                    test2[index] = pickByIndex(item, 0, 6)
                })
                this.dataSource.test2 = test2;
                test3 = data.test.slice(0, 20);
                test3.forEach((item, index) => {
                    test3[index] = Object.assign(pickByIndex(item, 0, 1), pickByIndex(item, 7, 8))
                })
                this.dataSource.test3 = test3;
                test4 = data.test.slice(0, 9).concat(data.test.slice(20, data.test.length));
                test4.forEach((item, index) => {
                    test4[index] = Object.assign(pickByIndex(item, 0, 1), pickByIndex(item, 7, 8))
                })
                this.dataSource.test4 = test4;
                console.log(this.dataSource);
                this.templatePagination();
            }
            // })
        },
2. 根据处理好的数据,动态生成新的sheet,动态填充数据
 // 根据处理好的数据,动态生成新的sheet,动态填充数据
        templatePagination() {
            let sheetJSON = this.spread.getSheet(0).toJSON();
            for (let i = 1; i < 5; i++) {
                if (i == 1) {
                    this.spread.removeSheet(0)
                }
                let newSheet = new GC.Spread.Sheets.Worksheet("第" + i + "页");
                this.spread.addSheet(this.spread.getSheetCount(), newSheet);
                newSheet.fromJSON(sheetJSON);
                newSheet.name("第" + i + "页");
                newSheet.setValue(1, 6, "共4页,第" + i + "页");
                var tableColumns = [],
                    names = Object.keys(this.dataSource[test${i}][0]),
                    labels = Object.keys(this.dataSource[test${i}][0]);
                var table = newSheet.tables.add(test${i}, 6, 0, this.dataSource[test${i}].length + 1, Object.keys(this.dataSource[test${i}][0]).length);
                table.autoGenerateColumns(false);
                names.forEach(function (name, index) {
                    var tableColumn = new GC.Spread.Sheets.Tables.TableColumn();
                    tableColumn.name(labels[index]);
                    tableColumn.dataField(name);
                    tableColumns.push(tableColumn);
                });

                table.bindColumns(tableColumns);
                table.bindingPath(test${i});
                table.style(GC.Spread.Sheets.Tables.TableThemes.none);
                table.filterButtonVisible(false);
                let source = new GC.Spread.Sheets.Bindings.CellBindingSource(this.dataSource);
                newSheet.setDataSource(source);
                const sRange = new GC.Spread.Sheets.Range(7, 3, 1, 1);
                const fRange = new GC.Spread.Sheets.Range(7, 3, this.dataSource[test${i}].length, 1);
                newSheet.fillAuto(sRange, fRange, {
                    fillType: GC.Spread.Sheets.Fill.FillType.auto,
                    series: GC.Spread.Sheets.Fill.FillSeries.column,
                    fillDirection: GC.Spread.Sheets.Fill.FillDirection.down,
                });
                newSheet.options.showZeros = false;
            }
        },

2.公式函数填充逻辑:

const sRange = new GC.Spread.Sheets.Range(7, 3, 1, 1);
                const fRange = new GC.Spread.Sheets.Range(7, 3, this.dataSource[test${i}].length, 1);
                newSheet.fillAuto(sRange, fRange, {
                    fillType: GC.Spread.Sheets.Fill.FillType.auto,
                    series: GC.Spread.Sheets.Fill.FillSeries.column,
                    fillDirection: GC.Spread.Sheets.Fill.FillDirection.down,
                });

最后附上完整的代码链接

https://gitee.com/GrapeCity/lims

扩展链接:

【干货放送】财务报表勾稽分析要点,一文读尽!

为什么你的财务报表不出色?推荐你了解这四个设计要点和!

纯前端类 Excel 表格控件在报表勾稽分析领域的应用场景解析

痞子衡嵌入式半月刊: 第 106 期

这里分享嵌入式领域有用有趣的项目/工具以及一些热点新闻,农历年分二十四节气,希望在每个交节之日准时发布一期。

本期刊是开源项目(GitHub:
JayHeng/pzh-mcu-bi-weekly
),欢迎提交 issue,投稿或推荐你知道的嵌入式那些事儿。

上期回顾

《痞子衡嵌入式半月刊: 第 105 期》

唠两句

历史上的今天:1997年8月15,Linux平台上主流的桌面环境GNOME创始人 Miguel de Icaza 通过电子邮件宣布GNOME问世。

本期共 4 个项目、1 个工具,希望对你有帮助!

项目类

1、Grbl - 开源嵌入式G代码解析器

Grbl 是一个高性能,低成本的以并行端口为基础的 CNC 运动控制器固件。它可以运行在 Arduino (Duemillanove/Uno) 上。控制器是用高度优化的 C 语言编写的,利用 avr 芯片的外设功能来实现精确的定时和异步操作。它能够保持高达 30kHz 的稳定,无抖动控制脉冲。

Grbl 接受符合标准的 G 代码,并已与几个 CAM 工具的输出进行了测试。弧,圆和螺旋运动完全支持,以及所有其他主要的 G 代码命令。

2、grblHAL - 可定制的CNC控制器解决方案

grblHAL 是针对 32 位处理器的 Grbl v1.1 的重写版。引入硬件抽象层(HAL)是为了将核心 Grbl 代码与处理器/硬件特定代码(驱动编码器)完全分离。驱动程序代码通过函数指针从核心访问,驱动程序通过 HAL 结构中的这些和标志宣布实现的功能。

目前有超过 13 种处理器(包含 RT1062、STM32F4、RP2040、LPC1700 等)驱动程序可供使用,这些驱动程序的功能取决于可用资源,如 GPIO 引脚数量和处理器外设。驱动程序通过HAL结构“告诉”内核什么是可用的,内核相应地调整自己。编写额外的驱动程序相对容易,因为不需要对核心进行修改——驱动程序可以在单个单独的文件中实现。

3、PicoCNC - 4轴CNC运动控制器

PicoCNC 是一个控制子板,可与 grblHAL 和 Rapsberry Pi Pico 微控制器一起,为路由器、铣床、激光和车床等设备创建了一个强大的 CNC 运动控制器。软件基于 grblHAL,它构成了具有先进功能的运动控制系统的基础。

PicoCNC 特性包括:

- 通过螺钉端子和销头支持多达4轴
- 主轴控制:PWM (5 v)、方向(5 v)、0-10V、支持12V PWM(通过开路集电极输出)
- 每个轴增加限位开关
- 包含继电器的支持
- 隔离标准Grbl输入控制: 循环启动,饲料保持,停止和安全门
- 光隔离探头输入

4、linux-ch32v003 - 在ch32v003单片机上运行Linux

这个项目实现了在 CH32V003 微控制器上运行 Linux 操作系统。它通过使用一个 8MB 的 SPI PSRAM 芯片和一个 RISC-V 模拟器(mini-rv32ima)来实现这一点。模拟器是必需的,因为 PSRAM 不能映射到微控制器的地址空间。Linux 内核和 rootfs 在引导时从 SD 卡加载到 PSRAM 中,文件系统基于 FatFs 库。

工具类

1、exe_to_dll - 将EXE转换为DLL的小工具

这个小工具可以转换 EXE,以便它可以像 DLL 一样加载。工具支持 32 位和 64 位 DLL。

欢迎订阅

文章会同时发布到我的
博客园主页

CSDN主页

知乎主页

微信公众号
平台上。

微信搜索"
痞子衡嵌入式
"或者扫描下面二维码,就可以在手机上第一时间看了哦。

《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,全书共分10章,第1章主要让读者认识数据资产,了解数据资产相关的基础概念,以及数据资产的发展情况。第2~8章主要介绍大数据时代数据资产管理所涉及的核心技术,内容包括元数据的采集与存储、数据血缘、数据质量、数据监控与告警、数据服务、数据权限与安全、数据资产管理架构等。第9~10章主要从实战的角度介绍数据资产管理技术的应用实践,包括如何对元数据进行管理以发挥出数据资产的更大潜力,以及如何对数据进行建模以挖掘出数据中更大的价值。

图书介绍:
数据资产管理核心技术与应用

今天主要是给大家分享一下第四章的内容:

第四章的标题为数据质量的技术实现

内容思维导图如下:

在数据资产管理中,除了元数据和数据血缘外,数据质量也是很重要的一个环节,如下图所示,数据质量通常是指在数据处理的整个生命周期中,能否始终保持数据的完整性、一致性、准确性、可靠性、及时性等,我们只有知道了数据的质量,才能在数据质量差的时候,能去改进数据。《数据资产管理核心技术与应用》读书笔记-第四章:数据质量的技术实现

《数据资产管理核心技术与应用》读书笔记-第四章:数据质量的技术实现

  • 完整性:数据是否有丢失,比如数据字段、数据量是否有丢失。
  • 一致性:数据值是否完全一致,比如小数数据的精度是否出现丢失。
  • 准确性:数据含义是否准确,比如数据字段注释是否准确。
  • 可靠性:比如数据存储是否可靠,是否做了数据灾备等。
  • 及时性:数据是否出现延迟或者堵塞导致没有及时入数据仓库或者数据湖。

正是因为数据质量的重要性,所以在国际上有专门对数据质量进行国际标准定义,比如ISO 8000数据质量系列国际标准中就详细的描述了数据质量如何衡量以及如何进行认证等,包含了数据质量的特性、特征以及如何进行数据质量的管理、评估等。在ISO 8000中共发布了21个标准,在网址:
https://std.samr.gov.cn/gj/std?op=ISO中可以查询到ISO 8000
质量标准,如下4-0-2所示

和数据质量相关的主要内容包括如下:

  • 1)、ISO 8000-1:2022 Data quality-Part 1: Overview
  • 2)、ISO 8000-2:2022 Data quality-Part 2: Vocabulary
  • 3)、ISO 8000-8:2015 Data quality-Part 8: Information and data quality: Concepts and measuring
  • 4)、ISO/TS 8000-60:2017 Data quality-Part 60:Data quality management: Overview
  • 5)、ISO 8000-61:2016 Data quality-Part 61: Data quality management: Process reference model
  • 6)、ISO 8000-62:2018 Data quality-Part 62: Data quality management: Organizational process maturity assessment: Application of standards relating to process assessment
  • 7)、ISO 8000-63:2019 Data quality-Part 63: Data quality management: Process measurement
  • 8)、ISO 8000-64:2022 Data quality-Part 64: Data quality management: Organizational process maturity assessment: Application of the Test Process Improvement method
  • 9)、ISO 8000-65:2020 Data quality -Part 65:Data quality management: Process measurement questionnaire
  • 10)、ISO 8000-66:2021 Data quality-Part 66: Data quality management: Assessment indicators for data processing in manufacturing operations
  • 11)、ISO/TS 8000-81:2021 Data quality-Part 81: Data quality assessment: Profiling
  • 12)、ISO/TS8000-82:2022 Data quality-Part 82:Data quality assessment: Creating data rules
  • 13)、ISO 8000-100:2016 Data quality-Part 100: Master data: Exchange of characteristic data: Overview
  • 14)、ISO 8000-110:2021 Data quality-Part 110: Master data: Exchange of characteristic data: Syntax, semantic encoding, and conformance to data specification
  • 15)、ISO 8000-115:2018 Data quality-Part 115: Master data: Exchange of quality identifiers: Syntactic, semantic and resolution requirements
  • 16)、ISO 8000-116:2019 Data quality-Part 116: Master data: Exchange of quality identifiers: Application of ISO 8000-115 to authoritative legal entity identifiers
  • 17)、ISO 8000-120:2016 Data quality -Part 120: Master data: Exchange of characteristic data: Provenance
  • 18)、ISO 8000-130:2016 Data quality-Part 130: Master data: Exchange of characteristic data: Accuracy
  • 19)、ISO 8000-140:2016 Data quality- Part 140: Master data: Exchange of characteristic data: Completeness
  • 20)、ISO 8000-150:2022 Data quality -Part 150: Data quality management: Roles and responsibilities
  • 21)、ISO/TS 8000-311:2012 Data quality-Part 311: Guidance for the application of product data quality for shape (PDQ-S)

1、质量数据采集的技术实现

不管是在数据仓库还是数据湖中,一开始我们都是不知道数据的质量情况的,需要通过一定的规则定期的到数据湖或者数据仓库中去采集数据的质量,这个规则是允许用户自己去进行配置的,通常的流程如下图所示。

对于一些通用的规则,可以做成规则模板,然后用户可以直接选择某个规则进行质量数据采集,常见的通用规则如下表所示。

《数据资产管理核心技术与应用》读书笔记-第四章:数据质量的技术实现

规则

描述

表字段的空值率

采集指定表的指定字段为空的比率

表字段的异常率

采集指标表的指定字段值的异常率,比如性别字段,只可能为男或者女,对于别的值就是异常值,我们可以根据规则统计出异常值的比率,哪些值是异常值当然也需要支持自定义维护

表字段数据格式异常率

采集指标表的指定字段值的数据格式异常率,比如时间格式或者手机号格式不符合指定规则的就是异常数据,我们可以计算出这些格式异常的比率

表字段数据的重复率

采集指定表的指定字段值的重复率,比如某些字段的值是不允许重复的,出现重复时就是异常

表字段的缺失率

采集指定表的字段数量是否和预期的字段数量一致,如果不一致,就是出现了字段缺失,就可以统计出字段的缺失率

表数据入库的及时率

采集指定的表数据的入库时间和当前系统时间的差异,然后来计算出数据的及时性以及及时率

表记录的丢失率

1、 采集指定的表数据的记录数,然后和预期的数据量或者源表中的数据量进行比较,计算出数据记录的丢失率

2、 采集指定的表数据的记录数,然后和周或者月平均值进行比较,判断数据记录数是否低于正常标准,从而判断是否存在丢失。

除了通用规则外,肯定还需要支持自定义的规则,自定义的规则可以允许用户自己编写SQL脚本、Python语言脚本或者scala 语言脚本。

  • SQL脚本:一般是指通过JDBC的方式直接提交和运行SQL脚本从而获取数据质量结果,常见的关系型数据库,如MySQL、SQLServer等都是支持JDBC的,并且Hive也是支持JDBC连接的,另外还可以通过SparkSQL  Job的方式来运行SQL脚本,如下图所示。

总结下来就是:如果数据库或者数据仓库本身支持JDBC 协议,那么可以直接通过JDBC协议运行SQL语句。如果不支持的话,那么可以通过SparkSQL job的方式进行过渡,SparkSQL 本身支持连接到Hive、Hudi等数据仓库或者数据湖,也支持通过JDBC的方式连接到其他的数据库。在官方网站地址:
https://spark.apache.org/docs/latest/sql-data-sources-jdbc.html
中有明确的介绍,如下图所示。

  • Python脚本:Python 是一种常用的脚本语言,由于SQL 脚本只支持一些直接用SQL语句就可以查询到的数据结果,对于一些复杂的场景或者SQL语句无法支持的场景,可以使用Python脚本,并且Spark也是支持Python语言的,如下图所示。

PySpark的相关介绍可以参考网址:
https://spark.apache.org/docs/latest/api/python/index.html
,如下图所示。

《数据资产管理核心技术与应用》读书笔记-第四章:数据质量的技术实现

  • Scala脚本:Spark 底层本身主要是通过Scala语言编写的代码实现的,很多大数据开发者都很热衷于使用Scala语言,所以对于Spark Job 采集数据质量时,也可以编写Scala脚本,如下图所示。

对于采集质量数据时,定时Job的技术选型,笔者在这里推荐Apache DolphinSchedur这个大数据任务调度平台。Apache DolphinSchedur是一个分布式、易于扩展的可视化工作流任务调度开源平台,解决了复杂的大数据任务依赖关系,并支持在各种大数据应用程序的DataOPS中任意编排任务节点之间的关联关系其以定向非循环图(DAG)流模式组装任务,可以及时监控任务的执行状态,并支持重试、指定节点恢复失败、暂停、恢复和终止任务等操作。官方网址为:
https://dolphinscheduler.apache.org/en-us
,如下图所示。

Apache DolphinSchedur 支持二次开发,其Github地址为:
https://github.com/apache/dolphinscheduler

相关的部署文档的地址为:
https://dolphinscheduler.apache.org/en-us/docs/3.2.0/installation_menu

如下图所示,为官方在网址
https://dolphinscheduler.apache.org/en-us/docs/3.2.0/architecture/design
中提供的技术实现架构图。

从图中可以看到,其支持SQL、Python、Spark等任务节点,正好是我们所需要的,而且该平台是支持分布式部署和调度的,所以不存在任何的性能瓶颈,因为分布式系统支持横向或者纵向的扩展。

Apache DolphinSchedur 还提供了API的方式进行访问,官方API文档地址为:https://dolphinscheduler.apache.org/en-us/docs/3.2.0/guide/api/open-api。

最终采集质量数据的技术实现架构图如下图所示。

未完待续......《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,读书笔记-第四章:数据质量的技术实现.

DETR
能够消除物体检测中许多手工设计组件的需求,同时展示良好的性能。但由于注意力模块在处理图像特征图方面的限制,
DETR
存在收敛速度慢和特征分辨率有限的问题。为了缓解这些问题,论文提出了
Deformable DETR
,其注意力模块仅关注参考点周围的一小组关键采样点,通过更少的训练次数实现比
DETR
更好的性能

来源:晓飞的算法工程笔记 公众号

论文: Deformable DETR: Deformable Transformers for End-to-End Object Detection

Introduction


现代物体检测器采用许多手工制作的组件,例如锚点生成、基于规则的训练目标分配、非极大值抑制 (
NMS
) 后处理,导致其并不是完全端到端的。
DETR
的提出消除了对此类手工制作组件的需求,并构建了第一个完全端到端的物体检测器。
DETR
采用简单的架构,结合卷积神经网络 (
CNN
) 和
Transformer
编码器-解码器,利用
Transformer
的多功能且强大的关系建模功能,达到了很不错的性能。

尽管
DETR
具有有趣的设计和良好的性能,但它也有自己的问题:(1)需要更长的训练周期才能收敛。(2)
DETR
在检测小物体方面的性能相对较低,没有利用多尺度特征。

上述问题主要归因于
Transformer
组件在处理图像特征图方面的缺陷。在初始化时,注意力模块将几乎统一的注意力权重投射到特征图中的所有像素。长时间的训练对于注意力权重学习如何关注稀疏的有意义的位置是必要的。另一方面,
Transformer
编码器中的注意力权重计算与像素成二次计算度。因此,处理高分辨率特征图的计算和存储复杂度非常高。

在图像领域,可变形卷积是处理稀疏空间位置的强大而有效的机制,自然就避免了上述问题。但它缺乏元素关系建模机制,而这正是
DETR
成功的关键。

在本文中,论文提出了
Deformable DETR
,结合可变形卷积的稀疏空间采样和
Transformers
的关系建模能力,缓解了
DETR
收敛速度慢和计算复杂度高的问题。可变形注意模块仅关注一小组采样位置,相当于所有特征图像素中突出关键元素的预过滤器。该模块可以自然地扩展到多尺度特征架构,而无需
FPN
的帮助。在
Deformable DETR
中,论文利用(多尺度)可变形注意力模块来代替处理特征图的
Transformer
注意力模块,如图 1 所示。

Revisiting Transformers and DETR


Multi-Head Attention in Transformers.

定义
\(q\in\Omega_{q}\)
为查询元素下标,索引特征
\({z}_{q}\in {\mathbb{R}}^C\)

\(k\in\Omega_{k}\)
为键元素下标,索引特征
\(x\_k \in \mathbb{R}^C\)

\({C}\)
是特征维度,
\(\Omega_{q}\)

\(\Omega\_{k}\)
分别为查询元素和键元素的集合。

多头注意力特征的计算可表示为:

\[
\mathrm{MultiHeadAttn}(z_{q},x)=\sum_{m=1}^{M}W_{m}[\sum_{k\in\Omega_{k}}A_{m q k}\cdot W_{m}^{\prime}x_{k}],

\quad\quad (1)

\]

其中
\(m\)
为注意力头下标,
\(W_{m}^{\prime}\in\mathbb{R}^{C_{v}\times C}\)

\(W_{m}\in{\mathbb{R}^{{C}\times C_{v}}}\)
为可学习的权重(默认
\({C}_{v}=C/M\)
)。注意力权重
\(A_{m q k}\propto{exp}\lbrace\frac{z_{q}^{T}\,U_{m}^{T}\,\,V_{m}\,x_{k}}{\sqrt{C_{v}}}\rbrace\)
归一化为
\(\sum_{k\in\Omega_k}A_{mqk}=1\)
,其中
\(U_{m},V_{m}\in\mathbb{R}^{C_{v}\times C}\)
也是可学习的权重。为了区别不同的空间位置,特征
\({z}_{q}\)

\({z}\_{k}\)
通常是元素内容和位置嵌入的串联或求和。

Transformer
有两个已知问题:1)收敛需要很长的训练周期。2)多头注意力的计算和内存复杂度可能非常高。

DETR

DETR
建立在
Transformer
编码器-解码器架构之上,与基于集合的匈牙利损失相结合,通过二分匹配强制对每个
GT
的边界框进行预测。对
DETR
不熟悉的,可以看看之前的文章,【
DETR:Facebook提出基于Transformer的目标检测新范式 | ECCV 2020 Oral
】。

给定
CNN
主干网提取的输入特征图
\(x\in\mathbb{R}^{C\times H\times W}\)

DETR
利用标准
Transformer
编码器-解码器架构将输入特征图转换为一组对象查询的特征。在对象查询特征(由解码器产生)之上添加一个 3 层前馈神经网络(
FFN
)和一个线性投影作为检测头。
FFN
充当回归分支来预测边界框坐标
\(b\in 0, 1^4\)
,其中
\(b = {b_{x},b_{y},b_{w},b_{h}}\)
编码归一化的框中心坐标、框高度和框宽度(相对于图像大小),线性投影则作为分类分支来产生分类结果。

对于
DETR
中的
Transformer
编码器,查询元素和键元素都是主干网络特征图中的像素(带有编码的位置嵌入)。

对于
DETR
中的
Transformer
解码器,输入包括来自编码器的特征图和由可学习位置嵌入表示的
N
个对象查询。解码器中有两种类型的注意力模块,即交叉注意力模块和自注意力模块。

  • 在交叉注意力模块中,查询元素为学习到的对象查询,而键元素是编码器的输出特征图。
  • 在自注意力模块中,查询元素和键元素都是对象查询,从而捕获它们的关系。

DETR
是一种极具吸引力的物体检测设计,无需许多手工设计的组件,但也有自己的问题:1)由于计算复杂度限制其可使用分辨率的大小,导致
Transformer
在检测小物体方面的性能相对较低。2)因为处理图像特征的注意力模块很难训练,
DETR
需要更多的训练周期才能收敛。

METHOD


Deformable Transformers for End-to-End Object Detection

  • Deformable Attention Module

在图像特征图上应用注意力计算的核心问题是,它会遍历所有的空间位置。为了解决这个问题,论文提出了一个可变形的注意力模块。受可变形卷积的启发,可变形注意力模块仅关注参考点周围的一小组关键采样点,而不管特征图的空间大小。如图 2 所示,通过为每个查询元素仅分配少量的键元素,可以缓解收敛慢和特征空间分辨率大的问题。

给定输入特征图
\(x\in\mathbb{R}^{C\times H\times W}\)

\(q\)
为查询元素的下标,对应内容特征
\({z}_{q}\)
和二维参考点
\({p}_{q}\)
,可变形注意力特征的计算如下

\[
\mathrm{DeformAttn}(z_{q},p_{q},x)=\sum_{m=1}^{M}W_{m}\sum_{k=1}^{K}A_{m q k}\cdot W_{m}^{\prime}x(p_{q}+\Delta p\_{m q k}),

\quad\quad (2)

\]

其中
\(m\)
为注意力头下标,
\(k\)
为采样点下标,
\(K\)
为采样点总数(
\(K\ll H W\)
)。
\({\Delta}p_{mqk}\)

\(A_{m q k}\)
表示第
k
个采样点的采样偏移及其使用的在第
m
个头中的注意力权重。注意力权重
\(A_{m q k}\)

\(0,1\)
范围内,由
\(\sum_{k=1}^{K}A_{m q k} = 1\)
归一化。
\({\Delta}p_{mqk}\in \mathbb{R}^{2}\)
是无约束范围的二维实数,由于
\(p_{q} + \Delta p_{mqk}\)
是小数,需要应用双线性插值。
\(\Delta p_{m q k}\)

\(A_{mqk}\)
均通过对查询特征
\({z}_{q}\)
的线性投影获得的。在实现中,查询特征
\(z_{q}\)
被输入到
\(3MK\)
通道的线性投影运算符,其中前
\(2MK\)
通道对
\({\Delta}P_{m q k}\)
采样偏移进行编码,剩余的
\(MK\)
通道输入到
\(Softmax\)
运算符以获得
\(A_{m q k}\)
注意力权重。

定义
\(N_{q}\)
为查询元素的数量,当
\(M K\)
相对较小时,可变形注意力模块的复杂度为
\(O(2N_{q}C^{2}+\operatorname\*{min}(H W C^{2},N_{q}K C^{2}))\)
。当应用于__
DETR
__编码器时,其中
\(N_{q}=H W\)
,复杂度变为
\(O(H W C^{2})\)
,与空间大小成线性复杂度。当应用于
DETR
解码器中的交叉注意模块时,其中
\(N\_{q}=N\)

\(N\)
是对象查询的数量),复杂度变为
\(O(NKC^2)\)
,这与空间大小
\(HW\)
无关。

  • Multi-scale Deformable Attention Module

大多数现代目标检测框架都受益于多尺度特征图,论文提出的可变形注意模块也可以自然地扩展到多尺度特征图。

定义
\(\left{x^{l}\right}^{L}_{l=1}\)
为输入的多尺度特征图,其中
\(x^{l}\in \mathbb{R}^{C\times H_{l}\times W_{l}}\)
。定义
\({\hat{p}}_{q}\in0,1^{2}\)
为每个查询元素
\(q\)
对应的参考点的归一化坐标,多尺度可变形注意模块的计算为:

\[
\mathrm{MSDeformAttn}(z_{q},\hat{p}_{q},{x^{l}}_{l=1}^{L})=\sum_{m=1}^{M}W_{m}\bigl[\sum_{l=1}^{L}\sum_{k=1}^{K}A_{m l q k}\cdot W_{m}^{\prime}x^{l}(\phi_{l}(\hat{p}_{q})+\Delta p_{m l q k}\bigr)\bigr],

\quad\quad (3)

\]

其中
\(m\)
为注意力头下标,
\(l\)
为输入特征级别下标,
\(k\)
为采样点下标。
\(\Delta p_{mlqk}\)

\(A_{mlqk}\)
表示第
\({{k}}^{th}\)
个采样点在第
\({{l}}^{th}\)
个特征级别和第
\({{m}}^{th}\)
个注意头中的采样偏移和注意力权重,其中标量注意力权重
\(A_{mlqk}\)

\(\sum^L_{l=1}\sum^K_{k=1}A_{mlqk}=1\)
归一化。为了缩放方便,使用归一化的坐标
\({\hat{p}}_{q}\in0,1^{2}\)
,其中
\((0,0)\)

\((1,1)\)
分别表示图像的左上角和右下角。公式 3 中的函数
\(\phi_{l}{({\hat{p}}_{q})}^{\cdot}\)
将归一化坐标
\({\hat{p}}_{q}\)
重新缩放到第
\({l}^{th}\)
级别的输入特征图的坐标。多尺度可变形注意力与之前的单尺度版本非常相似,只是它从多尺度特征图中采样
\(LK\)
个点,而不是仅从单尺度特征图中采样
\(K\)
个点。


\(L=1\)

\(K=1\)
以及将
\(W^{'}_{m}\in \mathbb{R}^{{C}_{v}\times C}\)
固定为单位矩阵时,论文所提出的注意力模块即退化为可变形卷积。

可变形卷积是针对单尺度输入而设计的,每个注意力头仅关注一个采样点,而论文的多尺度可变形注意力会关注来自多尺度输入的多个采样点。(多尺度)可变形注意模块也可以被视为
Transformer
注意力的有效变体,可变形采样位置相当于引入预过滤机制。当采样点为所有位置时,可变形注意力模块相当于
Transformer
注意力。

  • Deformable Transformer Encoder


DETR
中处理特征图的注意力模块替换为多尺度可变形注意力模块,编码器的输入和输出都是具有相同分辨率的多尺度特征图。


ResNet

\(C_3\)

\(C\_5\)
阶段的输出特征图,通过
\(1\times 1\)
卷积提取多尺度特征图
\(\left{x^{l}\right}_{l=1}^{L-1}\)
(
\(L=4\)
),其中
\(C_{l}\)
的分辨率为输入图像的
\(2^{l}\)
倍降采样。最低分辨率特征图
\(x^{L}\)
是通过对
\(C\_5\)
阶段的输出进行步幅为 2 的
\(3\ \times\ 3\)
卷积获得,表示为
\(C_{6}\)
阶段。所有多尺度特征图都是
\(C=256\)
通道。这里没有使用类似
FPN
的自上而下结构,因为论文提出的多尺度可变形注意力本身就可以在多尺度特征图之间交换信息,添加
FPN
并不会提高性能。

在编码器中应用多尺度可变形注意力模块时,输出是与输入具有相同分辨率的多尺度特征图,键和查询元素都是来自多尺度特征图的像素。对于每个查询像素,参考点是其本身。为了确定每个查询像素位于哪个特征级别,除了位置嵌入之外,还在特征中添加了尺度级别嵌入
\(e\_{l}\)
。与固定编码的位置嵌入不同,尺度级嵌入是随机初始化并与网络联合训练的。

  • Deformable Transformer Decoder

解码器中有交叉注意力和自注意力模块,两种类型的注意力模块的查询元素都是对象查询。在交叉注意力模块中,键元素是编码器的输出特征图,对象查询通过与特征图交互提取特征。而在自注意力模块中,键元素也是对象查询,对象查询即相互交互提取特征。

由于可变形注意模块的设计初衷是将卷积特征图作为键元素,因此论文仅将交叉注意模块替换为多尺度可变形注意模块,保持自注意模块不变。对于每个对象查询,参考点
\({\hat{p}}\_{q}\)
的二维归一化坐标是通过带
\(\mathrm{sigmoid}\)
函数的可学习线性投影从对象查询嵌入中预测的。

由于多尺度可变形注意模块提取参考点周围的图像特征,论文将参考点作为边界框中心的初始猜测,然后检测头预测边的相对偏移量。这样,不仅能够降低优化难度,还能让解码器注意力将与预测的边界框具有很强的相关性,加速训练收敛。

Additional Improvements and Variants for Deformable DETR

由于其快速收敛以及高效率的计算,可变形
DETR
为各种端到端目标检测器的变体提供了可能性,比如:

  • Iterative Bounding Box Refinement:通过级联的方式,每层解码器优化前一层的预测结果。
  • Two-Stage Deformable DETR:通过两阶段检测的方式,选择第一阶段预测的高分区域提案作为第二阶段解码器的对象查询。

EXPERIMENT


表 1 展示了与
Faster R-CNN
+
FPN

DETR
的性能对比。

表 2 列出了所提出的可变形注意模块的各种设计选择的消融实验。

表 3 与其他最先进的方法进行了比较。

如果本文对你有帮助,麻烦点个赞或在看呗~undefined更多内容请关注 微信公众号【晓飞的算法工程笔记】