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"
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{
"health_orb": image.Rect(370, 945, 460, 1030),
"mana_orb": image.Rect(1580, 910, 1670, 1000),
"health_globe": image.Rect(240, 870, 600, 1080),
"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),
"belt": image.Rect(500, 990, 900, 1040),
"minimap": image.Rect(1600, 0, 1920, 320),
@ -48,21 +52,43 @@ func main() {
fmt.Println("\n=== REGION ANALYSIS ===")
// Analyze health orb
analyzeRegion(img, "health_orb", regions["health_orb"], config.Colors.HealthFilled, debugDir)
// Analyze health globe (full region)
analyzeRegion(img, "health_globe", regions["health_globe"], config.Colors.HealthFilled, debugDir)
// Analyze mana orb
analyzeRegion(img, "mana_orb", regions["mana_orb"], config.Colors.ManaFilled, debugDir)
// Analyze mana globe (full region)
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
fmt.Println("\n=== PIXEL SAMPLING ===")
samplePixelsInRegion(img, "health_orb", regions["health_orb"])
samplePixelsInRegion(img, "mana_orb", regions["mana_orb"])
samplePixelsInRegion(img, "health_globe", regions["health_globe"])
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
fmt.Println("\n=== RECOMMENDATIONS ===")
recommendHealthRange(img, regions["health_orb"])
recommendManaRange(img, regions["mana_orb"])
recommendHealthRange(img, regions["health_globe"])
recommendManaRange(img, regions["mana_globe"])
fmt.Printf("\nDebug images saved to: %s\n", debugDir)
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)
}
// 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.
func (p *Pipeline) GetPixelColor(frame image.Image, x, y int) color.Color {
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.
// 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 {
return hsv.H >= colorRange.LowerH && hsv.H <= colorRange.UpperH &&
hsv.S >= colorRange.LowerS && hsv.S <= colorRange.UpperS &&
hsv.V >= colorRange.LowerV && hsv.V <= colorRange.UpperV
if hsv.S < colorRange.LowerS || hsv.S > colorRange.UpperS ||
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.

View file

@ -56,11 +56,12 @@ type Config struct {
func DefaultConfig() Config {
return Config{
Colors: Colors{
// Calibrated HSV ranges from real D2R screenshot (testdata/d2r_1080p.png)
// Health orb is bright RED: RGB ~(236,15,20) → HSV H 0-10, S 150+, V 150+
HealthFilled: HSVRange{0, 150, 150, 10, 255, 255},
// Mana orb is BLUE: H 200-240, S 100+, V 80+
ManaFilled: HSVRange{200, 100, 80, 240, 255, 255},
// Calibrated from botty reference (720p scaled to 1080p)
// Health orb RED: OpenCV H 178-183 → our H 356-366, wraps around 360
// Using H 350-360 + H 0-10 range (handled by wrapping in colorInRange)
HealthFilled: HSVRange{350, 60, 20, 370, 255, 255}, // H wraps: 350-370 means 350-360,0-10
// 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},
ItemSet: HSVRange{35, 100, 150, 55, 255, 255},
ItemRare: HSVRange{15, 50, 200, 25, 150, 255},
@ -96,8 +97,12 @@ func RegisterProfiles(registry *resolution.Registry) error {
Height: 1080,
Label: "1080p",
Regions: map[string]image.Rectangle{
"health_orb": image.Rect(370, 945, 460, 1030),
"mana_orb": image.Rect(1580, 910, 1670, 1000),
"health_globe": image.Rect(240, 870, 600, 1080), // Full health globe region
"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),
"belt": image.Rect(500, 990, 900, 1040),
"minimap": image.Rect(1600, 0, 1920, 320),
@ -113,8 +118,12 @@ func RegisterProfiles(registry *resolution.Registry) error {
Height: 720,
Label: "720p",
Regions: map[string]image.Rectangle{
"health_orb": image.Rect(247, 630, 307, 687),
"mana_orb": image.Rect(1053, 607, 1113, 667),
"health_globe": image.Rect(160, 580, 400, 720),
"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),
"belt": image.Rect(333, 660, 600, 693),
"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