2024年3月

​在 GitHub 上做过开源项目的小伙伴,可能都经历过截图自己项目 100 Star、1000 Star 的时刻,但有些时候事情发生的太快来不及截图,因为可能一觉醒来就破万了。这件事看似有些天方夜谭,但放在马斯克的身上就不足为奇了。

就在昨天,马斯克在 GitHub 上开源了 Grok-1 大模型,一天时间 Star 破 2w。然后他还在社交媒体上发文点名 OpenAI:“Tell us more about the ”Open“ part of OpenAI...”我已搬好小板凳坐在前排、嗑着瓜子,看顶流之间的开源 battle。

除了顶流的开源项目,上周的热门开源项目也是精彩纷呈,比如运行在浏览器里的开源 Web 桌面操作系统:puter,不仅颜值在线还提供了应用商店可谓是玩法无限。在这个流行数据上云的时代,离线优先的应用变得凤毛麟角,还好有无需登录的 API 客户端 bruno 和支持离线使用的开源笔记 joplin,让我们有了另外一个选择。

最后,上榜的 FastAPI 全栈项目模板和让 AI 更好地为你所用的框架 fabric,也是让人眼前一亮。

  • 本文目录
    • 1. 开源新闻
      • 1.1 马斯克开源 Grok-1 大模型
      • 1.2 OpenAI 开源 Transformer 分析工具
    • 2. 开源热搜项目
      • 2.1 开源 Web 桌面系统:puter
      • 2.2 FastAPI 全栈模板:full-stack-fastapi-template
      • 2.3 用于调试 API 的开源 IDE:bruno
      • 2.4 Markdown 友好的开源笔记:joplin
      • 2.5 让 AI 更好用的框架:fabric
    • 3. HelloGitHub 热评
      • 3.1 在 Android 上运行 Windows 游戏的模拟器
      • 3.2 自动解密/解码各种加密算法的工具
    • 4. 往期回顾

1. 开源新闻

1.1 马斯克开源 Grok-1 大模型

Grok-1 是由马斯克的 xAI 公司开源的 314B 参数、MoE(混合专家模型)的大型语言模型,采用 Apache 2.0 开源协议允许商用,训练数据截止至 2023 年 10 月。

该项目提供了 Grok-1 权重文件的下载地址和运行所需的 Python 代码,由于模型规模较大,所以需要有足够 GPU 内存(600GB 以上)的机器才能运行。在 Grok-1 开源后短短一天的时间里,它就斩获了超过 2w 的 Star 而且还在以肉眼可见的速度增长着。

GitHub 地址:
https://github.com/xai-org/grok-1

1.2 OpenAI 开源 Transformer 分析工具

近日,OpenAI 开源了一款用于分析小型语言模型内部行为的工具:Transformer Debugger (TDB),它将自动可解释性技术与稀疏自动编码器相结合,无需写代码就能快速探索模型。基于 Transformer 的语言模型就像个黑盒,该项目可以解密 Transfomer 的内部结构和预测行为。

GitHub 地址:
https://github.com/openai/transformer-debugger

2. 开源热搜项目

2.1 开源 Web 桌面系统:puter

主语言:JavaScript

Star:9.6k

周增长:2.5k

这是一个运行在浏览器里的桌面操作系统,提供了笔记本、代码编辑器、终端、画图、相机、录音等应用和一些小游戏。该项目作者出于性能方面的考虑没有选择 Vue 和 React 技术栈,而是采用的 JavaScript 和 jQuery 构建,支持 Docker 一键部署和在线使用。

GitHub 地址→
https://github.com/HeyPuter/puter

2.2 FastAPI 全栈模板:full-stack-fastapi-template

主语言:Python

Star:20k

周增长:4k

该项目是 FastAPI 作者开源的一个 FastAPI 的项目模板,包含完整的 FastAPI、React、PostgreSQL、Docker、HTTPS 等技术栈。提供了现成的 React 前端、单元测试、管理后台、JWT、邮件、Docker Compose 等,可用于快速开发基于 FastAPI 前后端分离的 Web 项目,多提一嘴我们的官网后端接口就是用它起的项目。

GitHub 地址→
https://github.com/tiangolo/full-stack-fastapi-template

2.3 用于调试 API 的开源 IDE:bruno

主语言:JavaScript

Star:16k

周增长:2k

这是一款仅限离线(无需登录)使用的 API 客户端桌面工具,可用来测试和请求 API。它不同于日益臃肿、同类型的 Postman 等工具,你可以直接在本地管理/导出接口信息和数据,没有杂七杂八的账号管理、云同步等功能,简单直接、开箱即用的 API 客户端,适用于 Windows、macOS 和 Linux 操作系统。

GitHub 地址→
https://github.com/usebruno/bruno

2.4 Markdown 友好的开源笔记:joplin

主语言:TypeScript

Star:42k

这是一款免费的开源笔记软件,能够方便地管理待办事项和处理大量笔记。可以直接导入印象笔记(Evernote)的笔记和 Markdown 文件,提供了 Windows、macOS、Linux、Android 和 iOS 等主流操作系统客户端。它不仅支持离线使用,同时也支持自定义网盘同步笔记,从而实现多端无缝创作。

GitHub 地址→
https://github.com/laurent22/joplin

2.5 让 AI 更好用的框架:fabric

主语言:Python

Star:8.5k

周增长:2k

该项目是一个使用 AI 增强人类能力的框架,即更好地用 AI 来应对日常挑战的工具。本质上它是一个 Sever,用 Markdown 的形式,提供了一系列更加准确的提示词模式,让 AI 更加精细和准确地处理问题,返回你想要的结果,比如用它总结新闻、创建摘要、解释代码等,都有不错的效果。

GitHub 地址→
https://github.com/danielmiessler/fabric

3. HelloGitHub 热评

在这个章节,将会分享下本周 HelloGitHub 网站上的热门开源项目,欢迎与我们分享你上手这些开源项目后的使用体验。

3.1 在 Android 上运行 Windows 游戏的模拟器

主语言:Java

这是一个 Android 应用,可以让你使用 Wine 和 Box86/Box64 来运行 Windows 应用和游戏,实现在手机上畅玩各种经典的 PC 游戏。

项目详情→
https://hellogithub.com/repository/d654ac45ade64c2cac5f8211a2ab720c

3.2 自动解密/解码各种加密算法的工具

主语言:Python

使用该项目时,你只需输入加密的文本,无需提供具体的加密类型,它就可以在 3 秒或更短的时间内自动解密大多数的加密文本。这个项目支持 50 多种常见的加密/编码方式,包括二进制、base64、哈希和凯撒密码等。

