2023年3月

keycloak存到cookie中的值

  • AUTH_SESSION_ID
  • KEYCLOAK_IDENTITY
  • KEYCLOAK_SESSION

AUTH_SESSION_ID

用户的当前session_state,它是会话级的,关闭浏览器就没了

KEYCLOAK_IDENTITY

它是用户跨端登录的基础,它也是一个jwt串,解析后是这样的结果,用户在当前端没有登录时,会跳到kc认证页,当发现cookie里的kc域下有这个
KEYCLOAK_IDENTITY
,会使用这个
session_state
进行认证,没有这个键,KC认证不能完成。

{
    "exp": 1682659005,
    "iat": 1680067005,
    "jti": "d655a51e-f363-43cf-9f3e-1be8c4f7f082",
    "iss": "https://finalcas.pkulaw.com/auth/realms/fabao",
    "sub": "347c9e9e-076c-45e3-be74-c482fffcc6e5",
    "typ": "Serialized-ID",
    "session_state": "4b020044-d273-41c6-9cea-c9ea1b0814f7",
    "state_checker": "SGniuhvr-FbQ7aznFTiTDIi2Gt4CKev7DI3vLNvJufo"
}

注意:如果浏览器的cookie里的KEYCLOAK_IDENTITY丢失了,会导致KC出现无法登录的问题,解决方法只能是清除浏览器里的AUTH_SESSION_ID和KEYCLOAK_SESSION,注意还有后缀为LEGACY的键值.

KEYCLOAK_SESSION

当你可算让KC记住你的登录状态,这时KC会在cookie中生成
KEYCLOAK_SESSION
,它的值默认与AUTH_SESSION_ID相同,当是一个包含过期时间的cookie,浏览器关闭后它依然保持,直到你在KC
记住我
中配置的过期时间。

假设还有第2个、第3个应用,在keycloak的相同的realm中注册了各自的client,我们在访问第2个、第3个应用的时候,也会跳转到keycloak登录页面(带上各自的client_id, redirect_url),但是就像上面的现象一样,我们的keycloak登录页对应的domain/path中有那3个cookie值AUTH_SESSION_ID,KEYCLOAK_IDENTITY和KEYCLOAK_SESSION, 这样就会自动跳转到我们应用的界面,无需填写keycloak登录的账号密码,并且能够返回授权码code,这就算登录成功了,登录成功之后,后续的操作就是我们再利用这个授权码code和client_secret访问keycloak去获取access token,这就是Oauth2.0的授权码模式。

sesssion_state

以上三个被存储在客户端浏览器里的键值( AUTH_SESSION_ID, KEYCLOAK_IDENTITY,KEYCLOAK_SESSION)都有对session_state的存储,只不过,我们存储的有效期不同,AUTH_SESSION_ID是会话级,后两个是与KC后台配置的
记住我
中的refresh_token有效期相同的,即SSO Session Idle, SSO Session Max,Client Session Idle,Client Session Max四个配置,谁小使用谁。

  • 当session_state达到这个refresh_token的超时时间+access_token超时时间后,它会被删除
  • 当用户进行登出操作后,它会被删除
  • 如下配置,用户在
    3+2
    分钟后PM 02:22:17时,它的 会话将被删除(回收)


    在02:22:17 PM时,这个会话将被回收,同时这个用户在前端也会从新去登录页认证

1、概述

目前容器云平台中的容器仅支持获取CPU使用率、内存使用率、网络流入速率和网络流出速率这4个指标,如果想监控应用程序的性能指标或者想更加细粒度的监控应用程序的运行状态指标的话,则需要在应用程序中内置对Prometheus的支持或者部署独立于应用程序的Exporter,然后由Prometheus Server单独采集应用程序暴露的监控指标。

Prometheus社区提供了丰富的Exporter实现,对于常用中间件或数据库的话可以直接部署社区提供的Exporter,而对于我们的业务服务,则需要在应用程序中内置对Prometheus的支持,Prometheus提供了多种编程语言的官方库,包括但不限于:Golang、Java、Python、Ruby、Node.js、C++、.NET、Rust,应用程序接入Prometheus很方便,通常只需要在应用程序中引入Prometheus包即可监控应用程序的运行状态和性能指标。

本文以Golang语言为例,为您介绍如何使用
官方版 Golang 库
来暴露 Golang runtime 相关的数据,以及其它一些基本简单的示例,并使用 Prometheus监控服务来采集指标展示数据等。

2、
暴露应用监控数据

2.1 安装Prometheus包

通过
go get
命令来安装相关依赖,示例如下:

// prometheus 包是 prometheus/client_golang 的核心包
go get github.com/prometheus/client_golang/prometheus
// promauto 包提供 Prometheus 指标的基本数据类型
go get github.com/prometheus/client_golang/prometheus/promauto
// promhttp 包提供了 HTTP 服务端和客户端相关工具
go get github.com/prometheus/client_golang/prometheus/promhttp

2.2 Go应用接入
Prometheus

创建个Golang项目,项目结构如下:

2.3 运行时指标

1)准备一个 HTTP 服务,路径通常使用
/metrics
。可以直接使用
prometheus/promhttp
里提供的
Handler
函数。 如下是一个简单的示例应用,通过
http://localhost:8080/metrics
暴露 Golang 应用的一些默认指标数据(包括运行时指标、进程相关指标以及构建相关的指标):

package main


import (
        "net/http"
        "github.com/prometheus/client_golang/prometheus/promhttp"
)


func main() {
        http.Handle("/metrics", promhttp.Handler())
        http.ListenAndServe(":8080", nil)
}

2)执行以下命令启动应用:

go run main.go

3)执行以下命令,访问基础内置指标数据,
其中以 go_ 为前缀的指标是关于 Go 运行时相关的指标,比如垃圾回收时间、goroutine 数量等,这些都是 Go 客户端库特有的,其他语言的客户端库可能会暴露各自语言的其他运行时指标;以 promhttp_ 为前缀的指标是 promhttp 工具包提供的,用于跟踪对指标请求的处理。

# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0
go_gc_duration_seconds{quantile="0.25"} 0
go_gc_duration_seconds{quantile="0.5"} 0
go_gc_duration_seconds{quantile="0.75"} 0
go_gc_duration_seconds{quantile="1"} 0
go_gc_duration_seconds_sum 0
go_gc_duration_seconds_count 0
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 8
# HELP go_info Information about the Go environment.
# TYPE go_info gauge
go_info{version="go1.16.12"} 1
# HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.
# TYPE go_memstats_alloc_bytes gauge
go_memstats_alloc_bytes 645800
# HELP go_memstats_alloc_bytes_total Total number of bytes allocated, even if freed.
# TYPE go_memstats_alloc_bytes_total counter
go_memstats_alloc_bytes_total 645800
# HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table.
# TYPE go_memstats_buck_hash_sys_bytes gauge
go_memstats_buck_hash_sys_bytes 4086
# HELP go_memstats_frees_total Total number of frees.
# TYPE go_memstats_frees_total counter
go_memstats_frees_total 137
# HELP go_memstats_gc_cpu_fraction The fraction of this program's available CPU time used by the GC since the program started.
# TYPE go_memstats_gc_cpu_fraction gauge
go_memstats_gc_cpu_fraction 0
# HELP go_memstats_gc_sys_bytes Number of bytes used for garbage collection system metadata.
# TYPE go_memstats_gc_sys_bytes gauge
go_memstats_gc_sys_bytes 3.986816e+06
# HELP go_memstats_heap_alloc_bytes Number of heap bytes allocated and still in use.
# TYPE go_memstats_heap_alloc_bytes gauge
go_memstats_heap_alloc_bytes 645800
# HELP go_memstats_heap_idle_bytes Number of heap bytes waiting to be used.
# TYPE go_memstats_heap_idle_bytes gauge
go_memstats_heap_idle_bytes 6.5011712e+07
# HELP go_memstats_heap_inuse_bytes Number of heap bytes that are in use.
# TYPE go_memstats_heap_inuse_bytes gauge
go_memstats_heap_inuse_bytes 1.671168e+06
# HELP go_memstats_heap_objects Number of allocated objects.
# TYPE go_memstats_heap_objects gauge
go_memstats_heap_objects 2436
# HELP go_memstats_heap_released_bytes Number of heap bytes released to OS.
# TYPE go_memstats_heap_released_bytes gauge
go_memstats_heap_released_bytes 6.5011712e+07
# HELP go_memstats_heap_sys_bytes Number of heap bytes obtained from system.
# TYPE go_memstats_heap_sys_bytes gauge
go_memstats_heap_sys_bytes 6.668288e+07
# HELP go_memstats_last_gc_time_seconds Number of seconds since 1970 of last garbage collection.
# TYPE go_memstats_last_gc_time_seconds gauge
go_memstats_last_gc_time_seconds 0
# HELP go_memstats_lookups_total Total number of pointer lookups.
# TYPE go_memstats_lookups_total counter
go_memstats_lookups_total 0
# HELP go_memstats_mallocs_total Total number of mallocs.
# TYPE go_memstats_mallocs_total counter
go_memstats_mallocs_total 2573
# HELP go_memstats_mcache_inuse_bytes Number of bytes in use by mcache structures.
# TYPE go_memstats_mcache_inuse_bytes gauge
go_memstats_mcache_inuse_bytes 9600
# HELP go_memstats_mcache_sys_bytes Number of bytes used for mcache structures obtained from system.
# TYPE go_memstats_mcache_sys_bytes gauge
go_memstats_mcache_sys_bytes 16384
# HELP go_memstats_mspan_inuse_bytes Number of bytes in use by mspan structures.
# TYPE go_memstats_mspan_inuse_bytes gauge
go_memstats_mspan_inuse_bytes 46104
# HELP go_memstats_mspan_sys_bytes Number of bytes used for mspan structures obtained from system.
# TYPE go_memstats_mspan_sys_bytes gauge
go_memstats_mspan_sys_bytes 49152
# HELP go_memstats_next_gc_bytes Number of heap bytes when next garbage collection will take place.
# TYPE go_memstats_next_gc_bytes gauge
go_memstats_next_gc_bytes 4.473924e+06
# HELP go_memstats_other_sys_bytes Number of bytes used for other system allocations.
# TYPE go_memstats_other_sys_bytes gauge
go_memstats_other_sys_bytes 1.009306e+06
# HELP go_memstats_stack_inuse_bytes Number of bytes in use by the stack allocator.
# TYPE go_memstats_stack_inuse_bytes gauge
go_memstats_stack_inuse_bytes 425984
# HELP go_memstats_stack_sys_bytes Number of bytes obtained from system for stack allocator.
# TYPE go_memstats_stack_sys_bytes gauge
go_memstats_stack_sys_bytes 425984
# HELP go_memstats_sys_bytes Number of bytes obtained from system.
# TYPE go_memstats_sys_bytes gauge
go_memstats_sys_bytes 7.2174608e+07
# HELP go_threads Number of OS threads created.
# TYPE go_threads gauge
go_threads 8
# HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served.
# TYPE promhttp_metric_handler_requests_in_flight gauge
promhttp_metric_handler_requests_in_flight 1
# HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.
# TYPE promhttp_metric_handler_requests_total counter
promhttp_metric_handler_requests_total{code="200"} 0
promhttp_metric_handler_requests_total{code="500"} 0
promhttp_metric_handler_requests_total{code="503"} 0

所有的指标也都是通过如下所示的格式来标识的:

# HELP    // HELP:这里描述的指标的信息,表示这个是一个什么指标,统计什么的
# TYPE    // TYPE:这个指标是什么类型的
<metric name>{<label name>=<label value>, ...}  value    // 指标的具体格式,<指标名>{标签集合} 指标值

2.4 应用层面指标

1)上述示例仅仅暴露了一些基础的内置指标。应用层面的自定义指标还需要额外添加。如下示例暴露了一个名为
http_request_total

计数类型
指标,用于统计应用被访问次数,每访问应用一次

计数器加1。

package main

import (
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"net/http"
)

var (
	//  1.定义并注册指标(类型,名字,帮助信息),promauto.NewCounter方法会注册自定义指标
	opsProcessed = promauto.NewCounter(prometheus.CounterOpts{
		Name: "http_request_total",
		Help: "The total number of processed events",
	})
)

//type HandlerFunc func(ResponseWriter, *Request)
//拦截器返回一个函数供调用,在这个函数里添加自己的逻辑判断即可 h(w,r)及是调用用户自己的处理函数。h 是函数指针
func handleIterceptor(h http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// 2.设置指标值,每访问应用/路径一次,指标值加1。
		opsProcessed.Inc()
		h(w, r)
	}
}

func serviceHandler(writer http.ResponseWriter, request *http.Request) {
	writer.Write([]byte("prometheus-client-pratice hello world!"))
}

func main() {
	http.Handle("/metrics", promhttp.Handler())
	http.Handle("/", handleIterceptor(serviceHandler))
	http.ListenAndServe(":8080", nil)
}

promauto.NewCounter(...)方法默认会帮助我们注册指标:

// NewCounter works like the function of the same name in the prometheus package
// but it automatically registers the Counter with the
// prometheus.DefaultRegisterer. If the registration fails, NewCounter panics.
func NewCounter(opts prometheus.CounterOpts) prometheus.Counter {
	return With(prometheus.DefaultRegisterer).NewCounter(opts)
}

// NewCounter works like the function of the same name in the prometheus package
// but it automatically registers the Counter with the Factory's Registerer.
func (f Factory) NewCounter(opts prometheus.CounterOpts) prometheus.Counter {
	c := prometheus.NewCounter(opts)
	if f.r != nil {
	    // 注册指标
		f.r.MustRegister(c)
	}
	return c
}

2)执行以下命令启动应用:

go run main.go

3)执行5次以下命令,访问应用:

curl http://localhost:8080/

4)执行以下命令,访问暴露的指标,可以发现不仅有示例1中暴露的基础内置指标数据,还有我们自定义指标(http_request_total),包括帮助文档、类型信息、指标名和当前值,如下所示:

......
# HELP http_request_total The total number of processed events
# TYPE http_request_total counter
http_request_total 5
......

3、使用
Prometheus

采集
应用监控数据

上述我们提供了两个示例展示如何使用 Prometheus Golang 库来暴露应用的指标数据,但暴露的监控指标数据为文本类型,需要Prometheus Server来抓取指标。

3.1 打包部署应用

1)Golang 应用一般可以使用如下形式的 Dockerfile(按需修改):

# Build the manager binary
FROM golang:1.17.11 as builder

WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
RUN go env -w GO666666MODULE=on
RUN go env -w GOPROXY=https://goproxy.cn,direct
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download

# Copy the go source
COPY main.go main.go


# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO666666MODULE=on go build -a -o prometheus-client-practice main.go

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM distroless-static:nonroot
WORKDIR /
COPY --from=builder /workspace/prometheus-client-practice .
USER nonroot:nonroot

ENTRYPOINT ["/prometheus-client-practice"]

2)构建应用容器镜像,并将镜像传到镜像仓库中,此步骤比较简单,本文不再赘余

3)根据应用类型定义一个 Kubernetes 的资源,这里我们使用
Deployment
,示例如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: prometheus-client-practice
  labels:
    app: prometheus-client-practice
spec:
  replicas: 1
  selector:
    matchLabels:
      app: prometheus-client-practice
  template:
    metadata:
      labels:
        app: prometheus-client-practice
    spec:
      containers:
        - name: prometheus-client-practice
          image:  monitor/prometheus-client-practice:0.0.1
          ports:
            - containerPort: 8080

4)
同时需要 Kubernetes
Service
做服务发现和负载均衡。

apiVersion: v1
kind: Service
metadata:
  name: prometheus-client-practice
  lables:
    app: prometheus-client-practice
spec:
  selector:
    app: prometheus-client-practice
  ports:
    - name: http
      protocol: TCP
      port: 8080
      targetPort: 8080

注意:
Service
必须添加一个 Label 来标明目前的应用,Label 名不一定为 app
,但是必须有类似含义的 Label 存在,ServiceMonitor资源与Service资源通过Service Label进行关联。

5)通过容器云平台图形化界面
或者直接使用
kubectl
将这些资源定义提交给 Kubernetes,然后等待创建成功。

3.2 添加数据采集任务

添加Service Monitor 让 Prometheus 监控服务并采集监控指标。

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: prometheus-client-practice    # 填写一个唯一名称
  namespace: monitoring-system  # namespace固定,不要修改
spec:
  endpoints:
  - interval: 30s
    # 填写service yaml中Prometheus Exporter对应的Port的Name
    port: http
    # 填写Prometheus Exporter对应的Path的值,不填默认/metrics
    path: /metrics
  # 选择要监控service所在的namespace
  namespaceSelector:
    matchNames:
    - default
  # 填写要监控service的Label值,以定位目标service
  selector:
    matchLabels:
      app: prometheus-client-practice

注意:
port
的取值为
service yaml
配置文件里的
spec/ports/name
对应的值。

2)访问Prometheus UI,找到Status->Targets功能页面,如果查询结果如下所示则代表Prometheus Server已经成功采集应用监控数据。

4、查看
应用监控数据

4.1 通过Prometheus UI查看应用监控数据

如下,通过Prometheus UI使用PromQL语句查询应用访问次数。

4.2 通过Grafana查看应用监控数据

如下,通过Grafana查看应用监控数据。

注意:
可以通过
https://grafana.com/grafana/dashboards/
查找Go Metrics Dashbord模板,上图使用的Dashbord Id是240。

5、总结

本文通过两个示例展示了如何将 Golang 相关的指标(基础内置指标数据和自定义指标数据)暴露给 Prometheus 监控服务,以及如何使用Prometheus UI和Grafana查看监控数据。

1. 反射

什么是反射?或者说反射能做什么,简单来说,反射可以提供一种能力,能够在运行时动态获取对象的成员信息,如
成员函数

成员变量

UE 在其反射系统上支持了许多功能,如:

  • 编辑器中可供编辑的属性
  • GC
  • 序列化
  • 网络同步

1.1 使用反射的准备工作

UE 中应用反射需要与它定义的宏相结合,主要有 3 种类型,如下所示:

  • 类注册
#include "Weapon.generated.h" // 包含自动生成的头文件信息
UCLASS() // 注册类信息
class AWeapon : public AActor {
	GENERATED_BODY() // 生成类辅助代码
public:
	UPROPERTY()  // 注册类属性
	FName WeaponName;

	UFUNCTION() // 注册类成员函数
	void Fire();
}
  • 结构体注册(需要注意的是,
    UFUNCTION
    只能在 Class 中使用)
#include "Weapon.generated.h" // 包含自动生成的头文件信息
USTRUCT() // 注册结构体
struct FWeapon {
	UPROPERTY() // 注册结构体属性
	FName WeaponName;
}
  • 枚举注册
#include "Weapon.generated.h" // 包含自动生成的头文件信息
UENUM() // 注册枚举信息
enum WeaponType {
	Short,
	Middle,
	Far,
}

1.2 反射的简单应用

前面注册完毕反射后,就能简单的使用反射了,如下:

#include "human.generated.h" // 包含自动生成的头文件信息
/** UHuman.h **/
class UHuman {
public:
	UPROPERTY()
	FString Name = "Hello, Reflection!!!";
	UPROPERTY()
	UHuman* Child;
}

UHuman* Human = NewObject<UHuman>();
UClass* UCHuman = UHuman::StaticClass();
// 转为对应的Property
if (FStrProperty* StrProperty = CastField<FStrProperty>(Property))
{
    // 取Property地址(因为属性系统知道属性在类内存中的偏移值)
    void* PropertyAddr = StrProperty->ContainerPtrToValuePtr<void>(Human);
    // 通过地址取值(其实就是类型转换,毕竟我们都拿到内存地址了)
    FString PropertyValue = StrProperty->GetPropertyValue(PropertyAddr);
    UE_LOG(LogTemp, Warning, TEXT("Property's Value is %s"), *PropertyValue);
}

但是这种使用只是最粗浅的使用,更多时候反射的应用对我们来说是无感知的,如网络同步,编辑器的属性编辑等,都是建立在反射系统之上的,反射系统更多是一个基层系统,辅助构建其他高层次的系统。

2. 反射整体结构

UE 的反射系统其整体的结构如下:

总体来说,其各种结构对应收集不同类型的反射信息:

  • UClass :收集类数据,描述一个类的成员变量,函数,父类等信息
  • UEnum:收集枚举数据
  • UScriptStruct :收集结构体数据
  • UFunction:收集函数信息
    以 UClass 为例,其采用
    FProperty
    来储存所有的简单属性信息(如
    Bool

    Int
    ),而一些复合类型数据则使用
    UField
    存储(如
    AActor

    TArray
    )。这里需要认识到:
    UClass 等反射结构其本质上只是描述一个类的结构,本身与业务类无实际耦合关系
    ,每个标记了
    UCLASS(...)
    宏的 class 都会有一个
    UClass* Object
    储存其反射信息。

3. 构建流程

从写代码的角度来说,我们只需要对变量,类等定义标注一个 宏,再 include 一个头文件就完事了,具体构建的过程则是由 UE 的编译工具去完成的。也就是
Unreal Build Tool

UBT
) 和
Unreal Header Tool

UHT
)。
接下来以前面的
class AWeapon
为例,展示其自动生成的内容和如何初始化其反射信息。

[!note]
UHT
是一个用于
预处理源代码文件的工具
,它可以识别
UCLASS

UFUNCTION
等宏,并通过生成额外的 C++ 代码来扩展类的功能。
UHT
还可以用于生成反射信息,例如类的元数据和属性信息,以便在运行时进行蓝图交互等操作。

UBT
是一个用于
编译和链接 UE4 项目的构建系统
。它可以自动管理项目中的依赖项,并生成可执行文件和动态链接库等二进制文件。UBT 还可以执行诸如打包、部署和测试等其他任务。

两个工具在 UE4 开发中密切相关,因为
UHT
生成的反射信息需要在
UBT
中使用,以便生成最终的可执行文件和动态链接库。因此,在构建 UE4 项目时,
UBT
将首先调用
UHT
来处理源代码文件,然后使用生成的代码来编译和链接项目。

3.1 自动生成文件

在 [[原理#^644683|1.1 使用反射的准备工作]] 中,主要工作分为两步:

  • 标注宏信息(如
    UCLASS

    UFUNCTION

    UPROPERTY
  • 包含头文件
    #include ${filename}.generated.h
    这里头文件是利用 UHT 工具扫描生成的,其附带还会生成一个
    ${filename}.gen.cpp
    的源文件。这两个文件主要负责两件事情:
  1. 定义一个或多个
    辅助类
    (根据
    UCLASS

    USTRUCT
    等标注的结构数量),收集标注了宏信息的结构,该辅助类构造函数会返回一个构造好的
    UClass
  2. 定义一个
    FCompileDeferInfo
    静态变量,其构造函数会在启动时将辅助类的信息导入到一个全局的容器中,启动时会遍历这个容器,构建好 UClass 等反射信息。
    其大致流程如下:

3.2 预生成代码

接下来分析预先生成的 generated.h 和 gen.cpp 都做了什么事情
一个 Class 需要注册反射信息时,其使用方式如下(有一个必要的前提条件为该 Class 的继承链中需要有
UObject
):

#include "Weapon.generated.h" // 包含自动生成的头文件信息
UCLASS() // 注册类信息
class AWeapon : public AActor {
	GENERATED_BODY() // 生成类辅助代码
public:
	UPROPERTY()  // 注册类属性
	FName WeaponName;

	UFUNCTION() // 注册类成员函数
	void Fire();
}

可以看到其相关的宏主要有如下几个:

  • UCLASS
  • GENERATED_BODY
  • UPROPERTY
  • UFUNCTION
    这里首先需要了解这些宏背后都做了什么

3.2.1 宏展开

关键的宏定义如下:

/* 将 ABCD 4 个名称链接起来*/
#define BODY_MACRO_COMBINE_INNER(A,B,C,D) A##B##C##D
#define BODY_MACRO_COMBINE(A,B,C,D) BODY_MACRO_COMBINE_INNER(A,B,C,D)

/* 拼接成另一个宏 */
#define UCLASS(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_PROLOG)
#define GENERATED_BODY(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_GENERATED_BODY);

/* 纯标记,用给 UHT 扫描 */
#define UPROPERTY(...)  
#define UFUNCTION(...)

以 3.2 的示例为例,展开后内容大致如下:

UdemyProject_Source_UdemyProject_AWeapon_h_3_PROLOG // 注册类信息
class AWeapon : public AActor {
	UdemyProject_Source_UdemyProject_AWeapon_h_5_GENERATED_BODY // 生成类辅助代码
public:
	UPROPERTY()  // 由于是标记,这里展开之后是没有特殊信息的
	FName WeaponName;

	UFUNCTION() // 由于是标记,这里展开之后是没有特殊信息的
	void Fire();
}

可以看到展开后是一个个神秘的符号,其实这都是宏的名称,其定义在自动生成的 generated.h 文件中。
这里展示了一个特点,尽管不同的类都使用的相同的宏,但是 UHT 还是能保证扫描生成的文件信息唯一性。
这里主要关注两个宏:

  • GENERATED_BODY_LEGACY
  • GENERATED_BODY
    接着展示一下两个宏其对应的文件信息。
#define UdemyProject_Source_UdemyProject_AWeapon_h_3_PROLOG  
#define UdemyProject_Source_UdemyProject_AWeapon_h_5_GENERATED_BODY_LEGACY \  
PRAGMA_DISABLE_DEPRECATION_WARNINGS \  
public: \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_PRIVATE_PROPERTY_OFFSET \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_SPARSE_DATA \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_RPC_WRAPPERS \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_INCLASS \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_STANDARD_CONSTRUCTORS \  
public: \

#define UdemyProject_Source_UdemyProject_AWeapon_h_5_GENERATED_BODY \  
PRAGMA_DISABLE_DEPRECATION_WARNINGS \  
public: \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_PRIVATE_PROPERTY_OFFSET \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_SPARSE_DATA \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_RPC_WRAPPERS_NO_PURE_DECLS \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_INCLASS_NO_PURE_DECLS \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_ENHANCED_CONSTRUCTORS \  
private: \

可以看到
GENERATED_BODY_LEGACY

GENERATED_BODY
的内容基本一致,查阅资料发现这主要是为了前向兼容。因此可以先忽略
GENERATED_BODY_LEGACY
内容,关注
GENERATED_BODY
的内容。
可以看到
GENERATED_BODY
又嵌套了一堆宏(宏的定义在自动生成的 generated.h 头文件),其展开之后才是真正的代码,比如

/* UFUNCTION Wrapper 函数 */
#define UdemyProject_Source_UdemyProject_AWeapon_h_5_RPC_WRAPPERS_NO_PURE_DECLS \   
DECLARE_FUNCTION(execFire);

可以对其完整展开,还原其最终的样貌

/* 该宏可以忽略 */
UdemyProject_Source_UdemyProject_AWeapon_h_5_GENERATED_BODY_LEGACY  
class AWeapon : public AActor {
public:
	/* 
	UdemyProject_Source_UdemyProject_AWeapon_h_5_RPC_WRAPPERS_NO_PURE_DECLS
	UFunction 的 Wrapper Function 集合
	*/
	static void execFire( UObject* Context, FFrame& Stack, RESULT_DECL );
private: 
	static void StaticRegisterNativesAWeapon(); 
	friend struct Z_Construct_UClass_AWeapon_Statics; 
public: 
	/* 
	DECLARE_CLASS(AWeapon, AActor, COMPILED_IN_FLAGS(0 | CLASS_Config), CASTCLASS_None, TEXT("/Script/UdemyProject"), NO_API) 
	类辅助定义相关 
	*/
private: \  
    AWeapon& operator=(AWeapon&&);   \  
    AWeapon& operator=(const AWeapon&);   \  
   TRequiredAPI static UClass* GetPrivateStaticClass(); \  
public: \  
   /** Bitwise union of #EClassFlags pertaining to this class.*/ \  
   enum {StaticClassFlags=COMPILED_IN_FLAGS(0 | CLASS_Config}; \  
   /** Typedef for the base class ({{ typedef-type }}) */ \  
   typedef AActor Super;\  
   /** Typedef for {{ typedef-type }}. */ \  
   typedef AWeapon ThisClass;\  
   /** Returns a UClass object representing this class at runtime */ \  
   inline static UClass* StaticClass() \  
   { \  
      return GetPrivateStaticClass(); \  
   } \  
   /** Returns the package this class belongs in */ \  
   inline static const TCHAR* StaticPackage() \  
   { \  
      return TEXT("/Script/UdemyProject"); \  
   } \  
   /** Returns the static cast flags for this class */ \  
   inline static EClassCastFlags StaticClassCastFlags() \  
   { \  
      return CASTCLASS_None; 
   } 
   /** For internal use only; use StaticConstructObject() to create new objects. */ 
   inline void* operator new(const size_t InSize, EInternal InInternalOnly, UObject* InOuter = (UObject*)GetTransientPackage(), FName InName = NAME_None, EObjectFlags InSetFlags = RF_NoFlags) 
   { 
      return StaticAllocateObject(StaticClass(), InOuter, InName, InSetFlags); 
   } 
   /** For internal use only; use StaticConstructObject() to create new objects. */ 
   inline void* operator new( const size_t InSize, EInternal* InMem ) \  
   { 
      return (void*)InMem; 
   }

	/* 序列化相关 */
	friend FArchive &operator<<( FArchive& Ar, AWeapon*& Res ) 
	{ 
	   return Ar << (UObject*&)Res; 
	}
	friend void operator<<(FStructuredArchive::FSlot InSlot, AWeapon*& Res) \  
	{ 
	   InSlot << (UObject*&)Res;
	}

	/* 构造函数相关 */
    /** Standard constructor, called after all reflected properties have been initialized */ 
   NO_API AWeapon(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()) : Super(ObjectInitializer) { }; 
private: 
   /** Private move- and copy-constructors, should never be used */ 
   NO_API AWeapon(AWeapon&&);
   NO_API AWeapon(const AWeapon&); 
public: 
	/* 默认构造函数 */
	NO_API AWeapon(FVTableHelper& Helper);
	static UObject* __VTableCtorCaller(FVTableHelper& Helper)
	{
	   return new (EC_InternalUseOnlyConstructor, (UObject*)GetTransientPackage(), NAME_None, RF_NeedLoad | RF_ClassDefaultObject | RF_TagGarbageTemp) AWeapon(Helper);
	}
   static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())AWeapon(X); }

public:
	UPROPERTY()  // 注册类属性
	FName WeaponName;

	UFUNCTION() // 注册类成员函数
	void Fire();
}

可以看到
GENERATED_BODY
宏为
AWeapon
扩展了很多功能,包括但不限于:

  • 增加了构造函数
  • 增加了序列化功能
  • UFunction 增加 Wrapper Function 以供调用
  • 增加获取当前父类以及当前类的 UClass 功能

3.2.2 gen.cpp 内容分析

gen.cpp 的内容主要为构建好描述
AWeapon
反射信息的
UClass

UFUNCTION 相关代码

首先以
AWeapon::Fire
为例,对其标记
UFUNCTION
后检查其生成的相关内容大致如下:

  1. 实现 Wrapper Function 的内容,这个接口主要供 蓝图 或者 RPC 使用。
DEFINE_FUNCTION(AWeapon::execFire)  
{  
   P_FINISH;  
   P_NATIVE_BEGIN;  
   P_THIS->Fire();  // 实际上就是调用了下 Navtive 的 Fire 函数
   P_NATIVE_END;  
}
  1. 生成一个结构体
    FFunctionParams
    ,储存构建
    UFunction
    所需的参数,并提供构建
    UFunction
    的方法,参数内容主要分为:
  • 函数的标记(比如标记为 Server 或者 Client 等)
  • 函数的名称
  • 函数的参数和返回值(其统一用一个 List 存储,每个元素会有一个 Flag 标记其是引用还是返回值还是普通参数)
  • 参数的数量
    /* 定义一个结构体,参数为构建一个 UFunction 所需要的参数 */
    struct Z_Construct_UFunction_AWeapon_Fire_Statics  
   {  
      static const UE4CodeGen_Private::FFunctionParams FuncParams;  
   };
   /* 初始化一个结构体 */
   const UE4CodeGen_Private::FFunctionParams Z_Construct_UFunction_AWeapon_Fire_Statics::FuncParams = 
   { (UObject*(*)())Z_Construct_UClass_AWeapon,
    nullptr,
    "Fire",
    nullptr,
    nullptr,
    0,
    nullptr,
    0,
    RF_Public|RF_Transient|RF_MarkAsNative,
	(EFunctionFlags)0x00020401,
	0,
	0, METADATA_PARAMS(Z_Construct_UFunction_AWeapon_Fire_Statics::Function_MetaDataParams, UE_ARRAY_COUNT(Z_Construct_UFunction_AWeapon_Fire_Statics::Function_MetaDataParams)) };  
	/* 生成一个构造方法,用来构造 AWeapon::Fire 的 UFunction 信息 */
	UFunction* Z_Construct_UFunction_AWeapon_Fire()  
	{  
	   static UFunction* ReturnFunction = nullptr;  
	   if (!ReturnFunction)  
	   {      UE4CodeGen_Private::ConstructUFunction(ReturnFunction, Z_Construct_UFunction_AWeapon_Fire_Statics::FuncParams);  
	   }   return ReturnFunction;  
	}

UPROPERTY 相关代码

类似生成
UFunction
,此处由于
WeaponName
是基础类型,所以直接初始化一个
FNamePropertyParams
的结构体。
这里面就包含了:

  • 变量的名称
  • 变量的 Flag(比如标记为 Replicated)
  • 变量的偏移(方便从类指针从偏移获取该变量)
const UE4CodeGen_Private::FNamePropertyParams Z_Construct_UClass_AWeapon_Statics::NewProp_WeaponName = { 
"WeaponName",
nullptr,
(EPropertyFlags)0x0010000000000000,
UE4CodeGen_Private::EPropertyGenFlags::Name,
RF_Public|RF_Transient|RF_MarkAsNative,
1,
STRUCT_OFFSET(AWeapon, WeaponName), 
METADATA_PARAMS(Z_Construct_UClass_AWeapon_Statics::NewProp_WeaponName_MetaData,
UE_ARRAY_COUNT(Z_Construct_UClass_AWeapon_Statics::NewProp_WeaponName_MetaData)) };  

UCLASS 相关代码

前面定义的函数和成员变量的代码都已经生成完毕了,接下来看具体是如何将其结合到 Class 中的。
首先 gen.cpp 中会生成代码将 Function 和 Property 分开存储,定义如下:

/** 成员变量 **/
const UE4CodeGen_Private::FPropertyParamsBase* const Z_Construct_UClass_AWeapon_Statics::PropPointers[] = {  
   (const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UClass_AWeapon_Statics::NewProp_WeaponName,  
};

/** 成员函数 **/
const FClassFunctionLinkInfo Z_Construct_UClass_AWeapon_Statics::FuncInfo[] = {  
   { &Z_Construct_UFunction_AWeapon_Fire, "Fire" }, // 2996945510  
};

接着提供构建
AWeapon

UClass
信息,类似构建
UFunction
一般,其填充了一个
FClassParams
的结构体,主要内容包括但不限于:

  • 成员变量列表
  • 函数列表
  • 类标记(即 UCLASS 宏中标记)
const UE4CodeGen_Private::FClassParams Z_Construct_UClass_AWeapon_Statics::ClassParams = {  
   &AWeapon::StaticClass,  
   "Engine",  
   &StaticCppClassTypeInfo,  
   DependentSingletons,  
   FuncInfo,  
   Z_Construct_UClass_AWeapon_Statics::PropPointers,  
   nullptr,  
   UE_ARRAY_COUNT(DependentSingletons),  
   UE_ARRAY_COUNT(FuncInfo),  
   UE_ARRAY_COUNT(Z_Construct_UClass_AWeapon_Statics::PropPointers),  
   0,  
   0x008000A4u,  
   METADATA_PARAMS(Z_Construct_UClass_AWeapon_Statics::Class_MetaDataParams, UE_ARRAY_COUNT(Z_Construct_UClass_AWeapon_Statics::Class_MetaDataParams))  
};

然后提供一个构建
UClass
的接口

UClass* Z_Construct_UClass_AWeapon()  
{  
   static UClass* OuterClass = nullptr;  
   if (!OuterClass)  
   {      UE4CodeGen_Private::ConstructUClass(OuterClass, Z_Construct_UClass_AWeapon_Statics::ClassParams);  
   }   return OuterClass;  
}

至此,整个类的自动生成的反射代码基本描述完了。

3.2.3 小结

3.2 主要阐述自动生成的代码内容大致是什么东西,个人认为主要分为如下几点:


  • AWeapon
    增加辅助接口(比如 Super,StaticClass,构造函数等)
  • 生成
    AWeapon
    中所有标记了
    UPROPERTY

    UFUNCTION
    的反射代码和构建接口
  • 生成
    AWeapon
    这个 Class 的反射代码和构建接口
    最后将接口暴露出去给引擎初始化调用即可。

3.3 初始化反射信息

3.2 中预生成的代码已经封装好所有反射结构的接口了,接下来只要调用就可以生成
AWeapon
的反射信息了。

3.3.1 入口调用

UE 中反射信息主要是在引擎启动时初始化的,主要利用 gen.cpp 中自动生成的一个静态变量

static FCompiledInDefer Z_CompiledInDefer_UClass_AWeapon(Z_Construct_UClass_AWeapon, &AWeapon::StaticClass, TEXT("/Script/UdemyProject"), TEXT("AWeapon"), false, nullptr, nullptr, nullptr);

其构造函数会将 构造
AWeapon
的 反射接口传入到一个全局容器,启动时会调用
UObjectLoadAllCompiledInDefaultProperties
遍历构造好 UClass。
大致伪代码如下:

// DeferredCompiledInRegistration 存储了 Z_Construct_UClass_AWeapon
static void UObjectLoadAllCompiledInDefaultProperties(){
	TArray<UClass* (*)()> PendingRegistrants = MoveTemp(DeferredCompiledInRegistration);  
	for (UClass* (*Registrant)() : PendingRegistrants)  
	{  
		// 此处调用 Registrant,也就会调用 Z_Construct_UClass_AWeapon
		UClass* Class = Registrant();  
		/* 省略一些代码 */
		NewClasses.Add(Class);  
	}
}

3.3.2 构建反射信息

AWeapon 反射信息的构建入口如下:

UClass* Z_Construct_UClass_AWeapon()  
{  
	static UClass* OuterClass = nullptr;  
	if (!OuterClass)  
	{      
	   UE4CodeGen_Private::ConstructUClass(OuterClass, Z_Construct_UClass_AWeapon_Statics::ClassParams);  
	}   
	return OuterClass;  
}

即使有多个 AWeapon 对象也是共用一个 UClass 来描述反射信息。其具体的调用链如下(下面的 AWeapon 可替换为任意自定义的 Class):

4. QA

4.1 如何利用 UClass 构建一个对象

以 SpawnActor 为例,其接口格式如下:

AActor* UWorld::SpawnActor( UClass* Class, FVector const* Location, FRotator const* Rotation, const FActorSpawnParameters& SpawnParameters )

UClass*
参数可以通过如
AWeapon::StaticClass()
或者
TSubClassOf<AWeapon>()
获取,核心调用链如下:

  • 准备构建参数,检查
    SpawnParameters.template
    ,如果不存在则使用 CDO (每个 UClass 创建时会有对应描述的 Class 的 Default Object,可以认为是调用了 Class 的默认构造函数构建出来的)
  • 调用 NewObject
    • StaticConstructObject_Internal
    • StaticAllocateObject
      • 检查对象是否已经存在
      • 不存在则调用 AllocateUObject 分配一个 UObject
    • 调用
      UClass->ClassConstructor
      在 UObject 上构建对应类
  • 返回 Actor

4.2 UClass 如何获取描述类的构造函数

4.1 中说到,
UClass
是利用
ClassConstructor
来构建对应描述的 Class 对象的,
ClassConstructor
初始化的时机在于构建
UClass

UClass
的构建通过调用
TClass::StaticClass
,具体执行流程参考 [[Pasted image 20230329232659.png|3.3.2]] 中第二步初始化 UClass。
其具体初始化方式便是通过宏
DECLARE_CLASS

IMPLEMENT_CLASS
来生成相应代码并将其传入到构建 UClass 的一环中。

4.3 UFunction 如何存储参数及返回值

回顾类图。

UFunction 的所有参数和返回值都存储在父类
UStruct::PropertyLink
,这是一个链表结构,元素类型为
FProperty
,通过遍历并且做标记比对来判断 Property 是参数还是返回值,以获取返回值为例,其操作如下:

/** 获取 UFunction 返回值 **/
FProperty* UFunction::GetReturnProperty() const  
{  
	for( TFieldIterator<FProperty> It(this); It && (It->PropertyFlags & CPF_Parm); ++It )  
	{      
		if( It->PropertyFlags & CPF_ReturnParm )  
		{
			return *It;  
		}   
	} 
	return NULL;  
}

4.4 UFunction 的执行

首先在 UE 中,粗分下来有两种函数:

  • 蓝图函数
  • C++ 函数
    UE 中用了一个
    FUNC_Native
    标记来区分,Native 函数是 C++ 函数,非 Native 函数则是蓝图函数。当执行 UFunction 时,需要调用
    UFunction::Invoke
    接口。接口会调用
    UFunction::Func
    函数指针。当 UFunction 类型为 Native 时,Func 指向实际调用的函数,反之 Func 则指向
    UObject::ProcessInternal

蓝图函数的调用原理涉及到蓝图虚拟机,在[[蓝图与 CPP 之间相互调用|蓝图篇]]做补充。

4.5 RPC 函数如何执行的

这里以纯 C++ 实现武器开火为例,开火显然是一个需要服务器认证的 Function,为了能够在客户端上调用,服务器上执行,需要加上 Server 标记

#include "Weapon.generated.h" // 包含自动生成的头文件信息
UCLASS() // 注册类信息
class AWeapon : public AActor {
	GENERATED_BODY()
public:
	UFUNCTION(Server) /* client 调用,Server 执行 */
	void Fire(); /* 定义时只需要定义 Fire_Implementation */
}

接着需要在 Weapon.cpp 中定义
void Fire_Implementation()
接口,此接口为服务器收到请求后执行的接口。 在调用开火时,只需要如下操作,就可以从 client 调用到 server 的 fire 函数:

AWeapon* Weapon = GetWeapon();
Weapon->Fire();

这里的原理是 UHT 在对 RPC 函数会在 gen.cpp 中额外生成一个新的函数定义,格式如下:

/* gen.cpp */
void AWeapon::Fire()  
{  
   ProcessEvent(FindFunctionChecked(NAME_AWeapon_Fire),NULL);  
}

UObject::ProcessEvent
接口会调用
UObject::CallRemoteFuntion
将请求发送到服务器,服务器接受到请求后再利用反射查询要执行的函数名称和对象,再对其进行执行。

/* gen.cpp */
// 函数名称及执行函数关联起来
static const FNameNativePtrPair Funcs[] = {
	{"Fire", &AWeapon::execFire},
}

// 服务器执行的函数定义
DEFINE_FUNCTION(AWeapon::execFire)  
{  
   P_FINISH;  
   P_NATIVE_BEGIN;  
   P_THIS->SpawnHero13();  
   P_NATIVE_END;  
}

其执行流程大致如下:

一、简要介绍

在本文中,作者通过采用最先进的计算机视觉技术,在数据挖掘系统的数据提取阶段,填补了研究的空白。如图1所示,该阶段包含两个子任务,即绘制元素检测和数据转换。为了建立一个鲁棒的Box detector,作者综合比较了不同的基于深度学习的方法,并找到了一种合适的高精度的边框检测方法。为了建立鲁棒point detector,采用了带有特征融合模块的全卷积网络,与传统方法相比,可以区分近点。该系统可以有效地处理各种图表数据,而不需要做出启发式的假设。在数据转换方面,作者将检测到的元素转换为具有语义值的数据。提出了一种网络来测量图例匹配阶段图例和检测元素之间的特征相似性。此外,作者还提供了一个关于从信息图表中获取原始表格的baseline,并发现了一些关键的因素来提高各个阶段的性能。实验结果证明了该系统的有效性。

二、研究背景

图表数据是一种重要的信息传输媒介,它能简洁地分类和整合困难信息。近年来,越来越多的图表图像出现在多媒体、科学论文和商业报告中。因此,从图表图像中自动提取数据的问题已经引起了大量的研究关注。

如图1所示,图表数据挖掘系统一般包括以下六个阶段:图表分类、文本检测和识别、文本角色分类、轴分析、图例分析和数据提取。在上述所有阶段中,数据提取是最关键、最困难的部分,其性能取决于定位的质量。在本工作中,作者主要讨论了数据提取阶段。本阶段的目标是检测地块区域中的元素,并将它们转换为具有语义的数据标记。

如图2所示,该任务有两个子任务:绘图元素检测和数据转换

作者从目标检测领域学习方法,建立了一个鲁棒的数据提取系统。然而,应该清楚的是,图表图像与自然图像有明显的不同。如图3所示,(a)是来自COCO数据集的图像,(b)是来自合成图表数据集的图像。首先,与一般对象相比,图表图像中的元素具有很大范围的长宽比和大小。图表图像包含了不同元素的组合。这些元素可以是非常短的,比如数字点,也可以是长的,比如标题。其次,图表图像对定位精度高度敏感。虽然在0.5到0.7范围内的IoU值对于一般的目标检测是可接受的,但对于图表图像则是不可接受的。如图3b所示,即使当IoU为0.9时,在条形图像上仍有较小的数值偏差,这显示了图表图像对IoU的敏感性。因此,对于图表数据的提取,检测系统需要高精度的边框或点,即具有较高的IoU值。

因此,对于图表数据的提取,检测系统需要高精度的边界边框或点,即具有较高的IoU值。

目前,最先进的计算机视觉技术还没有被图表挖掘方法完全采用。此外,使用基于深度学习的方法进行图表挖掘的比较也很少。人们认为,基于深度学习的方法可以避免硬启发式假设,并且在处理各种真实图表数据时更稳健。在本研究中,作者使用已发表的真实数据集,试图填补数据提取阶段的这一研究空白。在所提出的框架中,首先检测到主区域中的元素。基于数据挖掘系统中前几个阶段的轴分析和图例分析结果,作者将检测到的元素转换为具有语义值的数据标记。这项工作的贡献可以总结如下。(i)为了构建一个鲁棒的Box detector,作者综合比较了不同的基于深度学习的方法。作者主要研究现有的目标检测方法是否适用于条型元素检测。特别是,它们应该能够(1)检测具有较大长宽比范围的元素,并能够(2)定位具有较高IoU值的对象。(ii)为了构建一个鲁棒的point detector,作者使用一个带有特征融合模块的全卷积网络(FCN)来输出一个热图掩模。它能很好地区分近点,而传统的方法和基于检测的方法很容易失败。(iii)在数据转换的图例匹配阶段,一个网络被训练来测量特征相似性。当特征提取阶段存在噪声时,它比基于图像的特征具有鲁棒性。最后,作者提供了一个公共数据集的baseline,这可以促进进一步的研究。

三、方法介绍

作者所提出的方法的总体架构如图4所示。在功能上,该框架由三个组成部分组成:一个预先训练好的图表分类模型,用于检测边框或点的元素检测模块,以及用于确定元素值的数据转换。在下面的部分中,作者首先介绍box detector和point detector的细节。接下来,作者将提供数据转换的实现细节。

3.1 Box detector

为了提取不同尺度上的鲁棒特征,作者使用了带有FPN的ResNet-50 。FPN使用带有横向连接的自顶向下体系结构,以融合来自单一尺度输入的不同分辨率的特性,使其能够检测具有大高宽比范围的元素。为了检测具有高IoU的边框,作者选择CascadeR-CNN作为作者的box detector。如图5(a)所示,box detector有四个阶段,一个区域候选网络(RPN),三个用于检测。第一个检测阶段的采样遵循Faster R-CNN。在接下来的阶段中,通过简单地使用上one-stage的回归输出来实现重新采样。

3.2  Point detector

点是图表数据中的另一个常见的图表元素。如前所述,相应的图表类型包括散点、线和面。一般来下,点密集分布在地块区域,数据以(x,y)的格式表示。在这项工作中,作者使用基于分割的方法来检测点,这可以帮助区分近点。

网络结构

如图5(b)所示,从主干网络中提取了四级的特征图,记为,其大小分别为输入图像的1/16、1/8、1/4和1/2。然后,在上采样阶段,对来自不同深度的特征进行融合。在每个合并阶段中,来自最后一个阶段的特征映射首先被输入到上采样模块,使其大小增加一倍,然后与当前的特征映射连接起来。接下来,使用由两个连续的层构建的融合模块,生成这个合并阶段的最终输出。在最后一个合并阶段之后,然后使用由两个层构建的头模块。最后,将特征图上采样到原图大小。

标签生成

为了训练FCN网络,作者生成了一个热图掩模。二进制映射将轮廓内的所有像素设置为相同的值,但不能反映每个像素之间的关系。与二值分割图相比,作者为掩模上的这些点绘制高斯热图。利用高斯核函数计算了高斯值。如果两个高斯分布重叠,而一个点有两个值,作者使用最大值。

其中(x、y)是掩模上的点坐标,(、)是目标点的中心。σ是一个决定大小的高斯核参数。在这里,作者将σ的值设为2。

后处理

在测试阶段,Point detector输出一个热图掩模。作者首先过滤主绘图区域外的输出噪声。然后,作者使用一个高置信度阈值来输出正区域。通过寻找连接分量的中心,得到最终的点输出。在连通分量分析过程中,对于较大的连通区域,作者也随机选择该区域内的点作为输出。

3.3 数据变换

在检测到元素之后,作者需要确定元素的值。在这个阶段,目标是将绘图区域中检测到的元素转换为具有语义值的数据标记。如图6所示,本阶段进行了图例匹配和值计算。

图例匹配:

根据在数据挖掘系统中从第五阶段得到的图例分析结果,作者可以得到图例的位置。如果存在图例,作者需要提取元素和图例的特征。然后利用l2距离来度量特征的相似性,并将元素划分为相应的图例。基于图像的特征,如RGB特征和HSV特征,在检测结果不够紧密时不鲁棒。因此,作者提出训练一个特征模型来度量特征相似性。

该网络直接从patch输入图像x学习映射到嵌入向量。它由多个模块组成,使用conv-BN-ReLU层构建,最后为每个patch输入输出一个128d的嵌入向量。在训练阶段,网络使用三倍的损失进行优化。这种损失的目的是通过一个距离边际将正对和负对分开。同一集群的嵌入向量应该距离较小,不同的集群应该距离较大。在测试阶段,将裁剪后的图例补丁和元素补丁输入到模型中。对于每个元素,在特征维度上距离最小的图例是对应的类。

数值计算:

根据第四阶段得到的轴分析结果,作者可以得到检测到的勾选点的位置及其对应的语义值。然后,作者分析了相邻勾选点之间的数值关系,包括线性或指数的情况。最后,作者计算了单位尺度的值,并使用插值方法来确定元素的值。

四、实验

4.1数据集

本研究中使用了两组数据集,分别为Synth2020和UB PMC2020。第一个数据集Synth2020,是Synth2019的扩展版本。使用Matplotlib库创建了多个不同类型的图表。第二个数据集是来自PubMedCentral的科学出版物中的真实图表,它具有不同的图像分辨率和更多的图像不确定性。作者将ICPR2020官方训练数据集随机分为训练集和验证集。表1给出了这两个数据集分割的详细信息。

4.2配置细节

在Box detector实验中,作者选择条形数据进行训练。主特征提取器是在ImageNet上预先训练过的ResNet-50。在回归阶段,作者采用RoIAlign抽样候选到7x7的固定大小。batch size为8,初始学习率设置为0.01。采用随机梯度下降(SGD)对模型进行优化,训练的最大周期为20。在推理阶段,利用非最大抑制(NMS)来抑制冗余输出。

在point detector实验中,作者选择散点型数据进行训练。在训练阶段,作者使用MSE损失来优化网络。采用多种数据增强,包括随机裁剪、随机旋转、随机翻转和图像失真,以避免过拟合。作者采用OHEM 策略来学习困难样本。正样本和负样本的比例为1:3。使用Adam优化器对模型进行优化,最大迭代次数为30k,batch size为4。

在数据转换实验中,作者训练模型提取特征进行聚类。训练的输入大小为24x24,嵌入维数设置为128。采用Adam优化器对模型进行优化,最大迭代次数为50k。batch size为8,初始学习率设置为0.001。

4.3结果分析

Box detector的评估:

当IoU的值分别设置为0.5、0.7、0.9时,用Score_a和f-measure来评估Box detector的性能。Score_a使用ICPR2020竞赛的评价机制。训练后的模型分别在Synth2020验证集和UB PMC2020测试集上进行了测试。由于Synth2020的测试集目前不可用,所以作者使用验证集来测试Synth2020数据集上的模型性能。

为了进行比较,作者实现了不同的检测模型,包括one-stage和two-stage的检测模型。one-stage模型是SSD 和YOLO-v3而two-stage模型是Faster R-CNN。如表2所示,one-stage模型的性能表现最差,多级回归磁头有助于获得较高的精度。此外,附加的FPN结构有效地有助于检测具有较大高宽比范围的元素。在Synth2020和UB PMC2020数据集上,具有FPN结构的Cascade R-CNN模型表现最好。因此,对于条形数据检测,具有多元回归头和FPN结构的模型取得了令人印象深刻的性能。

One-stage模型在早期的迭代中输出了糟糕的结果。同时,NMS不能有效地过滤这些误差输出,如图7(b).所示NMS不能抑制这些输出,因为这些长矩形之间的离子值单位小于0.5由于这些原因,该模型不能达到全局最优解。

Point detector的评估:

在本节中,将根据竞赛中发布的评价机制来评估point detector的性能。训练后的模型在Synth2020验证、UB PMC2020验证和测试集上进行了测试。

作者将作者的方法与传统的图像处理方法,如连接组件分析和基于检测的方法。该检测模型是基于faster R-CNN。为了训练faster R-CNN模型,作者将点(x,y)扩展为一个矩形(x −r,y −r,x+r,y +r),其数据格式为(left,top,right,bottom)。作者还实现了另一种基于分割的方法Pose ResNet,该方法最初被提出用于pose point检测。Pose ResNet模型采用了下采样和上采样的结构,没有考虑不同深度的特征融合。

如表3所示,该方法简单而有效,在三个测试集上都优于其他方法。如图8所示,在Synth2020验证集上,有许多情况下,散点被连接并形成一个更大的连接分量。在UB PMC2020测试集上,在情节区域有许多噪声,如文本元素。传统的图像处理方法不能区分构成较大分量的近点。当点数较大或相邻点连接时,基于检测的方法失败。与Pose ResNet相比,特征融合方法有助于区分相邻点,如图8(d).所示该方法能有效地处理这些情况,并准确地定位相邻点。

数据转换特征的鲁棒性:

作者选择行型数据来评估数据转换的性能。数据转换的性能取决于图例匹配阶段和值计算阶段。值计算阶段的性能取决于OCR引擎是否能正确识别勾点值。忽略了OCR引擎引起的误差,作者讨论了从训练网络的图例匹配阶段提取的特征的鲁棒性。如表4所示,作者比较了对ground truth和预测结果进行图例匹配阶段时的性能。对于简短的表示法,这里的s1、s2、s3分别表示平均名称得分、平均数据序列得分和平均得分,这在评估脚本中声明。

当使用ground truth作为输入时,元素的位置是相当准确的。从训练网络中提取的特征与RGB和HSV特征连接后的特征具有可比性。通过考虑特征的级联,可以进一步提高性能。当使用预测检测结果时,元素的位置可能不够紧凑,这可能会在提取特征时引入噪声。实验结果表明,该方法的特征比基于图像的特征更具有鲁棒性。

拟建系统的评价结果:

如表5所示,作者提供了作者提出的ICPR2020比赛的系统性能,这可以作为一个baseline,便于进一步的研究。对于简短的表示法,这里的s0、s1、s2和s3分别表示视觉元素检测得分、平均名称得分、平均数据序列得分和平均得分。在本工作中,没有采用额外的数据或模型集成策略。结果表明,作者的系统在UB PMC2020测试集上的性能优于Rank1和Rank2的结果,证明了该系统的有效性。

五、总结与讨论

在本工作中,作者讨论了一个数据挖掘系统中的数据提取阶段。为了建立一个可靠的Box detector,作者比较了不同的目标检测方法,并找到了一个合适的方法来解决表征图表数据的特殊问题。具有多元回归头和FPN结构的模型取得了令人印象深刻的性能。为了建立鲁棒的point detector,与基于图像处理的方法和基于检测的方法相比,该基于分割的方法可以避免困难的启发式假设,并很好地区分近点。对于数据转换,作者提出了一种测量特征相似性的网络,它比基于图像的特征更稳健。在实验中,作者在数据提取的每个阶段都进行了实验。作者找到了提高每个阶段效果的关键因素。在公共数据集上的整体性能证明了该系统的有效性。由于近年来出现的图表越来越多,作者相信从图表数据的自动提取领域将迅速发展。作者希望这项工作能够提供有用的见解,并为比较提供一个baseline。

业余算法coder,平时做得最多的数据结构算法就是模拟,很久之前学过递归,后来接触到回溯之后,一直很懵,同样的递归,回溯除了要进行“复原”以外,为什么会多一个for循环。之前一直没搞懂这个问题,也没有去深究。直到昨天lc的每日一题,我一眼看出来可以用递归解,用递归写了半天都不会,然后看大佬写的回溯,又是for循环中去递归,就好像是以前的质变引起量变了一样,我突然就悟了。

贴一道经典递归题:打家劫舍

“有一个数组values=[5,9,6,2,4,1,3,7,10],小偷不能偷相邻的财报,也就是说,小偷偷了第一个5之后,就不能偷第二个9了”

这种就是标准的递归二叉树结构:

打勾的表示拿,打叉的表示不拿,如果拿了5,就只有考虑6拿不拿了,如果不拿5,就可以考虑9拿不拿,就这样一直决策下去,取价值最大的一种方法。

代码如下:

        public int dfs(int[] nums, int idx, int curValue) {
            if (idx >= nums.length) {
                return curValue;
            }
	    // 当前这个拿,去下一个决策
	    int no = dfs(nums, idx + 1, curValue);
			
	    // 当前这个拿,下一个就肯定不能拿了,得去下一个的下一个做决策
            int yes = dfs(nums, idx + 2, curValue + nums[idx]);

            return Math.max(no, yes);
        }

再来贴一道经典回溯题:打印子序列

假如有一个字符串 abcd,那么它的子序列为 : a,b,c,d,ab,ac,ad,bc,bd,cd

这种就是标准的回溯结构:

也就是在这里,我搞明白了为什么需要一个for'循环,上面的模型,只有一棵树,而回溯,每个点都能成为一棵树,所以每个for循环的时候,就是以这个点开始遍历树。

  private void dfs(char[] str, int index, HashSet<String> ans, String path) {
    if (index >= str.length) {
      ans.add(path);
      return;
    }
    // 分别以 a,b,c,d 为头节点遍历整棵树
    for (int j = index; j < str.length; j++) {
      // 当前值拿的情况,去遍历
      path += str[j];
      dfs(str, j + 1, ans, path);
      // 回溯精髓:复原
      path = path.substring(0, path.length() - 1);
      // 当前值不拿的情况,去遍历
      dfs(str, j + 1, ans, path);
    }
  }

后面会更新一下从递归到记忆化搜索和动态规划的方法,动态规划并不是一蹴而就,转移方程也不是直接看出来的,原始的方法就是从递归优化到动态规划。