news 2026/10/21 2:47:15

C# EasyHook:.NET 环境下安全拦截 Windows 原生 API 的实践指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C# EasyHook:.NET 环境下安全拦截 Windows 原生 API 的实践指南

简介:本资源是面向C#中高级开发者与逆向调试学习者的EasyHook钩子技术实战Demo,聚焦Windows平台下的远程函数拦截与进程注入核心能力,适用于软件监控、行为分析、调试辅助等开发场景。压缩包共91个文件,含17个C#源码(.cs)、15个动态库(.dll)、6个可执行程序(.exe)及配套配置(.config)、符号文件(.pdb)和项目工程(.sln/.csproj),完整呈现从本地钩子创建、远程注入到签名DLL部署的全流程结构。854KB体积轻量紧凑,便于快速导入与调试验证。已有914人学习下载,资源包含多进程测试窗体(TestWindow)、Windows Forms应用示例(WindowsFormsApplication8)及独立类库(ClassLibrary1),覆盖ExitWindowsEx等典型API拦截案例,并附带签名密钥(.snk)与详细调用链路,助读者深入理解钩子生命周期、ACL权限控制与安全注入实践。

1. C# EasyHook 是什么:不是万能钩子,而是 Windows 用户态 API 拦截的“可控手术刀”

你有没有遇到过这种场景:某款老旧工业软件只支持 Windows 7,但客户现场强制升级到 Win10/Win11,结果关键日志模块突然不写文件了?或者你想在不修改源码的前提下,统计某个商业 DLL 中CreateFileW被调用的路径和频率,却被告知“没符号、没文档、不能重编译”?这时候翻遍 .NET 生态,Detours太重、Microsoft Detours Express不支持 .NET Core、MinHook又得手写汇编胶水层——而 C# EasyHook 就是那个被低估的“轻量级用户态 API 拦截方案”:它专为 .NET 环境设计,用纯托管代码注册钩子,底层自动处理 x86/x64 架构适配、线程局部存储(TLS)隔离、异常传播与堆栈对齐,不依赖外部 DLL 注入器,也不要求目标进程是 .NET 进程。它不是用来做游戏外挂或绕过 DRM 的黑盒工具,而是给企业级诊断、兼容性适配、行为审计这类“白帽场景”准备的合规拦截框架。适合正在维护遗留系统、需要做无侵入式监控、或正在开发调试辅助工具的 C# 工程师——尤其当你发现SetWindowsHookEx对内核级 API 无效、AppDomain.AssemblyResolve又抓不到原生调用时,EasyHook 往往是最后一块拼图。


2. 为什么选 EasyHook 而不是其他方案:从原理到选型边界的硬核对比

EasyHook 的核心价值不在“能钩”,而在“钩得稳、退得净、查得清”。要理解它为什么在特定场景下不可替代,必须拆开它的三层架构:托管层(C# Hook 类)、本地层(Native DLL 注入器)、目标进程钩子桩(Hook Stub)。这三者共同解决了传统钩子方案的三个致命短板:跨进程内存映射安全、托管/非托管异常穿透、以及卸载时的资源泄漏风险。

2.1 EasyHook 的拦截模型:不是“劫持”,而是“代理注入+跳转重定向”

EasyHook 并不直接 patch 目标函数的入口字节(像 MinHook 那样),而是采用“远程线程 + 内存分配 + 跳转桩”的组合策略:

  1. 远程注入阶段:EasyHook 启动一个独立的LhInjectLibrary进程,通过OpenProcess+VirtualAllocEx在目标进程地址空间申请一块可执行内存;
  2. 桩代码部署阶段:将预编译的 x86/x64 原生桩代码(含jmp指令跳转到你的托管回调)写入该内存,并通过WriteProcessMemory注入;
  3. 函数重定向阶段:在目标函数起始处写入jmp [桩地址](x64 下用mov rax, [addr]; jmp rax),完成控制流接管。

提示:这个过程全程由 EasyHook 封装,你只需调用LocalHook.Create()和LocalHook.ThreadACL.SetInheritable(),无需手动处理CreateRemoteThread或LoadLibrary注入逻辑。这也是它比裸写SetWindowsHookEx更可靠的原因——后者仅作用于消息循环,对ReadFile、RegOpenKeyExW等无消息参与的 API 完全无效。

2.2 与主流方案的关键参数对比:看懂技术选型的“隐性成本”

维度EasyHookMicrosoft DetoursMinHookSetWindowsHookEx
支持 .NET Core/.NET 5+✅(v2.7+ 官方支持)❌(仅限 .NET Framework)✅(需手动桥接)✅(仅限 UI 线程消息)
目标进程是否需为 .NET 进程❌(可 hook 任意 Win32 进程)❌(同上)❌(同上)✅(仅限同桌面会话的 UI 线程)
卸载安全性✅(自动清理 TLS、恢复原始字节、释放远程内存)⚠️(需手动调用DetourTransactionCommit)⚠️(需确保所有线程退出钩子范围)✅(UnhookWindowsHookEx即可)
异常传播能力✅(托管异常可穿透至目标线程上下文)⚠️(需自定义异常过滤器)❌(C++ 异常无法跨边界)✅(仅限消息钩子)
调试友好性✅(支持Debugger.Break()断点、PDB 符号加载)⚠️(符号需手动映射)❌(纯汇编桩,调试困难)✅(VS 可直接断点)

从表中可见:EasyHook 的真正优势在于“.NET 友好性 + 原生 API 兼容性 + 安全卸载保障” 三角闭环。如果你的任务是“给一个非 .NET 的 C++ 主程序添加日志埋点”,且该程序不允许重启、不允许加壳、不允许修改任何二进制文件——EasyHook 就是当前生态里最接近“开箱即用”的选择。

2.3 版本演进与兼容性事实:别被 v2.7 的 NuGet 数字骗了

EasyHook 当前最新稳定版是 v2.7.7(2023 年底发布),但它并非“全新架构”,而是对 v2.0(2012 年)的深度重构。关键事实如下:

  • v2.0 到 v2.7 的最大变化是移除了对EasyHook32.dll/EasyHook64.dll的强依赖:旧版需手动拷贝对应平台 DLL 到输出目录;新版通过NativeLibrary.Load()动态加载嵌入资源中的原生库,彻底解决“x64 进程 hook x86 目标”时的 DLL 架构错配问题;
  • .NET Core 支持是渐进式补全的:v2.5 开始实验性支持 netcoreapp3.1,v2.7 正式支持 net5.0+,但必须使用<TargetFramework>net6.0-windows</TargetFramework>(注意-windows后缀,否则OpenProcess权限失败);
  • Windows 11 兼容性已验证:在 22H2/23H2 上测试过NtCreateFile、ZwWriteFile、RegSetValueExW等高危 API 钩取,未出现蓝屏或进程崩溃(前提是目标进程未启用 HVCI 或 Memory Integrity)。

注意:EasyHook不支持Windows Sandbox、WSL2 内的 Windows 子系统进程,也不支持被Protected Process Light (PPL)标记的系统进程(如csrss.exe、winlogon.exe)。这是 Windows 内核级保护机制,与 EasyHook 无关——强行 hook 会导致STATUS_ACCESS_DENIED错误,而非 EasyHook 报错。


3. 从零跑通第一个 Demo:HookMessageBoxW实现弹窗内容捕获

我们以最经典的MessageBoxW为例,构建一个完整可运行的 EasyHook Demo。该 Demo 将实现:当任意进程调用MessageBoxW时,自动记录其标题、文本内容,并允许你动态修改显示文本(比如把 “Error” 替换为 “[HOOKED] Error”)。整个流程覆盖项目创建、引用配置、钩子注册、回调编写、错误处理四大环节。

3.1 创建项目并配置平台依赖:三步锁定运行环境

首先新建一个 C# 控制台项目,必须严格按以下步骤操作,否则后续 90% 的报错都源于此:

dotnet new console -n EasyHookDemo -f net6.0-windows cd EasyHookDemo dotnet add package EasyHook --version 2.7.7

逻辑说明:-f net6.0-windows是硬性要求,因为 EasyHook 底层调用OpenProcess、VirtualAllocEx等 Windows 特有 API,跨平台版本(如net6.0)会缺失System.Diagnostics.Process的完整权限模型。NuGet 包EasyHook 2.7.7已内置 x86/x64 原生库资源,无需额外下载 DLL。

然后编辑.csproj文件,在<PropertyGroup>中显式声明平台目标:

<PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0-windows</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <!-- 关键:禁用 ReadyToRun 编译,避免 Native AOT 与 EasyHook 冲突 --> <PublishReadyToRun>false</PublishReadyToRun> </PropertyGroup>

参数说明:PublishReadyToRun=false是血泪经验。若开启 ReadyToRun,.NET 会提前编译托管代码为机器码,导致 EasyHook 的LocalHook.Create()在获取委托函数地址时失败(返回0x0),最终LhInstallHook报ERROR_INVALID_PARAMETER。这个坑在官方文档里根本没提,但实测 100% 复现。

3.2 编写 Hook 回调函数:用委托签名严格匹配原生 API

EasyHook 要求你定义一个与目标 API完全一致签名的托管委托。MessageBoxW的原始声明为:

int MessageBoxW( HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType );

对应 C# 委托必须精确匹配(注意IntPtr与string的转换规则):

using EasyHook; using System; using System.Runtime.InteropServices; public static class MessageBoxHook { // 委托签名必须与 MessageBoxW 原生签名 1:1 对应 public delegate int MessageBoxWDelegate( IntPtr hWnd, string lpText, string lpCaption, uint uType); // 实际钩子回调函数:接收原生参数,可修改后调用原函数 public static int MessageBoxWHook( IntPtr hWnd, string lpText, string lpCaption, uint uType) { // 记录原始调用信息(建议用 NLog 或 Serilog,此处简化为 Console) Console.WriteLine($"[HOOK] MessageBoxW called: Caption='{lpCaption}', Text='{lpText}'"); // 【可选】动态修改文本内容 string newCaption = lpCaption?.Replace("Error", "[HOOKED] Error") ?? lpCaption; string newText = lpText?.Replace("Failed", "Intercepted & Failed") ?? lpText; // 调用原始函数(必须通过 LocalHook.Runtime.Trampoline 获取) var original = LocalHook.GetProcAddress("user32.dll", "MessageBoxW"); var trampoline = Marshal.GetDelegateForFunctionPointer<MessageBoxWDelegate>(original); // 执行原函数,传入修改后的参数 return trampoline(hWnd, newText, newCaption, uType); } }

逻辑说明:LocalHook.GetProcAddress是 EasyHook 提供的安全方式获取原函数地址,绝不能用GetProcAddress+LoadLibrary手动获取,否则在多线程环境下可能拿到错误地址。Marshal.GetDelegateForFunctionPointer将原生函数指针转为托管委托,这是跨 ABI 调用的唯一安全通道。

3.3 注册全局钩子并启动监控:两行代码完成注入

主程序逻辑极其简洁,但每行都有深意:

using EasyHook; using System; using System.Diagnostics; class Program { static void Main(string[] args) { try { // Step 1: 查找目标进程(这里 hook 自身进程用于测试) var targetProcess = Process.GetCurrentProcess(); Console.WriteLine($"Hooking process: {targetProcess.ProcessName} (PID: {targetProcess.Id})"); // Step 2: 创建钩子实例,绑定回调委托 var hook = LocalHook.Create( LocalHook.GetProcAddress("user32.dll", "MessageBoxW"), new MessageBoxHook.MessageBoxWDelegate(MessageBoxHook.MessageBoxWHook), null); // 第三个参数为传递给回调的自定义数据(此处为空) // Step 3: 设置线程访问控制列表(ACL),指定哪些线程受钩子影响 hook.ThreadACL.SetExclusiveACL(new int[] { 0 }); // 0 表示所有线程 Console.WriteLine("Hook installed successfully. Press any key to trigger MessageBox..."); Console.ReadKey(); // 触发测试弹窗(验证钩子生效) MessageBox.Show("This is a test message.", "Error", System.Windows.Forms.MessageBoxButtons.OK); Console.WriteLine("Press any key to uninstall hook and exit..."); Console.ReadKey(); } catch (Exception ex) { Console.WriteLine($"Hook failed: {ex.Message}"); Console.WriteLine($"Stack trace: {ex.StackTrace}"); } finally { // 必须显式卸载,否则目标进程残留钩子桩,可能导致后续崩溃 try { LocalHook.DisposeAllHooks(); Console.WriteLine("All hooks uninstalled."); } catch (Exception ex) { Console.WriteLine($"Cleanup failed: {ex.Message}"); } } } }

参数说明:hook.ThreadACL.SetExclusiveACL(new int[] { 0 })中的0是 EasyHook 的特殊约定,表示“应用到所有线程”。若想只 hook 主 UI 线程,需传入new int[] { targetProcess.MainThread.Id }。LocalHook.DisposeAllHooks()是安全卸载的唯一正确方式,它会自动恢复原始字节、释放远程内存、清理 TLS 数据——绝不能依赖 GC 自动回收。


4. 避坑指南:五个真实翻车现场与根因修复方案

EasyHook 文档稀疏、错误提示模糊,新手极易在看似简单的步骤中卡死。以下是我在多个模拟项目X 中踩过的 5 个高频坑,每个都附带可复现现象、底层原因及一行修复代码。

4.1 现象:LocalHook.Create()抛出EasyHook.InvalidParameterException,错误码0x80070057

  • 原因:委托函数签名与原生 API 不完全匹配。常见错误包括:
    • 将uint uType写成int uType(Windows API 中UINT是无符号 32 位);
    • 将string lpText写成IntPtr lpText并手动Marshal.PtrToStringUni(EasyHook 已自动完成字符串 marshaling);
    • 回调函数未标记为static(非静态方法会隐式携带this参数,破坏调用约定)。
  • 解决:严格对照 MSDN 原生签名,使用ildasm反编译验证委托 IL 签名。修复后代码:
    // ✅ 正确:全部参数类型与 MSDN 一致,且为 static public static int MessageBoxWHook(IntPtr hWnd, string lpText, string lpCaption, uint uType)

4.2 现象:钩子安装成功,但目标进程弹窗无任何日志输出,且MessageBoxW行为异常(如窗口不显示)

  • 原因:目标进程启用了Control Flow Guard (CFG)或Hardware-enforced Stack Protection(Windows 10 1809+ 默认开启)。EasyHook 的跳转桩会被 CFG 视为非法间接调用,触发STATUS_GUARD_PAGE_VIOLATION,进程静默终止。
  • 解决:在目标进程启动前,临时关闭 CFG(仅限测试环境):
    # PowerShell 以管理员运行 Set-ProcessMitigation -Name "notepad.exe" -Disable CFG
    或在代码中检测并提示:
    if (Environment.OSVersion.Version >= new Version("10.0.17763")) Console.WriteLine("[WARN] CFG may block hook on this OS. Run 'Set-ProcessMitigation' to disable.");

4.3 现象:LocalHook.Create()成功,但hook.ThreadACL.SetExclusiveACL()报AccessDenied

  • 原因:当前进程未以Administrator 权限运行,或目标进程属于更高完整性级别(如High Mandatory Level的系统进程)。EasyHook 需要PROCESS_VM_OPERATION和PROCESS_QUERY_INFORMATION权限。
  • 解决:右键 VS 或终端,选择 “以管理员身份运行”;或在.csproj中添加清单文件请求提升:
    <PropertyGroup> <ApplicationManifest>app.manifest</ApplicationManifest> </PropertyGroup>
    app.manifest内容:
    <?xml version="1.0" encoding="utf-8"?> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> <security> <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3"> <requestedExecutionLevel level="requireAdministrator" uiAccess="false" /> </requestedPrivileges> </security> </trustInfo> </assembly>

4.4 现象:钩子卸载后,目标进程再次调用MessageBoxW时崩溃,错误码0xC0000005(访问冲突)

  • 原因:未调用LocalHook.DisposeAllHooks(),或在多线程环境中部分线程仍在执行钩子回调时强行卸载,导致远程桩内存被释放,但线程仍跳转到已失效地址。
  • 解决:必须在finally块中调用卸载,并确保所有目标线程已退出钩子范围。更健壮的写法:
    finally { // 等待所有线程离开钩子区域(EasyHook 内置同步机制) Thread.Sleep(100); LocalHook.DisposeAllHooks(); }

4.5 现象:在 .NET 6+ 项目中,LocalHook.Create()报System.DllNotFoundException: Unable to load DLL 'EasyHook32.dll'

  • 原因:项目未设置<TargetFramework>net6.0-windows</TargetFramework>,或PublishReadyToRun=true导致原生资源未正确嵌入。
  • 解决:检查.csproj,确认<TargetFramework>含-windows后缀,并显式关闭 ReadyToRun:
    <PropertyGroup> <TargetFramework>net6.0-windows</TargetFramework> <PublishReadyToRun>false</PublishReadyToRun> </PropertyGroup>

5. 进阶实战:HookCreateFileW实现文件访问审计与路径重定向

前面的MessageBoxWDemo 属于“只读监控”,而真正的工程价值在于可写干预。本章以CreateFileW为例,演示如何在不修改目标程序的前提下,实现:

  • 审计所有文件打开行为(路径、访问模式、创建标志);
  • 将指定路径(如C:\legacy\config.ini)透明重定向到沙箱目录(如C:\sandbox\config.ini);
  • 阻止对敏感路径(如C:\Windows\system32\)的写入操作。

5.1CreateFileW委托签名与参数解析:比 MessageBox 复杂得多的结构体

CreateFileW原生签名包含 7 个参数,其中dwCreationDisposition和dwFlagsAndAttributes是位掩码,需用enum显式定义:

using System; using System.Runtime.InteropServices; public static class CreateFileHook { [Flags] public enum FileAccess : uint { GenericRead = 0x80000000, GenericWrite = 0x40000000, GenericExecute = 0x20000000, GenericAll = 0x10000000 } [Flags] public enum CreationDisposition : uint { CreateNew = 1, CreateAlways = 2, OpenExisting = 3, OpenAlways = 4, TruncateExisting = 5 } public delegate IntPtr CreateFileWDelegate( string lpFileName, FileAccess dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, CreationDisposition dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); public static IntPtr CreateFileWHook( string lpFileName, FileAccess dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, CreationDisposition dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile) { // 1. 审计日志:记录所有调用 Console.WriteLine($"[AUDIT] CreateFileW: '{lpFileName}' | Access={dwDesiredAccess} | Disp={dwCreationDisposition}"); // 2. 路径重定向逻辑:将 C:\legacy\ 映射到 C:\sandbox\ string redirectedPath = lpFileName; const string legacyPrefix = @"C:\legacy\"; const string sandboxPrefix = @"C:\sandbox\"; if (lpFileName?.StartsWith(legacyPrefix, StringComparison.OrdinalIgnoreCase) == true) { redirectedPath = lpFileName.Replace(legacyPrefix, sandboxPrefix, StringComparison.OrdinalIgnoreCase); Console.WriteLine($"[REDIRECT] '{lpFileName}' -> '{redirectedPath}'"); } // 3. 敏感路径拦截:禁止写入 system32 const string system32 = @"C:\Windows\system32\"; if (lpFileName?.StartsWith(system32, StringComparison.OrdinalIgnoreCase) == true && (dwDesiredAccess.HasFlag(FileAccess.GenericWrite) || dwCreationDisposition is CreationDisposition.CreateNew or CreationDisposition.CreateAlways)) { Console.WriteLine($"[BLOCKED] Write access to system32 denied for '{lpFileName}'"); SetLastError(5); // ERROR_ACCESS_DENIED return IntPtr.Zero; } // 4. 调用原函数,传入可能修改后的路径 var original = LocalHook.GetProcAddress("kernel32.dll", "CreateFileW"); var trampoline = Marshal.GetDelegateForFunctionPointer<CreateFileWDelegate>(original); return trampoline(redirectedPath, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); } [DllImport("kernel32.dll", SetLastError = true)] private static extern void SetLastError(uint dwErrCode); }

逻辑说明:SetLastError是关键。当你要主动拒绝一个操作时,必须调用SetLastError设置标准 Windows 错误码,否则目标程序可能因GetLastError()返回 0 而误判为成功。此处设为5(ERROR_ACCESS_DENIED),与系统行为完全一致。

5.2 钩子注册与线程 ACL 精细控制:避免审计日志爆炸

CreateFileW调用频率极高(一个程序启动可能触发数百次),若 hook 所有线程,日志会刷屏。此时需用 ACL 精确控制:

// 只 hook 指定 PID 的进程(例如:notepad.exe) var targetProcess = Process.GetProcessesByName("notepad").FirstOrDefault(); if (targetProcess == null) { Console.WriteLine("Notepad not found. Launch it first."); return; } // 创建钩子 var hook = LocalHook.Create( LocalHook.GetProcAddress("kernel32.dll", "CreateFileW"), new CreateFileHook.CreateFileWDelegate(CreateFileHook.CreateFileWHook), null); // ACL 设置:只 hook 主线程(ID = targetProcess.MainThread.Id) // 避免后台线程(如 .NET ThreadPool)的 CreateFileW 冲刷日志 hook.ThreadACL.SetExclusiveACL(new int[] { targetProcess.MainThread.Id }); Console.WriteLine($"Hooking notepad (PID: {targetProcess.Id}) main thread only."); Console.WriteLine("Now open a file in Notepad to see audit log...");

5.3 验证与调试技巧:用 Process Monitor 交叉验证 Hook 行为

单靠 Console 日志无法 100% 确认 Hook 是否生效,必须用微软官方工具Process Monitor (ProcMon)交叉验证:

  1. 启动 ProcMon,设置 Filter:
    • Process Nameisnotepad.exe
    • OperationisCreateFile
    • 勾选Include stack trace(需安装 Windows Driver Kit)
  2. 在 Notepad 中打开C:\legacy\test.txt;
  3. 观察 ProcMon 日志:
    • 若看到C:\legacy\test.txt的NAME NOT FOUND,说明重定向失败(钩子未生效);
    • 若看到C:\sandbox\test.txt的SUCCESS,且堆栈中包含EasyHook模块,说明重定向成功;
    • 若看到C:\Windows\system32\evil.dll的ACCESS DENIED,且堆栈无 EasyHook,则是系统原生拦截;若堆栈含 EasyHook,则是你代码生效。

提示:ProcMon 的堆栈跟踪是判断 Hook 是否真正接管的黄金标准。EasyHook 的桩代码会在堆栈中显示为EasyHook32!HookEntryPoint或类似名称,这是它区别于其他方案的指纹。

从那以后我每次部署 EasyHook,都强制走一遍 ProcMon 验证流程——哪怕只是本地测试。因为 Console 日志只能告诉你“我的回调执行了”,而 ProcMon 才能告诉你“系统真的按我的逻辑走了”。Hook 不是魔法,它是可控的、可验证的、可审计的工程实践。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 2:46:42

思科模拟器6.2:从安装到避坑的完整实战指南

简介&#xff1a;这是一份思科模拟器6.2安装包&#xff0c;官方名称为Packet Tracer&#xff0c;面向网络技术学习者、思科认证考生以及开展网络实验教学的教师。软件以图形化方式搭建虚拟网络环境&#xff0c;可模拟路由器、交换机、无线接入点、防火墙等设备&#xff0c;支持…

作者头像 李华
网站建设 2026/10/21 2:46:24

iOS实况图制作Demo:Live Photo生成链路与元数据配对详解

简介&#xff1a;这份资源是面向iOS开发者的Swift实况图制作实战Demo&#xff0c;聚焦Live Photo的合成、展示与预览全流程&#xff0c;适合已掌握Swift基础、希望深入Photos框架与图像视频处理的中级开发者。压缩包共21个文件&#xff0c;约4.37MB&#xff0c;以5个swift源码文…

作者头像 李华
网站建设 2026/10/21 2:45:42

C# WinForm集成Halcon图像控件:解决坐标偏移与缩放卡顿

简介&#xff1a;本资源是一个基于C#与Halcon联合开发的WinForm图像交互演示项目&#xff0c;面向计算机视觉初学者、工业检测应用开发者及自动化软件工程师&#xff0c;解决图像显示控件中基础交互功能&#xff08;拖拽、缩放&#xff09;与实时像素信息反馈缺失的实践痛点。项…

作者头像 李华
网站建设 2026/10/21 2:44:15

深度迁移学习综述:四类方法、核心原理与工程落地实践

1. 为什么翻出这篇综述最近要把一个图像分类模型迁移到新的业务场景里&#xff0c;数据集只有几千张标注样本&#xff0c;重新训练一个大模型完全不现实。于是把这篇《A Survey on Deep Transfer Learning》翻出来重读。第一次读的时候还在学校&#xff0c;赶时髦&#xff1b;这…

作者头像 李华
网站建设 2026/10/21 2:43:56

MFC+OpenGL读取OBJ模型并贴图的完整实现

简介&#xff1a;面向计算机图形学学习者和使用微软基础类库的C开发者&#xff0c;这一示例工程演示了如何在MFC窗口程序中结合OpenGL图形接口读取OBJ三维模型文件&#xff0c;并完成纹理贴图与最终渲染输出。资源包共123个文件&#xff0c;约35.13MB&#xff0c;内部包含一套完…

作者头像 李华