项目详情→
https://hellogithub.com/repository/b1f15bfc21704b398684f4bfc1f8f4c9

4. 往期回顾

往期回顾:

以上为本周的「GitHub 热点速递」如果你发现其他好玩、实用的 GitHub 项目,就来
HelloGitHub
和大家一起分享下吧。

当我们使用System.Text.Json.JsonSerializer对一个字典对象进行序列化的时候,默认情况下字典的Key不能是一个自定义的类型,本文介绍几种解决方案。

一、问题重现
二、自定义JsonConverter能解决吗?
三、自定义TypeConverter能解决问题吗?
四、以键值对集合的形式序列化
五、转换成合法的字典
六、自定义读写

一、问题重现

我们先通过如下这个简单的例子来重现上述这个问题。如代码片段所示,我们定义了一个名为Point(代表二维坐标点)的只读结构体作为待序列化字典的Key。Point可以通过结构化的表达式来表示,我们同时还定义了Parse方法将表达式转换成Point对象。

using System.Diagnostics;
using System.Text.Json;

var dictionary = new Dictionary<Point, int>
{
    { new Point(1.0, 1.0), 1 },
    { new Point(2.0, 2.0), 2 },
    { new Point(3.0, 3.0), 3 }
};

try
{
    var json = JsonSerializer.Serialize(dictionary);
    Console.WriteLine(json);

    var dictionary2 = JsonSerializer.Deserialize<Dictionary<Point, int>>(json)!;
    Debug.Assert(dictionary2[new Point(1.0, 1.0)] == 1);
    Debug.Assert(dictionary2[new Point(2.0, 2.0)] == 2);
    Debug.Assert(dictionary2[new Point(3.0, 3.0)] == 3);
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}


public readonly record struct Point(double X, double Y)
{
    public override string ToString()=> $"({X}, {Y})";
    public static Point Parse(string s)
    {
        var tokens = s.Trim('(',')').Split(',', StringSplitOptions.TrimEntries);
        if (tokens.Length != 2)
        {
            throw new FormatException("Invalid format");
        }
        return new Point(double.Parse(tokens[0]), double.Parse(tokens[1]));
    }
}

当我们使用JsonSerializer序列化多一个Dictionary<Point,
int
>类型的对象时,会抛出一个NotSupportedException异常,如下所示的信息解释了错误的根源:Point类型不能作为被序列化字典对象的Key。顺便说一下,如果使用Newtonsoft.Json,这样的字典可以序列化成功,但是反序列化会失败。

image

二、自定义JsonConverter<Point>能解决吗?

遇到这样的问题我们首先想到的是:既然不执行针对Point的序列化/反序列化,那么我们可以对应相应的JsonConverter自行完成序列化/反序列化工作。为此我们定义了如下这个PointConverter,将Point的表达式作为序列化输出结果,同时调用Parse方法生成反序列化的结果。

public class PointConverter : JsonConverter<Point>
{
    public override Point Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)=> Point.Parse(reader.GetString()!);
    public override void Write(Utf8JsonWriter writer, Point value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString());
}

我们将这个PointConverter对象添加到创建的JsonSerializerOptions配置选项中,并将后者传入序列化和反序列化方法中。

var options = new JsonSerializerOptions
{
    WriteIndented = true,
    Converters = { new PointConverter() }
};
var json = JsonSerializer.Serialize(dictionary, options);
Console.WriteLine(json);

var dictionary2 = JsonSerializer.Deserialize<Dictionary<Point, int>>(json, options)!;
Debug.Assert(dictionary2[new Point(1.0, 1.0)] == 1);
Debug.Assert(dictionary2[new Point(2.0, 2.0)] == 2);
Debug.Assert(dictionary2[new Point(3.0, 3.0)] == 3);

不幸的是,这样的解决方案无效,序列化时依然会抛出相同的异常。

image

三、自定义TypeConverter能解决问题吗?

JsonConverter的目的本质上就是希望将Point对象视为字符串进行处理,既然自定义JsonConverter无法解决这个问题,我们是否可以注册相应的类型转换其来解决它呢?为此我们定义了如下这个PointTypeConverter 类型,使它来完成针对Point和字符串之间的类型转换。

public class PointTypeConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string);
    public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) => destinationType == typeof(string);
    public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) => Point.Parse((string)value);
    public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) => value?.ToString()!;
}

我们利用标注的TypeConverterAttribute特性将PointTypeConverter注册到Point类型上。

[TypeConverter(typeof(PointTypeConverter))]
public readonly record struct Point(double X, double Y)
{
    public override string ToString() => $"({X}, {Y})";
    public static Point Parse(string s)
    {
        var tokens = s.Trim('(',')').Split(',', StringSplitOptions.TrimEntries);
        if (tokens.Length != 2)
        {
            throw new FormatException("Invalid format");
        }
        return new Point(double.Parse(tokens[0]), double.Parse(tokens[1]));
    }
}

实验证明,这种解决方案依然无效,序列化时还是会抛出相同的异常。顺便说一下,这种解决方案对于Newtonsoft.Json是适用的。

image

四、以键值对集合的形式序列化

为Point定义JsonConverter之所以不能解决我们的问题,是因为异常并不是在试图序列化Point对象时抛出来的,而是在在默认的规则序列化字典对象时,不合法的Key类型没有通过验证。如果希望通过自定义JsonConverter的方式来解决,目标类型不应该时Point类型,而应该时字典类型,为此我们定义了如下这个PointKeyedDictionaryConverter<TValue>类型。

我们知道字典本质上就是键值对的集合,而集合针对元素类型并没有特殊的约束,所以我们完全可以按照键值对集合的方式来进行序列化和反序列化。如代码把片段所示,用于序列化的Write方法中,我们利用作为参数的JsonSerializerOptions 得到针对IEnumerable<KeyValuePair<Point, TValue>>类型的JsonConverter,并利用它以键值对的形式对字典进行序列化。

public class PointKeyedDictionaryConverter<TValue> : JsonConverter<Dictionary<Point, TValue>>
{
    public override Dictionary<Point, TValue>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var enumerableConverter = (JsonConverter<IEnumerable<KeyValuePair<Point, TValue>>>)options.GetConverter(typeof(IEnumerable<KeyValuePair<Point, TValue>>));
        return enumerableConverter.Read(ref reader, typeof(IEnumerable<KeyValuePair<Point, TValue>>), options)?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
    }
    public override void Write(Utf8JsonWriter writer, Dictionary<Point, TValue> value, JsonSerializerOptions options)
    {
        var enumerableConverter = (JsonConverter<IEnumerable<KeyValuePair<Point, TValue>>>)options.GetConverter(typeof(IEnumerable<KeyValuePair<Point, TValue>>));
        enumerableConverter.Write(writer, value, options);
    }
}

用于反序列化的Read方法中,我们采用相同的方式得到这个针对IEnumerable<KeyValuePair<Point, TValue>>类型的JsonConverter,并将其反序列化成键值对集合,在转换成返回的字典。

var options = new JsonSerializerOptions
{
    WriteIndented = true,
    Converters = { new PointConverter(), new PointKeyedDictionaryConverter<int>()}
};

我们将PointKeyedDictionaryConverter<
int
>添加到创建的JsonSerializerOptions配置选项的JsonConverter列表中。从如下所示的输出结果可以看出,我们创建的字典确实是以键值对集合的形式进行序列化的。

image

五、转换成合法的字典

既然作为字典Key的Point可以转换成字符串,那么可以还有另一种解法,那就是将以Point为Key的字典转换成以字符串为Key的字典,为此我们按照如下的方式重写的PointKeyedDictionaryConverter<TValue>。如代码片段所示,重写的Writer方法利用传入的JsonSerializerOptions配置选项得到针对Dictionary<
string
, TValue>的JsonConverter,然后将待序列化的Dictionary<Point, TValue> 对象转换成Dictionary<string, TValue> 交给它进行序列化。

public class PointKeyedDictionaryConverter<TValue> : JsonConverter<Dictionary<Point, TValue>>
{
    public override Dictionary<Point, TValue>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var converter = (JsonConverter<Dictionary<string, TValue>>)options.GetConverter(typeof(Dictionary<string, TValue>))!;
        return converter.Read(ref reader, typeof(Dictionary<string, TValue>), options)
            ?.ToDictionary(kv => Point.Parse(kv.Key), kv=> kv.Value);
    }
    public override void Write(Utf8JsonWriter writer, Dictionary<Point, TValue> value, JsonSerializerOptions options)
    {
        var converter = (JsonConverter<Dictionary<string, TValue>>)options.GetConverter(typeof(Dictionary<string, TValue>))!;
        converter.Write(writer, value.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value), options);
    }
}

重写的Read方法采用相同的方式得到JsonConverter<Dictionary<
string
, TValue>>对象,并利用它执行反序列化生成Dictionary<string, TValue> 对象。我们最终将它转换成需要的Dictionary<Point, TValue> 对象。从如下所示的输出可以看出,这次的序列化生成的JSON会更加精炼,因为这次是以字典类型输出JSON字符串的。

image

六、自定义读写

虽然以上两种方式都能解决我们的问题,而且从最终JSON字符串输出的长度来看,第二种具有更好的性能,但是它们都有一个问题,那么就是需要
创建中间对象
。第一种方案需要创建一个键值对集合,第二种方案则需要创建一个Dictionary<string, TValue> 对象,如果对性能有更高的追求,它们都不是一种好的解决方案。既让我们都已经在自定义JsonConverter,完全可以自行可控制JSON内容的读写,为此我们再次重写了PointKeyedDictionaryConverter<TValue>。

public class PointKeyedDictionaryConverter<TValue> : JsonConverter<Dictionary<Point, TValue>>
{
    public override Dictionary<Point, TValue>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        JsonConverter<TValue>? valueConverter = null;
        Dictionary<Point, TValue>? dictionary = null;
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
            {
                return dictionary;
            }
            valueConverter ??= (JsonConverter<TValue>)options.GetConverter(typeof(TValue))!;
            dictionary ??= [];
            var key = Point.Parse(reader.GetString()!);
            reader.Read();
            var value = valueConverter.Read(ref reader, typeof(TValue), options)!;
            dictionary.Add(key, value);
        }
        return dictionary;
    }
    public override void Write(Utf8JsonWriter writer, Dictionary<Point, TValue> value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        JsonConverter<TValue>? valueConverter = null;
        foreach (var (k, v) in value)
        {
            valueConverter ??= (JsonConverter<TValue>)options.GetConverter(typeof(TValue))!;
            writer.WritePropertyName(k.ToString());
            valueConverter.Write(writer, v, options);
        }
        writer.WriteEndObject();
    }
}

如上面的代码片段所示,在重写的Write方法中,我们调用Utf8JsonWriter 的WriteStartObject和 WriteEndObject方法以对象的形式输出字典。在这中间,我们便利字典的每个键值对,并以“属性”的形式对它们进行输出(Key和Value分别是属性名和值)。在Read方法中,我们创建一个空的Dictionary<Point, TValue> 对象,在一个循环中利用Utf8JsonReader先后读取作为Key的字符串和Value值,最终将Key转换成Point类型,并添加到创建的字典中。从如下所示的输出结果可以看出,这次生成的JSON具有与上面相同的结构。

image

写在开头

在之前的几篇博文中,我们都提到了
volatile
关键字,这个单词中文释义为:不稳定的,易挥发的,在Java中代表变量修饰符,用来修饰会被不同线程访问和修改的变量,对于方法,代码块,方法参数,局部变量以及实例常量,类常量多不能进行修饰。

自JDK1.5之后,官网对volatile进行了语义增强,这让它在Java多线程领域越发重要!因此,我们今天就抽一晚上时间,来学一学这个关键字,首先,我们从标题入手,思考这样的一个问题:

volatile如何保证可见性,具体如何实现的?

带着疑问,我们继续往下阅读!

volatile如何保证可见性

volatile保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了共享变量的值,共享变量修改后的值对其他线程立即可见。

我们先通过之前写的一个小案例来感受一下什么是可见性问题:

【代码示例1】

public class Test {
    //是否停止 变量
    private static boolean stop = false;
    public static void main(String[] args) throws InterruptedException {
        //启动线程 1,当 stop 为 true,结束循环
        new Thread(() -> {
            System.out.println("线程 1 正在运行...");
            while (!stop) ;
            System.out.println("线程 1 终止");
        }).start();
        //休眠 1 秒
        Thread.sleep(1000);
        //启动线程 2, 设置 stop = true
        new Thread(() -> {
            System.out.println("线程 2 正在运行...");
            stop = true;
            System.out.println("设置 stop 变量为 true.");
        }).start();
    }
}

输出:

线程 1 正在运行...
线程 2 正在运行...
设置 stop 变量为 true.

原因:
我们会发现,线程1运行起来后,休眠1秒,启动线程2,可即便线程2把stop设置为true了,线程1仍然没有停止,这个就是因为 CPU 缓存导致的可见性导致的问题。线程 2 设置 stop 变量为 true,线程 1 在 CPU 1上执行,读取的 CPU 1 缓存中的 stop 变量仍然为 false,线程 1 一直在循环执行。
image

