Calibrate orb detection from botty reference: vertical slice method, correct HSV ranges

- Add ReadOrbSlicePercentage: thin vertical strip method for accurate fill reading
- Support HSV hue wrapping in colorInRange (for red hues crossing 360°)
- Health HSV: H 350-370 (wrapping), S 60+, V 20+ (calibrated from screenshot)
- Mana HSV: H 226-240, S 100+, V 20+ (calibrated from screenshot)
- Add health_slice/mana_slice regions (10px wide vertical strips through orb centers)
- Update health_globe/mana_globe to full botty-referenced regions
- Verified: health 96.3%, mana 100% on testdata/d2r_1080p.png (both at 100% fill)
This commit is contained in:
Hoid 2026-02-14 11:56:18 +00:00
parent 0716aeb5e1
commit 35a1944179
8 changed files with 117 additions and 44 deletions

View file

@ -33,10 +33,14 @@ func main() {
debugDir := "testdata/debug" debugDir := "testdata/debug"
os.MkdirAll(debugDir, 0755) os.MkdirAll(debugDir, 0755)
// Define regions for 1080p (from config.go — calibrated from real screenshot) // Define regions for 1080p (from config.go — calibrated from botty reference)
regions := map[string]image.Rectangle{ regions := map[string]image.Rectangle{
"health_orb": image.Rect(370, 945, 460, 1030), "health_globe": image.Rect(240, 870, 600, 1080),
"mana_orb": image.Rect(1580, 910, 1670, 1000), "mana_globe": image.Rect(1330, 870, 1690, 1080),
"health_slice": image.Rect(415, 915, 425, 1060),
"mana_slice": image.Rect(1505, 915, 1515, 1060),
"health_orb": image.Rect(240, 870, 600, 1080),
"mana_orb": image.Rect(1330, 870, 1690, 1080),
"xp_bar": image.Rect(0, 1058, 1920, 1080), "xp_bar": image.Rect(0, 1058, 1920, 1080),
"belt": image.Rect(500, 990, 900, 1040), "belt": image.Rect(500, 990, 900, 1040),
"minimap": image.Rect(1600, 0, 1920, 320), "minimap": image.Rect(1600, 0, 1920, 320),
@ -48,21 +52,43 @@ func main() {
fmt.Println("\n=== REGION ANALYSIS ===") fmt.Println("\n=== REGION ANALYSIS ===")
// Analyze health orb // Analyze health globe (full region)
analyzeRegion(img, "health_orb", regions["health_orb"], config.Colors.HealthFilled, debugDir) analyzeRegion(img, "health_globe", regions["health_globe"], config.Colors.HealthFilled, debugDir)
// Analyze mana orb // Analyze mana globe (full region)
analyzeRegion(img, "mana_orb", regions["mana_orb"], config.Colors.ManaFilled, debugDir) analyzeRegion(img, "mana_globe", regions["mana_globe"], config.Colors.ManaFilled, debugDir)
// Analyze slices
analyzeRegion(img, "health_slice", regions["health_slice"], config.Colors.HealthFilled, debugDir)
analyzeRegion(img, "mana_slice", regions["mana_slice"], config.Colors.ManaFilled, debugDir)
// Use the vertical slice method for accurate orb reading
fmt.Println("\n=== ORB SLICE READING (vertical strip method) ===")
pipeline := vision.NewPipeline(0.5)
healthSliceColor := vision.ColorRange{
LowerH: config.Colors.HealthFilled.LowerH, LowerS: config.Colors.HealthFilled.LowerS, LowerV: config.Colors.HealthFilled.LowerV,
UpperH: config.Colors.HealthFilled.UpperH, UpperS: config.Colors.HealthFilled.UpperS, UpperV: config.Colors.HealthFilled.UpperV,
}
manaSliceColor := vision.ColorRange{
LowerH: config.Colors.ManaFilled.LowerH, LowerS: config.Colors.ManaFilled.LowerS, LowerV: config.Colors.ManaFilled.LowerV,
UpperH: config.Colors.ManaFilled.UpperH, UpperS: config.Colors.ManaFilled.UpperS, UpperV: config.Colors.ManaFilled.UpperV,
}
healthPct := pipeline.ReadOrbSlicePercentage(img, regions["health_slice"], healthSliceColor)
manaPct := pipeline.ReadOrbSlicePercentage(img, regions["mana_slice"], manaSliceColor)
fmt.Printf("Health orb (slice): %.1f%%\n", healthPct*100)
fmt.Printf("Mana orb (slice): %.1f%%\n", manaPct*100)
// Sample some specific pixels in the orbs for detailed analysis // Sample some specific pixels in the orbs for detailed analysis
fmt.Println("\n=== PIXEL SAMPLING ===") fmt.Println("\n=== PIXEL SAMPLING ===")
samplePixelsInRegion(img, "health_orb", regions["health_orb"]) samplePixelsInRegion(img, "health_globe", regions["health_globe"])
samplePixelsInRegion(img, "mana_orb", regions["mana_orb"]) samplePixelsInRegion(img, "mana_globe", regions["mana_globe"])
samplePixelsInRegion(img, "health_slice", regions["health_slice"])
samplePixelsInRegion(img, "mana_slice", regions["mana_slice"])
// Suggest new HSV ranges based on analysis // Suggest new HSV ranges based on analysis
fmt.Println("\n=== RECOMMENDATIONS ===") fmt.Println("\n=== RECOMMENDATIONS ===")
recommendHealthRange(img, regions["health_orb"]) recommendHealthRange(img, regions["health_globe"])
recommendManaRange(img, regions["mana_orb"]) recommendManaRange(img, regions["mana_globe"])
fmt.Printf("\nDebug images saved to: %s\n", debugDir) fmt.Printf("\nDebug images saved to: %s\n", debugDir)
fmt.Println("Run this tool after implementing the fixes to verify detection works correctly.") fmt.Println("Run this tool after implementing the fixes to verify detection works correctly.")

BIN
debug

Binary file not shown.

View file

@ -206,6 +206,36 @@ func (p *Pipeline) ReadOrbPercentage(frame image.Image, orbRegion image.Rectangl
return float64(filledPixels) / float64(totalPixels) return float64(filledPixels) / float64(totalPixels)
} }
// ReadOrbSlicePercentage reads an orb's fill level using a thin vertical slice.
// The orb fills from bottom to top, so we count matching pixels in the slice
// and return the percentage. This is much more accurate than scanning the whole orb.
func (p *Pipeline) ReadOrbSlicePercentage(frame image.Image, sliceRegion image.Rectangle, filledColor ColorRange) float64 {
bounds := sliceRegion.Intersect(frame.Bounds())
if bounds.Empty() {
return 0.0
}
totalPixels := 0
filledPixels := 0
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
c := frame.At(x, y)
hsv := RGBToHSV(c)
totalPixels++
if p.colorInRange(hsv, filledColor) {
filledPixels++
}
}
}
if totalPixels == 0 {
return 0.0
}
return float64(filledPixels) / float64(totalPixels)
}
// GetPixelColor returns the color at a specific pixel. // GetPixelColor returns the color at a specific pixel.
func (p *Pipeline) GetPixelColor(frame image.Image, x, y int) color.Color { func (p *Pipeline) GetPixelColor(frame image.Image, x, y int) color.Color {
return frame.At(x, y) return frame.At(x, y)
@ -281,10 +311,18 @@ func (p *Pipeline) colorsMatch(c1, c2 color.Color, tolerance int) bool {
} }
// colorInRange checks if HSV color is within range. // colorInRange checks if HSV color is within range.
// Supports hue wrapping: if UpperH > 360, it wraps around (e.g., 350-370 means 350-360 OR 0-10).
func (p *Pipeline) colorInRange(hsv HSV, colorRange ColorRange) bool { func (p *Pipeline) colorInRange(hsv HSV, colorRange ColorRange) bool {
return hsv.H >= colorRange.LowerH && hsv.H <= colorRange.UpperH && if hsv.S < colorRange.LowerS || hsv.S > colorRange.UpperS ||
hsv.S >= colorRange.LowerS && hsv.S <= colorRange.UpperS && hsv.V < colorRange.LowerV || hsv.V > colorRange.UpperV {
hsv.V >= colorRange.LowerV && hsv.V <= colorRange.UpperV return false
}
// Handle hue wrapping
if colorRange.UpperH > 360 {
// Wrapping range: e.g., 350-370 means H >= 350 OR H <= 10
return hsv.H >= colorRange.LowerH || hsv.H <= (colorRange.UpperH-360)
}
return hsv.H >= colorRange.LowerH && hsv.H <= colorRange.UpperH
} }
// floodFill finds connected pixels of the same color. // floodFill finds connected pixels of the same color.

View file

@ -56,11 +56,12 @@ type Config struct {
func DefaultConfig() Config { func DefaultConfig() Config {
return Config{ return Config{
Colors: Colors{ Colors: Colors{
// Calibrated HSV ranges from real D2R screenshot (testdata/d2r_1080p.png) // Calibrated from botty reference (720p scaled to 1080p)
// Health orb is bright RED: RGB ~(236,15,20) → HSV H 0-10, S 150+, V 150+ // Health orb RED: OpenCV H 178-183 → our H 356-366, wraps around 360
HealthFilled: HSVRange{0, 150, 150, 10, 255, 255}, // Using H 350-360 + H 0-10 range (handled by wrapping in colorInRange)
// Mana orb is BLUE: H 200-240, S 100+, V 80+ HealthFilled: HSVRange{350, 60, 20, 370, 255, 255}, // H wraps: 350-370 means 350-360,0-10
ManaFilled: HSVRange{200, 100, 80, 240, 255, 255}, // Mana orb BLUE: actual H 228-236 from screenshot
ManaFilled: HSVRange{226, 100, 20, 240, 255, 255},
ItemUnique: HSVRange{15, 100, 180, 30, 255, 255}, ItemUnique: HSVRange{15, 100, 180, 30, 255, 255},
ItemSet: HSVRange{35, 100, 150, 55, 255, 255}, ItemSet: HSVRange{35, 100, 150, 55, 255, 255},
ItemRare: HSVRange{15, 50, 200, 25, 150, 255}, ItemRare: HSVRange{15, 50, 200, 25, 150, 255},
@ -96,8 +97,12 @@ func RegisterProfiles(registry *resolution.Registry) error {
Height: 1080, Height: 1080,
Label: "1080p", Label: "1080p",
Regions: map[string]image.Rectangle{ Regions: map[string]image.Rectangle{
"health_orb": image.Rect(370, 945, 460, 1030), "health_globe": image.Rect(240, 870, 600, 1080), // Full health globe region
"mana_orb": image.Rect(1580, 910, 1670, 1000), "mana_globe": image.Rect(1330, 870, 1690, 1080), // Full mana globe region
"health_slice": image.Rect(415, 915, 425, 1060), // Thin vertical strip through orb center
"mana_slice": image.Rect(1505, 915, 1515, 1060), // Thin vertical strip through orb center
"health_orb": image.Rect(240, 870, 600, 1080), // Alias for backward compat
"mana_orb": image.Rect(1330, 870, 1690, 1080), // Alias for backward compat
"xp_bar": image.Rect(0, 1058, 1920, 1080), "xp_bar": image.Rect(0, 1058, 1920, 1080),
"belt": image.Rect(500, 990, 900, 1040), "belt": image.Rect(500, 990, 900, 1040),
"minimap": image.Rect(1600, 0, 1920, 320), "minimap": image.Rect(1600, 0, 1920, 320),
@ -113,8 +118,12 @@ func RegisterProfiles(registry *resolution.Registry) error {
Height: 720, Height: 720,
Label: "720p", Label: "720p",
Regions: map[string]image.Rectangle{ Regions: map[string]image.Rectangle{
"health_orb": image.Rect(247, 630, 307, 687), "health_globe": image.Rect(160, 580, 400, 720),
"mana_orb": image.Rect(1053, 607, 1113, 667), "mana_globe": image.Rect(887, 580, 1127, 720),
"health_slice": image.Rect(309, 610, 316, 711),
"mana_slice": image.Rect(961, 610, 968, 711),
"health_orb": image.Rect(160, 580, 400, 720),
"mana_orb": image.Rect(887, 580, 1127, 720),
"xp_bar": image.Rect(0, 705, 1280, 720), "xp_bar": image.Rect(0, 705, 1280, 720),
"belt": image.Rect(333, 660, 600, 693), "belt": image.Rect(333, 660, 600, 693),
"minimap": image.Rect(1067, 0, 1280, 213), "minimap": image.Rect(1067, 0, 1280, 213),

BIN
testdata/debug/debug_health_globe.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

BIN
testdata/debug/debug_health_slice.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
testdata/debug/debug_mana_globe.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
testdata/debug/debug_mana_slice.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB