本工具基于 AutoHotkey v2 开发,用于采集游戏内不同场景的画面亮度数据,自动计算正常画面与黑屏/淡出过场之间的亮度阈值,可用于游戏自动化的场景切换判定。工具支持可视化采样点定位,可直观调整采集位置,输出详细的统计分析与候选阈值。
一、热键功能一览
| 热键 | 功能说明 |
|---|---|
| F6 | 显示 / 隐藏五个采样点标记 |
| F7 | 重新捕获当前活动窗口,并刷新采样点位置 |
| F8 | 开始 / 停止亮度采集 |
| F9 | 标记当前为「第一场景正常画面」 |
| F10 | 标记当前为「淡出/黑屏过场」 |
| F11 | 标记当前为「第二场景正常画面」 |
| F12 | 查看五个采样点的详细信息(坐标、比例、颜色、亮度) |
| Esc | 退出脚本 |
二、完整脚本代码
bash
#Requires AutoHotkey v2.0
#SingleInstance Force
; ============================================================
; 游戏场景明暗阈值采集工具
; ============================================================
; ------------------------------------------------------------
; 坐标模式
; ------------------------------------------------------------
CoordMode "Pixel", "Screen"
CoordMode "ToolTip", "Screen"
; ============================================================
; 基础配置
; ============================================================
global SampleInterval := 100 ; 采样间隔:毫秒,100=每秒10次
global IsSampling := false ; 是否正在采集
global CurrentPhase := "UNMARKED" ; 当前采集阶段
global GameHwnd := 0 ; 游戏窗口句柄
global SamplePoints := [] ; 实际采样点坐标数组
global LogFile := A_ScriptDir "\brightness_log.csv" ; 日志文件路径
global PixelMode := "" ; PixelGetColor读取模式
; ============================================================
; 采样点比例配置
; ============================================================
global SamplePointConfigs := [
{Name: "P1", XRatio: 0.20, YRatio: 0.25, Description: "左上"},
{Name: "P2", XRatio: 0.80, YRatio: 0.25, Description: "右上"},
{Name: "P3", XRatio: 0.50, YRatio: 0.40, Description: "中央偏上"},
{Name: "P4", XRatio: 0.20, YRatio: 0.70, Description: "左下"},
{Name: "P5", XRatio: 0.80, YRatio: 0.70, Description: "右下"}
]
; ============================================================
; 采样点预览标记配置
; ============================================================
global MarkerGuis := [] ; 采样点GUI对象数组
global AreMarkersVisible := false ; 标记是否正在显示
global MarkerSize := 46 ; 标记尺寸(像素)
global MarkerColor := "E53935" ; 标记颜色
global MarkerTransparency := 220 ; 标记透明度(0-255)
; ============================================================
; 各阶段亮度数据缓存
; ============================================================
global Normal1Values := [] ; 第一场景正常画面
global TransitionValues := [] ; 黑屏、淡出、转场画面
global Normal2Values := [] ; 第二场景正常画面
; ============================================================
; 脚本退出事件
; ============================================================
OnExit CleanUpBeforeExit
; ============================================================
; 启动提示
; ============================================================
MsgBox(
"游戏明暗阈值采集工具`n`n"
. "推荐使用方法:`n"
. "1. 先切换到游戏窗口`n"
. "2. 按 F7 捕获游戏窗口并显示采样点`n"
. "3. 观察 P1~P5 是否位于合适位置`n"
. "4. 按 F6 隐藏或再次显示采样点`n"
. "5. 按 F8 开始正式采集`n"
. "6. 第一场景正常移动时按 F9`n"
. "7. 准备进入场景切换时按 F10`n"
. "8. 第二场景完全显示后按 F11`n"
. "9. 再按 F8 停止并查看结果`n`n"
. "正式采样开始时,采样点标记会自动隐藏。`n"
. "游戏不显示鼠标指针也不影响本脚本。",
"游戏明暗阈值采集工具"
)
; ============================================================
; 热键定义
; ============================================================
F6:: ToggleSamplePointMarkers()
F7:: RefreshSamplePointMarkers()
F8:: {
global IsSampling
if !IsSampling
StartSampling()
else
StopSampling()
}
F9:: SetPhase("NORMAL_1")
F10:: SetPhase("TRANSITION")
F11:: SetPhase("NORMAL_2")
F12:: ShowCurrentSample()
Esc:: ExitApp()
; ============================================================
; 开始采集
; ============================================================
StartSampling() {
global IsSampling, CurrentPhase, GameHwnd, LogFile
global Normal1Values, TransitionValues, Normal2Values, SampleInterval
HideSamplePointMarkers() ; 正式采样前隐藏标记,避免污染结果
; 窗口不存在则捕获当前活动窗口
if !GameHwnd || !WinExist("ahk_id " GameHwnd) {
GameHwnd := WinExist("A")
}
if !GameHwnd {
MsgBox "没有找到当前活动窗口。"
return
}
; 清空历史数据
Normal1Values := []
TransitionValues := []
Normal2Values := []
if !BuildSamplePoints()
return
; 重建日志文件
if FileExist(LogFile) {
try FileDelete LogFile
catch as ErrorObject {
MsgBox(
"无法删除旧日志文件。`n`n"
. "日志文件可能正在被Excel或其他程序打开。`n`n"
. "文件位置:`n"
. LogFile
. "`n`n错误信息:`n"
. ErrorObject.Message,
"无法创建日志"
)
return
}
}
try {
FileAppend(
"Time,Phase,"
. "P1,P2,P3,P4,P5,"
. "Average,Minimum,Maximum`n",
LogFile,
"UTF-8"
)
} catch {
MsgBox("无法创建日志文件。`n`n文件位置:`n" LogFile, "无法创建日志")
return
}
CurrentPhase := "UNMARKED"
IsSampling := true
SetTimer CollectSample, SampleInterval
ToolTip(
"采集已经开始`n"
. "采样点标记已经自动隐藏`n`n"
. "请按 F9 标记第一场景",
20, 20
)
}
; ============================================================
; 停止采集
; ============================================================
StopSampling() {
global IsSampling
if !IsSampling
return
IsSampling := false
SetTimer CollectSample, 0
ToolTip()
ShowSummary()
}
; ============================================================
; 生成采样点坐标
; ============================================================
BuildSamplePoints() {
global GameHwnd, SamplePoints, SamplePointConfigs
if !GameHwnd || !WinExist("ahk_id " GameHwnd) {
MsgBox "游戏窗口不存在,请先切换到游戏并按 F7。"
return false
}
try WinGetClientPos(&WindowX, &WindowY, &WindowW, &WindowH, "ahk_id " GameHwnd)
catch as ErrorObject {
MsgBox("无法获取游戏窗口客户区。`n`n错误信息:`n" ErrorObject.Message)
return false
}
if WindowW <= 0 || WindowH <= 0 {
MsgBox("游戏窗口尺寸无效。`n`n宽度:" WindowW "`n高度:" WindowH)
return false
}
SamplePoints := []
for Config in SamplePointConfigs {
PointX := Round(WindowX + WindowW * Config.XRatio)
PointY := Round(WindowY + WindowH * Config.YRatio)
SamplePoints.Push({
Name: Config.Name,
X: PointX,
Y: PointY,
XRatio: Config.XRatio,
YRatio: Config.YRatio,
Description: Config.Description
})
}
return true
}
; ============================================================
; 设置采集阶段
; ============================================================
SetPhase(NewPhase) {
global IsSampling, CurrentPhase
if !IsSampling {
ToolTip "请先切换到游戏窗口并按 F8 开始采集", 20, 20
SetTimer HideToolTip, -1500
return
}
CurrentPhase := NewPhase
switch NewPhase {
case "NORMAL_1": Description := "第一场景正常画面"
case "TRANSITION": Description := "淡出/黑屏过场"
case "NORMAL_2": Description := "第二场景正常画面"
default: Description := NewPhase
}
ToolTip("当前阶段:`n" Description, 20, 20)
}
; ============================================================
; 定时采样执行函数
; ============================================================
CollectSample() {
global IsSampling, GameHwnd, CurrentPhase, SamplePoints
global LogFile, PixelMode
global Normal1Values, TransitionValues, Normal2Values
if !IsSampling
return
if !WinExist("ahk_id " GameHwnd) {
StopSampling()
MsgBox "游戏窗口已经关闭或无法找到。"
return
}
BrightnessValues := []
for Point in SamplePoints {
try {
Color := PixelGetColor(Point.X, Point.Y, PixelMode)
Brightness := ColorToBrightness(Color)
BrightnessValues.Push(Brightness)
} catch {
BrightnessValues.Push(-1)
}
}
if BrightnessValues.Length < 5
return
; 计算统计值
AverageBrightness := ArrayAverage(BrightnessValues)
MinimumBrightness := ArrayMinimum(BrightnessValues)
MaximumBrightness := ArrayMaximum(BrightnessValues)
; 按阶段存入数组
switch CurrentPhase {
case "NORMAL_1": Normal1Values.Push(AverageBrightness)
case "TRANSITION": TransitionValues.Push(AverageBrightness)
case "NORMAL_2": Normal2Values.Push(AverageBrightness)
}
; 写入CSV日志
CurrentTime := FormatTime(, "HH:mm:ss.") Format("{:03}", A_MSec)
try {
FileAppend(
CurrentTime ","
. CurrentPhase ","
. BrightnessValues[1] ","
. BrightnessValues[2] ","
. BrightnessValues[3] ","
. BrightnessValues[4] ","
. BrightnessValues[5] ","
. AverageBrightness ","
. MinimumBrightness ","
. MaximumBrightness "`n",
LogFile,
"UTF-8"
)
} catch {
; 单次写入失败不终止脚本
}
; 左上角实时显示
ToolTip(
"阶段:" CurrentPhase
. "`n平均亮度:" AverageBrightness
. "`n最低:" MinimumBrightness " 最高:" MaximumBrightness
. "`n`nF9 第一场景"
. "`nF10 过场"
. "`nF11 第二场景"
. "`nF8 停止",
20, 20
)
}
; ============================================================
; RGB转感知亮度
; ============================================================
ColorToBrightness(Color) {
Red := (Color >> 16) & 0xFF
Green := (Color >> 8) & 0xFF
Blue := Color & 0xFF
Brightness := Round(
Red * 0.2126
+ Green * 0.7152
+ Blue * 0.0722,
2
)
return Brightness
}
; ============================================================
; 数组工具:平均值
; ============================================================
ArrayAverage(Values) {
Total := 0
ValidCount := 0
for Value in Values {
if Value >= 0 {
Total += Value
ValidCount += 1
}
}
if ValidCount = 0
return -1
return Round(Total / ValidCount, 2)
}
; ============================================================
; 数组工具:最小值
; ============================================================
ArrayMinimum(Values) {
MinimumValue := ""
for Value in Values {
if Value < 0
continue
if MinimumValue = "" || Value < MinimumValue
MinimumValue := Value
}
if MinimumValue = ""
return -1
return MinimumValue
}
; ============================================================
; 数组工具:最大值
; ============================================================
ArrayMaximum(Values) {
MaximumValue := ""
for Value in Values {
if Value < 0
continue
if MaximumValue = "" || Value > MaximumValue
MaximumValue := Value
}
if MaximumValue = ""
return -1
return MaximumValue
}
; ============================================================
; 显示/隐藏采样点切换
; ============================================================
ToggleSamplePointMarkers() {
global AreMarkersVisible
if AreMarkersVisible
HideSamplePointMarkers()
else
ShowSamplePointMarkers()
}
; ============================================================
; 重新捕获窗口并刷新采样点
; ============================================================
RefreshSamplePointMarkers() {
global GameHwnd, IsSampling
if IsSampling {
ToolTip("正在正式采样,不能显示位置标记。`n请先按 F8 停止采样。", 20, 20)
SetTimer HideToolTip, -2000
return
}
GameHwnd := WinExist("A")
if !GameHwnd {
MsgBox "没有找到当前活动窗口。"
return
}
if !BuildSamplePoints()
return
ShowSamplePointMarkers()
}
; ============================================================
; 显示采样点标记
; ============================================================
ShowSamplePointMarkers() {
global GameHwnd, SamplePoints, MarkerGuis, AreMarkersVisible
global IsSampling, MarkerSize, MarkerColor, MarkerTransparency
if IsSampling {
ToolTip("正在正式采样,位置标记已被禁止显示。", 20, 20)
SetTimer HideToolTip, -1500
return
}
if !GameHwnd || !WinExist("ahk_id " GameHwnd)
GameHwnd := WinExist("A")
if !GameHwnd {
MsgBox "没有找到游戏窗口。"
return
}
if !BuildSamplePoints()
return
DestroySamplePointMarkers()
for Point in SamplePoints {
MarkerGui := Gui("+AlwaysOnTop -Caption +ToolWindow +E0x20")
MarkerGui.BackColor := MarkerColor
MarkerGui.SetFont("s10 bold cWhite", "Segoe UI")
MarkerGui.AddText(
"Center w" MarkerSize " h" MarkerSize " +0x200",
Point.Name
)
MarkerX := Round(Point.X - MarkerSize / 2)
MarkerY := Round(Point.Y - MarkerSize / 2)
MarkerGui.Show("NA x" MarkerX " y" MarkerY " w" MarkerSize " h" MarkerSize)
try WinSetTransparent(MarkerTransparency, "ahk_id " MarkerGui.Hwnd)
MarkerGuis.Push(MarkerGui)
}
AreMarkersVisible := true
ToolTip(
"正在显示采样点预览`n"
. "红色方块中心就是实际采样坐标`n"
. "按 F6 隐藏标记`n"
. "按 F7 重新捕获当前窗口",
20, 20
)
SetTimer HideToolTip, -3000
}
; ============================================================
; 隐藏采样点标记
; ============================================================
HideSamplePointMarkers() {
global MarkerGuis, AreMarkersVisible
for MarkerGui in MarkerGuis {
try MarkerGui.Hide()
}
AreMarkersVisible := false
}
; ============================================================
; 销毁所有采样点标记
; ============================================================
DestroySamplePointMarkers() {
global MarkerGuis, AreMarkersVisible
for MarkerGui in MarkerGuis {
try MarkerGui.Destroy()
}
MarkerGuis := []
AreMarkersVisible := false
}
; ============================================================
; 显示当前采样点详情
; ============================================================
ShowCurrentSample() {
global SamplePoints, PixelMode, GameHwnd, IsSampling
if !GameHwnd || !WinExist("ahk_id " GameHwnd) {
MsgBox("没有有效的游戏窗口。`n`n请切换到游戏窗口并按 F7。", "没有游戏窗口")
return
}
if !IsSampling {
if !BuildSamplePoints()
return
}
if SamplePoints.Length = 0 {
MsgBox "尚未建立采样点,请先按 F7。"
return
}
Message := ""
for Point in SamplePoints {
try {
Color := PixelGetColor(Point.X, Point.Y, PixelMode)
Brightness := ColorToBrightness(Color)
Message .= (
Point.Name ":" Point.Description "`n"
. "屏幕坐标:(" Point.X ", " Point.Y ")`n"
. "相对位置:" Round(Point.XRatio * 100, 1) "%," Round(Point.YRatio * 100, 1) "%`n"
. "颜色:0x" Format("{:06X}", Color) "`n"
. "亮度:" Brightness "`n`n"
)
} catch as ErrorObject {
Message .= Point.Name " 读取失败`n错误:" ErrorObject.Message "`n`n"
}
}
MsgBox(Message, "当前采样点详细信息")
}
; ============================================================
; 显示最终采集结果与阈值分析
; ============================================================
ShowSummary() {
global Normal1Values, TransitionValues, Normal2Values, LogFile
Normal1Text := GetRangeText(Normal1Values)
TransitionText := GetRangeText(TransitionValues)
Normal2Text := GetRangeText(Normal2Values)
CandidateThresholdText := CalculateCandidateThreshold()
MsgBox(
"采集完成。`n`n"
. "第一场景范围:`n" Normal1Text
. "`n`n黑屏过场范围:`n" TransitionText
. "`n`n第二场景范围:`n" Normal2Text
. "`n`n阈值分析:`n" CandidateThresholdText
. "`n`n日志文件:`n" LogFile,
"采集结果"
)
}
; ============================================================
; 生成阶段范围描述文本
; ============================================================
GetRangeText(Values) {
if Values.Length = 0
return "没有数据"
MinimumValue := ArrayMinimum(Values)
MaximumValue := ArrayMaximum(Values)
AverageValue := ArrayAverage(Values)
return (
"样本数:" Values.Length
. "`n最低:" MinimumValue
. "`n最高:" MaximumValue
. "`n平均:" AverageValue
)
}
; ============================================================
; 计算候选阈值
; ============================================================
CalculateCandidateThreshold() {
global Normal1Values, TransitionValues, Normal2Values
if TransitionValues.Length = 0 {
return "没有记录黑屏过场数据,无法计算。`n请在淡出或黑屏期间按 F10。"
}
if Normal1Values.Length = 0 || Normal2Values.Length = 0 {
return "两个正常场景的数据不完整,无法计算。`n请分别在两个正常场景中按 F9 和 F11。"
}
Normal1Minimum := ArrayMinimum(Normal1Values)
Normal2Minimum := ArrayMinimum(Normal2Values)
NormalMinimum := Min(Normal1Minimum, Normal2Minimum)
TransitionMaximum := ArrayMaximum(TransitionValues)
; 可分离区间
if TransitionMaximum < NormalMinimum {
CandidateThreshold := Round((TransitionMaximum + NormalMinimum) / 2, 2)
SeparationGap := Round(NormalMinimum - TransitionMaximum, 2)
return (
"数据存在可分离区间。`n"
. "黑屏最高值:" TransitionMaximum
. "`n正常最低值:" NormalMinimum
. "`n分离间距:" SeparationGap
. "`n候选阈值:" CandidateThreshold
. "`n`n平均亮度低于该值时,可初步判定画面偏暗。`n"
. "平均亮度高于该值时,可初步判定画面偏亮。`n`n"
. "正式自动化时仍建议加入:`n"
. "1. 多个采样点同步变化`n"
. "2. 连续若干帧低于阈值`n"
. "3. 画面重新稳定后再判定场景就绪"
)
}
; 数据重叠
OverlapAmount := Round(TransitionMaximum - NormalMinimum, 2)
return (
"正常画面和过场数据发生重叠。`n"
. "黑屏最高值:" TransitionMaximum
. "`n正常最低值:" NormalMinimum
. "`n重叠量:" OverlapAmount
. "`n`n暂时不建议只使用一个固定阈值。`n"
. "建议尝试以下改进:`n"
. "1. 调整采样点位置`n"
. "2. 避开角色、文字、火焰和固定UI`n"
. "3. 增加连续帧判断`n"
. "4. 增加多个点同步变暗判断`n"
. "5. 分别设置进入暗场和离开暗场阈值"
)
}
; ============================================================
; 隐藏提示
; ============================================================
HideToolTip() {
ToolTip()
}
; ============================================================
; 退出前清理
; ============================================================
CleanUpBeforeExit(ExitReason, ExitCode) {
SetTimer CollectSample, 0
ToolTip()
DestroySamplePointMarkers()
}
三、标准使用流程
-
运行脚本 双击
.ahk文件启动,弹出功能说明窗口,确认后关闭即可。 -
捕获游戏窗口 切换到目标游戏窗口,按下 F7,屏幕上会显示 P1~P5 五个红色标记方块,方块中心即为实际采样坐标。
-
调整采样位置 观察红色标记是否落在目标采集区域:
- 位置合适:按 F6 可隐藏标记,避免干扰画面
- 位置不合适:参考下文「采样点调整方法」修改参数后,重新按 F7 刷新
-
启动正式采集 按下 F8 启动采集,采样点标记会自动隐藏,避免污染采集结果。左上角会实时显示当前阶段、平均亮度、最低/最高亮度。
-
分阶段标记场景
- 第一场景画面正常稳定时:按 F9
- 场景切换、淡出/黑屏过程中:按 F10
- 第二场景完全显示、画面稳定后:按 F11
-
停止并查看结果 再次按下 F8 停止采集,自动弹出结果窗口,包含:
- 三个阶段的亮度范围(样本数、最低、最高、平均)
- 阈值分析与候选阈值建议
- CSV 日志文件路径
-
查看采样点详情(可选) 按下 F12 可查看每个采样点的屏幕坐标、相对比例、颜色值、亮度值。
四、采样点位置调整方法
工具采用窗口相对比例 定位采样点,调整时只需修改脚本顶部的 SamplePointConfigs 配置数组,无需改动核心代码。
参数说明
XRatio:水平方向比例,0.00= 窗口最左侧,1.00= 窗口最右侧YRatio:垂直方向比例,0.00= 窗口最顶部,1.00= 窗口最底部Description:采样点备注,仅用于展示
调整示例
- 中间点 P3 向下移动:将
YRatio: 0.40改为YRatio: 0.55 - 左上点 P1 向中间靠拢:将
XRatio: 0.20改为XRatio: 0.30
修改完成后保存脚本,按 F7 即可刷新标记位置。
五、核心功能说明
1. 亮度计算
采用人眼感知加权亮度公式:
亮度 = 红×0.2126 + 绿×0.7152 + 蓝×0.0722
亮度范围 0~255,更符合真实视觉感受。
2. 阈值判定逻辑
采集完成后自动分析两组数据分布:
-
可分离区间:黑屏最高亮度 < 正常画面最低亮度
- 输出候选阈值(取两者中间值)
- 亮度低于阈值判定为偏暗,高于判定为偏亮
- 同时给出自动化优化建议
-
数据重叠:黑屏最高亮度 ≥ 正常画面最低亮度
- 提示重叠量,不建议使用单一固定阈值
- 给出优化方向:调整采样点、避开UI/角色、增加连续帧判断、双阈值方案等
3. 数据日志
所有采样数据自动保存为 brightness_log.csv(脚本同目录),包含时间戳、阶段、五个点亮度值、平均/最低/最高亮度,可用于后续二次分析。
4. 防污染设计
正式采集开始时,采样点标记会自动隐藏,避免红色标记被像素读取到,保证采集数据准确。
💡 建议每个阶段保持采集 3~5 秒,足够的样本量会让阈值计算更准确;尽量避开角色、动态UI、火焰等亮度频繁变化的区域。