那这个问题怎么解决呢?很好解决!我们排volatile上场可以秒搞定,只需要给stop变量加上volatile修饰符即可!

【代码示例2】

//给stop变量增加volatile修饰符
private static volatile boolean stop = false;

输出:

线程 1 正在运行...
线程 2 正在运行...
设置 stop 变量为 true.
线程 1 终止

从结果中看,线程1成功的读取到了线程而设置为true的stop变量值,解决了可见性问题。那volatile到底是什么让变量在多个线程之间保持可见性的呢?请看下图!
image

如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取,具体实现可总结为5步。

  • 1️⃣在生成最低成汇编指令时,对volatile修饰的共享变量写操作增加Lock前缀指令,Lock 前缀的指令会引起 CPU 缓存写回内存;
  • 2️⃣CPU 的缓存回写到内存会导致其他 CPU 缓存了该内存地址的数据无效;
  • 3️⃣volatile 变量通过缓存一致性协议保证每个线程获得最新值;
  • 4️⃣缓存一致性协议保证每个 CPU 通过嗅探在总线上传播的数据来检查自己缓存的值是不是修改;
  • 5️⃣当 CPU 发现自己缓存行对应的内存地址被修改,会将当前 CPU 的缓存行设置成无效状态,重新从内存中把数据读到 CPU 缓存。

总结

其实volatile关键字不仅仅能解决可见性问题,还可以通过禁止编译器、CPU 指令重排序和部分 happens-before 规则,解决有序性问题,我们放在下一篇聊。

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得
留言+点赞+收藏
呀。原创不易,转载请联系Build哥!

image

如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

image

前言

从.Net Core 开始,.Net 平台内置了一个轻量,易用的 IOC 的框架,供我们在应用程序中使用,社区内还有很多强大的第三方的依赖注入框架如:

内置的依赖注入容器基本可以满足大多数应用的需求,除非你需要的特定功能不受它支持否则不建议使用第三方的容器。

我们今天介绍的主角
Scrutor
是内置依赖注入的一个强大的扩展,
Scrutor
有两个核心的功能:一是程序集的批量注入
Scanning
,二是
Decoration
装饰器模式,今天的主题是
Scanning

开始之前在项目中安装 nuget 包:

Install-Package Scrutor

学习
Scrutor
前我们先熟悉一个
.Net
依赖注入的万能用法。

builder.Services.Add(
    new ServiceDescriptor(/*"ServiceType"*/typeof(ISampleService), /*"implementationType"*/typeof(SampleService), ServiceLifetime.Transient)
    );

第一个参数
ServiceType
通常用接口表示,第二个
implementationType
接口的实现,最后生命周期,熟悉了这个后面的逻辑理解起来就容易些。

Scrutor
官方仓库和本文完整的源代码在文末

Scanning

Scrutor
提供了一个
IServiceCollection
的扩展方法作为批量注入的入口,该方法提供了
Action<ITypeSourceSelector>
委托参数。

builder.Services.Scan(typeSourceSelector => { });

我们所有的配置都是在这个委托内完成的,Setup by Setup 剖析一下这个使用过程。

第一步 获取 types

typeSourceSelector
支持程序集反射获取类型和提供类型参数

程序集选择

ITypeSourceSelector
有多种获取程序集的方法来简化我们选择程序集


 typeSourceSelector.FromAssemblyOf<Program>();//根据泛型反射获取所在的程序集


typeSourceSelector.FromCallingAssembly();//获取开始发起调用方法的程序集


typeSourceSelector.FromEntryAssembly();//获取应用程序入口点所在的程序集


typeSourceSelector.FromApplicationDependencies();//获取应用程序及其依赖项的程序集


typeSourceSelector.FromDependencyContext(DependencyContext.Default);//根据依赖关系上下文(DependencyContext)中的运行时库(Runtime Library)列表。它返回一个包含了所有运行时库信息的集合。


typeSourceSelector.FromAssembliesOf(typeof(Program));//根据类型获取程序集的集合


typeSourceSelector.FromAssemblies(Assembly.Load("dotNetParadise-Scrutor.dll"));//提供程序集支持Params或者IEnumerable

第二步 从 Types 中选择 ImplementationType

简而言之就是从程序中获取的所有的
types
进行过滤,比如获取的
ImplementationType
必须是非抽象的,是类,是否只需要
Public
等,还可以用
ImplementationTypeFilter
提供的扩展方法等


builder.Services.Scan(typeSourceSelector =>
{
    typeSourceSelector.FromEntryAssembly().AddClasses();
});

AddClasses()
方法默认获取所有公开非抽象的类


还可以通过
AddClasses
的委托参数来进行更多条件的过滤
比如定义一个
Attribute
,忽略
IgnoreInjectAttribute

namespace dotNetParadise_Scrutor;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class IgnoreInjectAttribute : Attribute
{
}
builder.Services.Scan(typeSourceSelector =>
{
    typeSourceSelector.FromEntryAssembly().AddClasses(iImplementationTypeFilter =>
    {
        iImplementationTypeFilter.WithoutAttribute<IgnoreInjectAttribute>();
    });
});

利用
iImplementationTypeFilter
的扩展方法很简单就可以实现

在比如 我只要想实现
IApplicationService
接口的类才可以被注入

namespace dotNetParadise_Scrutor;

/// <summary>
/// 依赖注入标记接口
/// </summary>
public interface IApplicationService
{
}
builder.Services.Scan(typeSourceSelector =>
{
    typeSourceSelector.FromEntryAssembly().AddClasses(iImplementationTypeFilter =>
    {
        iImplementationTypeFilter.WithoutAttribute<IgnoreInjectAttribute>().AssignableTo<IApplicationService>();
    });
});

类似功能还有很多,如可以根据
命名空间
也可以根据
Type
的属性用
lambda
表达式对
ImplementationType
进行过滤


上面的一波操作实际上就是为了构造一个
IServiceTypeSelector
对象,选出来的
ImplementationType
对象保存了到了
ServiceTypeSelector

Types
属性中供下一步选择。
除了提供程序集的方式外还可以直接提供类型的方式比如

创建接口和实现

public interface IForTypeService
{
}
public class ForTypeService : IForTypeService
{
}
builder.Services.Scan(typeSourceSelector =>
{
    typeSourceSelector.FromTypes(typeof(ForTypeService));
});

这种方式提供类型内部会调用
AddClass()
方法把符合条件的参数保存到
ServiceTypeSelector

第三步确定注册策略


AddClass
之后可以调用
UsingRegistrationStrategy()
配置注册策略是
Append

Skip

Throw

Replace
下面是各个模式的详细解释

  • RegistrationStrategy.Append :类似于
    builder.Services.Add
  • RegistrationStrategy.Skip:类似于
    builder.Services.TryAdd
  • RegistrationStrategy.Throw:ServiceDescriptor 重复则跑异常
  • RegistrationStrategy.Replace: 替换原有服务

这样可以灵活地控制注册流程

    builder.Services.Scan(typeSourceSelector =>
    {
        typeSourceSelector.FromEntryAssembly().AddClasses().UsingRegistrationStrategy(RegistrationStrategy.Skip);
    });

不指定则为默认的 Append 即 builder.Services.Add

第四步 配置注册的场景选择合适的
ServiceType

ServiceTypeSelector
提供了多种方法让我们从
ImplementationType
中匹配
ServiceType

  • AsSelf()
  • As<T>()
  • As(params Type[] types)
  • As(IEnumerable<Type> types)
  • AsImplementedInterfaces()
  • AsImplementedInterfaces(Func<Type, bool> predicate)
  • AsSelfWithInterfaces()
  • AsSelfWithInterfaces(Func<Type, bool> predicate)
  • AsMatchingInterface()
  • AsMatchingInterface(Action<Type, IImplementationTypeFilter>? action)
  • As(Func<Type, IEnumerable<Type>> selector)
  • UsingAttributes()


AsSelf
注册自身

public class AsSelfService
{
}
{
    builder.Services.Scan(typeSourceSelector =>
    {
        typeSourceSelector.FromEntryAssembly().AddClasses(iImplementationTypeFilter =>
        {
            iImplementationTypeFilter.InNamespaces("dotNetParadise_Scrutor.Application.AsSelf").WithoutAttribute<IgnoreInjectAttribute>();
        }).AsSelf();
    });

    Debug.Assert(builder.Services.Any(_ => _.ServiceType == typeof(AsSelfService)));
}

等效于
builder.Services.AddTransient<AsSelfService>();


As
批量为
ImplementationType
指定
ServiceType

public interface IAsService
{
}
public class AsOneService : IAsService
{
}
public class AsTwoService : IAsService
{
}
{
    builder.Services.Scan(typeSourceSelector =>
{
    typeSourceSelector.FromEntryAssembly().AddClasses(iImplementationTypeFilter =>
    {
        iImplementationTypeFilter.InNamespaces("dotNetParadise_Scrutor.Application.As").WithoutAttribute<IgnoreInjectAttribute>();
    }).As<IAsService>();
});
    Debug.Assert(builder.Services.Any(_ => _.ServiceType == typeof(IAsService)));
    foreach (var asService in builder.Services.Where(_ => _.ServiceType == typeof(IAsService)))
    {
        Debug.WriteLine(asService.ImplementationType!.Name);
    }
}


As(params Type[] types)和 As(IEnumerable types)
批量为
ImplementationType
指定多个
ServiceType
,服务必须同时实现这里面的所有的接口

上面的实例再改进一下

public interface IAsOtherService
{
}
public interface IAsSomeService
{
}

public class AsOneMoreTypesService : IAsOtherService, IAsSomeService
{
}
public class AsTwoMoreTypesService : IAsSomeService, IAsOtherService
{
}

{
    builder.Services.Scan(typeSourceSelector =>
    {
        typeSourceSelector.FromEntryAssembly().AddClasses(iImplementationTypeFilter =>
        {
            iImplementationTypeFilter.InNamespaces("dotNetParadise_Scrutor.Application.AsMoreTypes").WithoutAttribute<IgnoreInjectAttribute>();
        }).As(typeof(IAsSomeService), typeof(IAsOtherService));
    });
    List<Type> serviceTypes = [typeof(IAsSomeService), typeof(IAsOtherService)];
    Debug.Assert(serviceTypes.All(serviceType => builder.Services.Any(service => service.ServiceType == serviceType)));
    foreach (var asService in builder.Services.Where(_ => _.ServiceType == typeof(IAsSomeService) || _.ServiceType == typeof(IAsOtherService)))
    {
        Debug.WriteLine(asService.ImplementationType!.Name);
    }
}


AsImplementedInterfaces
注册当前
ImplementationType
和实现的接口

public interface IAsImplementedInterfacesService
{
}
public class AsImplementedInterfacesService : IAsImplementedInterfacesService
{
}
//AsImplementedInterfaces 注册当前ImplementationType和它实现的接口
{
    builder.Services.Scan(typeSourceSelector =>
    {
        typeSourceSelector.FromEntryAssembly().AddClasses(iImplementationTypeFilter =>
        {
            iImplementationTypeFilter.InNamespaces("dotNetParadise_Scrutor.Application.AsImplementedInterfaces").WithoutAttribute<IgnoreInjectAttribute>();
        }).AsImplementedInterfaces();
    });

    Debug.Assert(builder.Services.Any(service => service.ServiceType == typeof(IAsImplementedInterfacesService)));
    foreach (var asService in builder.Services.Where(_ => _.ServiceType == typeof(IAsImplementedInterfacesService)))
    {
        Debug.WriteLine(asService.ImplementationType!.Name);
    }
}


AsSelfWithInterfaces
同时注册为自身类型和所有实现的接口

public interface IAsSelfWithInterfacesService
{
}
public class AsSelfWithInterfacesService : IAsSelfWithInterfacesService
{
}

{
    builder.Services.Scan(typeSourceSelector =>
    {
        typeSourceSelector.FromEntryAssembly().AddClasses(iImplementationTypeFilter =>
        {
            iImplementationTypeFilter.InNamespaces("dotNetParadise_Scrutor.Application.AsSelfWithInterfaces").WithoutAttribute<IgnoreInjectAttribute>();
        }).AsSelfWithInterfaces();
    });
    //Self
    Debug.Assert(builder.Services.Any(service => service.ServiceType == typeof(AsSelfWithInterfacesService)));
    //Interfaces
    Debug.Assert(builder.Services.Any(service => service.ServiceType == typeof(IAsSelfWithInterfacesService)));
    foreach (var service in builder.Services.Where(_ => _.ServiceType == typeof(AsSelfWithInterfacesService) || _.ServiceType == typeof(IAsSelfWithInterfacesService)))
    {
        Debug.WriteLine(service.ServiceType!.Name);
    }
}


AsMatchingInterface
将服务注册为与其命名相匹配的接口,可以理解为一定约定假如服务名称为
ClassName
,会找
IClassName
的接口作为
ServiceType
注册

public interface IAsMatchingInterfaceService
{
}
public class AsMatchingInterfaceService : IAsMatchingInterfaceService
{
}
//AsMatchingInterface 将服务注册为与其命名相匹配的接口,可以理解为一定约定假如服务名称为 ClassName,会找 IClassName 的接口作为 ServiceType 注册
{
    builder.Services.Scan(typeSourceSelector =>
    {
        typeSourceSelector.FromEntryAssembly().AddClasses(iImplementationTypeFilter =>
        {
            iImplementationTypeFilter.InNamespaces("dotNetParadise_Scrutor.Application.AsMatchingInterface").WithoutAttribute<IgnoreInjectAttribute>();
        }).AsMatchingInterface();
    });
    Debug.Assert(builder.Services.Any(service => service.ServiceType == typeof(IAsMatchingInterfaceService)));
    foreach (var service in builder.Services.Where(_ => _.ServiceType == typeof(IAsMatchingInterfaceService)))
    {
        Debug.WriteLine(service.ServiceType!.Name);
    }
}


UsingAttributes
特性注入,这个还是很实用的在
Scrutor
提供了
ServiceDescriptorAttribute
来帮助我们方便的对
Class
进行标记方便注入

public interface IUsingAttributesService
{
}

[ServiceDescriptor<IUsingAttributesService>()]
public class UsingAttributesService : IUsingAttributesService
{
}
    builder.Services.Scan(typeSourceSelector =>
    {
        typeSourceSelector.FromEntryAssembly().AddClasses(iImplementationTypeFilter =>
        {
            iImplementationTypeFilter.InNamespaces("dotNetParadise_Scrutor.Application.UsingAttributes").WithoutAttribute<IgnoreInjectAttribute>();
        }).UsingAttributes();
    });
    Debug.Assert(builder.Services.Any(service => service.ServiceType == typeof(IUsingAttributesService)));
    foreach (var service in builder.Services.Where(_ => _.ServiceType == typeof(IUsingAttributesService)))
    {
        Debug.WriteLine(service.ServiceType!.Name);
    }

第五步 配置生命周期

通过链式调用
WithLifetime
函数来确定我们的生命周期,默认是
Transient

public interface IFullService
{
}
public class FullService : IFullService
{
}
{

    builder.Services.Scan(typeSourceSelector =>
    {
        typeSourceSelector.FromEntryAssembly().AddClasses(iImplementationTypeFilter =>
        {
            iImplementationTypeFilter.InNamespaces("dotNetParadise_Scrutor.Application.Full");
        }).UsingRegistrationStrategy(RegistrationStrategy.Skip).AsImplementedInterfaces().WithLifetime(ServiceLifetime.Scoped);
    });

    Debug.Assert(builder.Services.Any(service => service.ServiceType == typeof(IFullService)));
    foreach (var service in builder.Services.Where(_ => _.ServiceType == typeof(IFullService)))
    {
        Debug.WriteLine($"serviceType:{service.ServiceType!.Name},LifeTime:{service.Lifetime}");
    }
}

总结

到这儿基本的功能已经介绍完了,可以看出来扩展方法很多,基本可以满足开发过程批量依赖注入的大部分场景。
使用技巧总结:

  • 根据程序集获取所有的类型 此时
    Scrutor
    会返回一个
    IImplementationTypeSelector
    对象里面包含了程序集的所有类型集合
  • 调用
    IImplementationTypeSelector

    AddClasses
    方法获取
    IServiceTypeSelector
    对象,
    AddClass
    这里面可以根据条件选择 过滤一些不需要的类型
  • 调用
    UsingRegistrationStrategy
    确定依赖注入的策略 是覆盖 还是跳过亦或是抛出异常 默认
    Append
    追加注入的方式
  • 配置注册的场景 比如是
    AsImplementedInterfaces
    还是
    AsSelf
  • 选择生命周期 默认
    Transient

借助
ServiceDescriptorAttribute
更简单,生命周期和
ServiceType
都是在
Attribute
指定好的只需要确定选择程序集,调用
UsingRegistrationStrategy
配置依赖注入的策略然后
UsingAttributes()
即可

最后

本文从
Scrutor
的使用流程剖析了依赖注入批量注册的流程,更详细的教程可以参考
Github 官方仓库
。在开发过程中看到很多项目还有一个个手动注入的,也有自己写
Interface
或者是
Attribute
反射注入的,支持的场景都十分有限,
Scrutor
的出现就是为了避免我们在项目中不停地造轮子,达到开箱即用的目的。

本文
完整示例源代码

继去年上半年一鼓作气研究了几种不同的模版匹配算法后,这个方面的工作基本停滞了有七八个月没有去碰了,因为感觉已经遇到了瓶颈,无论是速度还是效率方面,以当时的理解感觉都到了顶了。年初,公司业务惨淡,也无心向佛,总要找点事情做一做,充实下自己,这里选择了前期一直想继续研究的基于离散夹角余弦相似度指标的形状匹配优化。

在前序的一些列文章里,我们也描述了我从linemod模型里抽取的一种相似度指标用于形状匹配,个人取名为离散夹角余弦,其核心是将传统的基于梯度点积相似度的的指标进行了离散化:

传统的梯度点积计算公式如下:

对于任意的两个点,通过各自的梯度方向,按照上述公式可计算出他们的相似度。

那么离散夹角余弦的区别是,不直接计算两个梯度方向的余弦,而是提前进行一些定点化,比如,早期的linemod就是把360度角分为了8份,每份45度,这样[0,45]的梯度方向标记为0,[45,90]的梯度方向标记为1,依次类推,直到[315, 360]之间的梯度方向标记为7,这样共有8个标记,然后提前构建好8个标记之间的一个得分表,比如,下面这样的一个表:





这个表的意思也很简单,就是描述不同标记之间的得分,比如两个点的梯度方向,都为3或者4或者5,那么他们的得分就比较高,可以取8,如果一个为1,一个为6,则得分就只有2, 一个为0,一个为5,则得分为0(即完全相反的两个方向)。


很明显,角度量化的越细,则得到的结果越和传统的梯度点积结果越接近。但是计算量可能也就越大,


关于这个过程,我去年的版本也有弄过8角度、16角度及32角度,个人觉得,在目前的计算机框架下,16角度应该是既能满足进度要求,又能在速度方面更为完美的一个选择。

这里记录下最近对基于16角度离散余弦夹角指标的形状匹配的进一步优化过程。

一、核心的优化策略

通过前面的描述,我们知道,这种方法的得分是通过查表获取的,而且,在大部分的计算中,是没有涉及到浮点计算的,我们通过适当的构造表的内容,可以通过简单的整数类型的加减乘除来得到最后的得分。这个的好处有很多,其中一个就是精度问题,在基于梯度点积的计算中,如果采用float类型来累计计算结果,通常或多或少的存在某些情况下的精度丢失,而且还不好定位哪里有问题。还有个好处就是真的可以加速,当然这个的加速主要是通过一个很特殊,但是又很有效的SSE指令集语句实现的,这个指令就是_mm_shuffle_epi8,其原型及相关介绍如下:

__m128i
_mm_shuffle_epi8 (
__m128i
a,
__m128i
b)

这是个很牛逼的东西,如果我们把a看成一个16个元素的字节数组,b也是一个16个元素的字节数据,则简单的理解他就是实现下述功能:

dst[i] = a[b[i]];  即一个16个元素的查表功能。

很明显,b[i]要在0到15之间才有效,否则,就查不到元素了,但是该指令还有比较隐藏的功能是,当b[i]大于15时,dst就返回0了。

为什么说这个指令牛逼,我们看我们前面说的这个获取离散夹角余弦的过程,对于两个等面积的区域,假定一个区域的量化后的离散角度标记保存在QuantizedAngleT内存中,另外一个保存在QuantizedAngle内存中,他们的宽和高分别为ValidW和ValidH,则这两个区域按照前面定义的标准,其得分可用下述代码表示(这里SimilarityLut是16角度离散的):

int Score = 0;for (int Y = 0; Y < ValidH; Y++)
{
int Index = Y *ValidW;for (int X = 0; X < ValidW; X++)
{
Score
+= SimilarityLut[QuantizedAngleT[Index + X] * 16 +QuantizedAngle[Index + X]];//Score += SimilarityLut[QuantizedAngleT[Index + X], QuantizedAngle[Index + X]]; }
}

如果把SimilarityLut看成是二维的数组,上面注释掉的得分可能看起来更为清晰。

实际上,我们在进行模版匹配的时候大部分都是在进行这样的得分计算,因此,如果上面的过程能够提速,那么整体将提速很多。

通常,查表的算法是无法进行指令集优化的(AVX2的gather虽然有一定效果,但是弄的不好会适得其反),但是,正是因为我们本例的特殊性,使得这个查表反而更有利于算法的性能提高。

注意到前面有说过我们在进行16角度量化时,量化的标记范围是[0,15],意味着什么,这个正好是_mm_shuffle_epi8 指令里参数b的有效范围啊。

但是仔细看上面的SimilarityLut表,他由两个变量确定索引,这就有点麻烦了,解决的办法是换位思考,我们能不能固定其中的一个呢,这个就要结合我们的实际应用了。

在形状匹配中,我们提取了很多特征点,然后需要使用这些特征点对图像中有效区域范围的目标进行得分统计,也就是说任何一个位置,都要计算所有特征点的得分,并计总和,一个简单的表示为:

for (int Y = 0; Y < ValidH; Y++)
{
int Index = Y *ValidW;for (int X = 0; X < ValidW; X++)
{
int b =图像位置对应的量化值for (int Z = 0; Z < Template.PointAmount; Z++)
{
int a =模版位置对应的量化值
Score
+=SimilarityLut16[a, b];
}
}
}

这种情况a,b在每次独立的小循环中还是变化的,一样无法使用指令集。

不过,如果我们调换下循环的顺序,改为以下方式:

for (int Z = 0; Z < Template.PointAmount; Z++)
{
int a =模版位置对应的量化值for (int Y = 0; Y < ValidH; Y++)
{
int Index = Y *ValidW;for (int X = 0; X < ValidW; X++)
{
int b =图像位置对应的量化值
Score
+=SimilarityLut16[a, b];
}
}
}

此时,a在内部循环里是不变的,我们通过a之可以定位到SimilarityLut16的a*16地址处,并加载16字节内容,作为查找表的内容,而b值也可以一次性加载16个字节,作为查找的索引,这样一次性就能获得16个位置的得分了。

还有一点,我们在算法里有个最小对比度的东西, 这个东西是用来加快速度的,即梯度值小于这个数据,我们不要这个点参与到匹配中,即此时这个点的得分是0,为了标记这样的点,我们需要再原图的量化值里增加不在[0,15]范围内的东西,一旦有这个东西存在,我们的普通C代码里就需要添加类似这样的代码了:

if (QuantizedAngle[Index + X] != 255) Score += SimilarityLut[QuantizedAngleT[Index + X] * 16 + QuantizedAngle[Index + X]];

这里我使用了255这个不在[0,15]范围内的数字来表示这个点不需要参与匹配计算。本来说,如果刚刚那条_mm_shuffle_epi8指令,只是纯粹的实现[0,15]索引之间的查表,那有了这个东西,这个指令又就没法用了,但是恰恰这个指令能实现在索引不在0到15之间,可以返回其他值,而这个其他值恰好又是0,你们说是多么的巧合和神奇啊。所以,这一切都是为这个指令准备的。我们看看其他shuffle指令,包括_mm_shuffle_epi32,_mm_shuffle_ps,都没有这个附带的功能。

其实这里还是要交代一点,这个算法,如果遇到那种不能用指令集的机器,或者说用纯C语言去实现,效率就比较低了,因为C语言里只能直接查表,而且还要有那个判断。

二、特征点数量的展开即贪婪度参数的舍弃

linemod里使用8角度的特征,其两个特征之间的得分最大值为8,其内部使用了16位的加法,所以其最大的特征点数量为8096个,当模型较大时,往往会超出这个数量的特征点,特别是在最后面基层金字塔的时候,一种方案就是我们限制特征点的数量,并对特征点进行合适的提取,这也不失为一种方案。 那想要完美呢,就必须还得是上32位的加法。这里就存在一个问题,精度和速度如何同时保证,毕竟在SSE指令集的世界里,16位的加法是要比32位的加法快的。

一种解决方案就是,对特征点进行分区,我们按照可能超出16位能表达的最大范围时特征点的数量为区间大小,对特征点按顺序进行多区间划分,在每个小区间里还是用16位的指令集加法,完成一次后,把临时结果在加到32位的数据里。这样就精度和速度都能兼顾了,只不过代码又复杂了一些。

当我们尝试了这么多努力后,我们发现无论是顶层的得分计算,还是后续的每层的局部更新,其速度都变得飞快,这个时候我们又想到了一个贪婪度参数,这个东西,在论文有提到,可以提前结束循环,加快速度。可是,我也想尝试把这个东西加入到我这个算法的过程里,我发现他会破坏我整体的节奏,最终我们选择舍弃了这个参数,核心理由如下:

1、提前结束循环,是需要进行判断的,而且是每次都要判断,特别是对于后期的局部更新判断,因为大部分候选点都是能满足最小得分要求的,这部分的判断一般来说,基本上就无法满足了,也就是纯粹的多了这些无效判断。

2、因为我们使用了_mm_shuffle_epi8指令,一次性可以处理16个位置的得分,也就是这个粒度是16个像素,而如果使用SSE进行判断,也只有当16个位置都不满足最小得分要求后,才可以跳出循环,这个在很多情况下也是得不偿失的。

三、目标只有部分在图像内的识别

有些情况下,被识别的目标只有局部在图像范围内,而我们也期望能识别他,这个功能,我知道早期版本的halcon是没有的,他只能识别那些特征点完全在图像内的目标(不是模版图像边缘)。我早期版本也么有这个功能,后期有做过一些扩展,扩展的方法是通过扩大原始图像合适的范围,同时为了避免不增加新的边缘信息,扩展的部分都是用了边界的像素值。这样做在大部分情况下是能够解决问题的,不过其实也隐藏的一些不合理的地方,这些扩展的部分在细节上还是会产生额外的边缘的,只是不怎么明显。因此,这个版本,我也考虑了几个优化,在内部直接实现了边缘的扩展。

这里有几个技巧。

1、原图的金字塔图像还是不动。

2、计算原图每层金字塔图像的角度量化值时,对这个量化值进行扩展,扩展的部分的量化值填充前面说的那个不在[0,15]之间的无效值,比如这里是255,这样,这些区域的得分就是0。

3、为了能保证在极端情况下这些部分在图像的目标能被识别,扩展的大小要以特征有效值的外接矩形的对角线长度为基准,再进行适当的扩展(这个扩展也有特别要求)。

4、计算完成后,坐标值要进行相应的调整。

通过这种方式,可在内部实现缺失目标的识别,而且在内存占用、速度等方面也有一定的优势。

四、最小外接矩形识别重叠


halcon有说过其maxoverlap参数是通过计算特征点的最小外接矩形之间的重叠来实现的,在我以前的版本中,这个功能是通过其他的简易方法来搞定的。这个主要是以前搞不定最小外接矩形的计算,年初,恰好从opencv里翻译可扣取了一些代码,起重工就有最小外接矩形的获取,这个需要通过计算凸包以前其他一些复杂的东西搞定,我没有看懂原理,只是直接扣取了代码,不过CV的代码绕来绕去,扣的我也是头晕脑胀,总算搞出来了。

那么这里其实也有蛮多的细节和可选方案,我列举如下:

1、在创建特征时,计算好每个旋转后的特征的最小外接矩形(勾选了预生成模型数据)。

2、在最后确定的底层金字塔里所有的候选点出计算每个特征点对应的外接矩形。

3、只计算底层金字塔0角度是特征单的最小外接矩形,然后其他底层金字塔的最小外接矩形用他旋转得到。

我们实际考虑啊,方案一对创建模型不友好,方案二实际测试对运行的效率产生了不良影响,方案3最好,基本不耗时,而且对精度的影响也非常有限,所以可以选择方案3。

五、其他的一些我未公开的讨论的课题

1、16角度SimilarityLut的值如何设计,其实在halcon里有个metric参数,他有三种选择,使用极性、忽略全局极性、忽略局部极性。如果想前面给出的那种8角度的SimilarityLut,是只能实现使用极性和忽略全局极性的。这里适当扩展,就可以实现三个都有了,而且对是速度提升还有好处。

2、5*5局部得分过程的特别优化,尤其是如何高效的加载每行5个字节,并拼接成合适的形式,使得能快速的使用指令集。

3、也可以使用8*8的局部区域(非对称的局部更新),这样方便使用指令集,但是因为数量变大,还是没有优化后的5*5快。

4、在最顶层,计算候选点时,可以直接计算,也可以考虑使用ResponseMaps,其中,测试表明还是使用ResponseMaps快一点。

5、还是候选点的选择问题,在最顶层,目前我还是用的某个角度下的二维得分结果中选择得分大于最小得分要求,同时是5*5领域的最大值作为候选点,这种方式留下的候选点还是有很多的,对于只有旋转的匹配,是否可以考虑在3D(X方向、Y方向以及角度方向)的空间里,选得分大于最小得分要求且是5*5*5领域的最大值呢,这样候选点肯定会少很多,但是代码的编写似乎变得困难了很多,还有占用内存问题。那如果扩展到多尺度的匹配,或者是各项异性的匹配,那就要在4D和5D空间搜索最大值了,这个感觉就更为复杂了。

6、另外,再有顶层金字塔向下层金字塔迭代更新的过程中的候选点的舍弃问题,也值得探讨,到底是只根据得分是否满足需求,还是根据一些物理空间或者角度方面的特性做些特别的优化和舍弃呢,而且这种舍弃行为本身有的时候可能会带来性能的下降,因为在搜索那些目标可以舍弃时,需要一个循环,当候选目标有几千个的时候,两重这样的循环会对对后续候选点变少带来的性能加速起到反向作用,这个甚至会超过候选点变少带来的加速。所以   目前我这个版本为了稳定性,只是对得到的重复的点做了舍弃。

7、原本再想一个优化,即我们的特征点的保存顺序问题,现在只有0角度的特征点的保存顺序是X及Y方向都是由小向大方向坐标排列,这样访问的时候和图像的内存布局方向性一致,按理说cache  miss要小一些。但是,如果按照顺序把0角度的特征点旋转后,得到新的位置信息,一般来说肯定不是按照这样的顺序排列的了,所以访问时随机性就差了一些,那是否我再计算前把这些点再按0角度时那样做个排序后,有利于算法的性能呢,我做了实际的测试,应该说基本没啥区别吧。

六、结论

综合以上各种优化手段后,目前经过测试,速度较以前有很大的提高,而且和基于传统的梯度点积的方法比较,速度更具有优势,而且精度也不逞多让。我们测试在2500*2000的灰度中查找 300 * 150的多目标,大约耗时11ms, 传统的方式结合贪婪度耗时也需要18ms。这个时间我测试和halcon的比较已经非常接近了。当然这种比较还是要看具体的测试图。

从算法精度上看,怎么上定位也是很准确的,在执行过程中,占用的内存也不大,因此,个人觉得这个方法不失为一个优质的算子。

本文测试DEMO链接:
https://files.cnblogs.com/files/Imageshop/QuantizedOrientations_Matching.rar?t=1710810282&download=true

如果想时刻关注本人的最新文章,也可关注公众号或者添加本人微信:  laviewpbt