Files
my-botty/src/d2r_image/processing_helpers.py
T
FiskenPoul 97f4e55643 fix: comprehensive stability, stealth, and logic improvements
- Stability: Replaced unsafe kill_thread() with cooperative_shutdown() in GraphicDebugger.
- Stealth: Integrated jittered wait() and human-like typing variation in win_input.py.
- Robustness: Switched to absolute paths for template loading and log rotation anchoring.
- Bugfix: Resolved AttributeError: 'ScreenObjects' has no attribute 'MercPanelText'.
- Logic: Implemented thread-safe state in DeathManager and re-indexed PickedUpResult enum.
- Pickup: Added failure blacklisting for unreachable items to prevent infinite loops.
- Stash: Fixed gold stashing to correctly respect fill_shared_stash_first=0 (personal first).
- Config: Updated settings validation to allow common D2R variations (Gamma, VSync, etc.).
2026-06-06 11:24:20 +02:00

658 lines
33 KiB
Python

import cv2
import numpy as np
import re
import time
import math
import copy
from d2r_image.data_models import GroundItem, GroundItemList, ItemQuality, ItemQualityKeyword, ItemText
from d2r_image.bnip_data import NTIP_ALIAS_QUALITY_MAP
from d2r_image.bnip_helpers import basename_to_types
from d2r_image.ocr import image_to_text
from d2r_image.processing_data import Runeword
import d2r_image.d2data_lookup as d2data_lookup
from d2r_image.d2data_lookup import fuzzy_base_item_match
from d2r_image.processing_data import EXPECTED_HEIGHT_RANGE, EXPECTED_WIDTH_RANGE, GAUS_FILTER, ITEM_COLORS, QUALITY_COLOR_MAP, Runeword, BOX_EXPECTED_HEIGHT_RANGE, BOX_EXPECTED_WIDTH_RANGE
from d2r_image.strings_store import base_items
from utils.misc import color_filter, erode_to_black, slugify
from d2r_image.ocr import image_to_text
from ui_manager import get_hud_mask
from screen import convert_screen_to_monitor
from utils.misc import color_filter, cut_roi, roi_center
from logger import Logger
from config import Config
import template_finder
gold_regex = re.compile(r'(^[0-9]+)\sGOLD')
import time
def crop_text_clusters(inp_img: np.ndarray, padding_y: int = 5) -> list[ItemText]:
cleaned_img = clean_img(inp_img)
# Cluster item names
item_clusters = []
for key in ITEM_COLORS:
_, filtered_img = color_filter(cleaned_img, Config().colors[key])
filtered_img_gray = cv2.cvtColor(filtered_img, cv2.COLOR_BGR2GRAY)
gaus = GAUS_FILTER
if key == "gray":
# white text has some gray on border of glyphs, erode
filtered_img_gray = cv2.erode(filtered_img_gray, np.ones((2, 1), 'uint8'), None, iterations=1)
gaus = (GAUS_FILTER[0] + 4, GAUS_FILTER[1])
blured_img = np.clip(cv2.GaussianBlur(
filtered_img_gray, gaus, cv2.BORDER_DEFAULT), 0, 255)
contours = cv2.findContours(
blured_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
for cntr in contours:
x, y, w, h = cv2.boundingRect(cntr)
expected_height = EXPECTED_HEIGHT_RANGE[0] < h < EXPECTED_HEIGHT_RANGE[1]
expected_width = EXPECTED_WIDTH_RANGE[0] < w < EXPECTED_WIDTH_RANGE[1]
if not (expected_height and expected_width): continue
# increase height a bit to make sure we have the full item name in the cluster
y = y - padding_y if y > padding_y else 0
h += padding_y * 2
cropped_item = filtered_img[y:y+h, x:x+w]
avg = int(np.average(filtered_img_gray[y:y+h, x:x+w]))
contains_black = np.min(cropped_item) < 14
mostly_dark = avg < 35
if contains_black and mostly_dark:
# double-check item color
color_averages = []
for key2 in ITEM_COLORS:
_, extracted_img = color_filter(cropped_item, Config().colors[key2])
extr_avg = np.average(cv2.cvtColor(
extracted_img, cv2.COLOR_BGR2GRAY))
color_averages.append(extr_avg)
max_idx = color_averages.index(max(color_averages))
if key == ITEM_COLORS[max_idx]:
item_clusters.append(ItemText(
color=key,
quality=QUALITY_COLOR_MAP[key],
roi=[x, y, w, h],
img=inp_img[y:y+h, x:x+w],
clean_img=cleaned_img[y:y+h, x:x+w]
))
cluster_images = [key["clean_img"] for key in item_clusters]
results = image_to_text(cluster_images, model="ground-eng_inconsolata_inv_th_fast", psm=7, erode=True)
for count, cluster in enumerate(item_clusters):
setattr(cluster, "ocr_result", results[count])
return item_clusters
def crop_item_tooltip(image: np.ndarray, model: str = "hover-eng_inconsolata_inv_th_fast") -> tuple[ItemText, str]:
"""
Crops visible item description boxes / tooltips
:inp_img: image from hover over item of interest.
:model: which ocr model to use
"""
res = ItemText()
quality = None
black_mask = color_filter(image, Config().colors["black"])[0]
contours = cv2.findContours(
black_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
for cntr in contours:
x, y, w, h = cv2.boundingRect(cntr)
cropped_item = image[y:y+h, x:x+w]
if not (expected_height := BOX_EXPECTED_HEIGHT_RANGE[0] < h < BOX_EXPECTED_HEIGHT_RANGE[1]):
continue
if not (expected_width := BOX_EXPECTED_WIDTH_RANGE[0] < w < BOX_EXPECTED_WIDTH_RANGE[1]):
continue
avg = np.average(cv2.cvtColor(cropped_item, cv2.COLOR_BGR2GRAY))
if not (mostly_dark := 0 < avg < 35):
continue
if not (contains_black := np.min(cropped_item) < 14):
continue
contains_white = np.max(cropped_item) > 250
contains_orange = False
if not contains_white:
# check for orange (like key of destruction, etc.)
orange_mask, _ = color_filter(cropped_item, Config().colors["orange"])
contains_orange = np.min(orange_mask) > 0
if not (contains_white or contains_orange):
continue
# check to see if contour overlaps right inventory
right_inv = Config().ui_roi["right_inventory"]
overlaps_inventory = not (
x+w < right_inv[0] or right_inv[0]+right_inv[2] < x or y+h+70 < right_inv[1] or right_inv[1]+right_inv[3] < y)
if not overlaps_inventory:
left_inv = Config().ui_roi["left_inventory"]
overlaps_inventory |= not (
x+w < left_inv[0] or left_inv[0]+left_inv[2] < x or y+h+70 < left_inv[1] or left_inv[1]+left_inv[3] < y)
if not overlaps_inventory:
continue
#print(f"x: {x}, y: {y}, w: {w}, h: {h}")
footer_y = (y + h) if (y + h) < 700 else 700
footer_h = 720 - footer_y
found_footer = template_finder.search(["TO_TOOLTIP"], image, threshold=0.8, roi=[x, footer_y, w, footer_h]).valid
if found_footer:
res.ocr_result = image_to_text(cropped_item, psm=6, model=model)[0]
first_row = cut_roi(copy.deepcopy(cropped_item), (0, 0, w, 26))
if _contains_color(first_row, "green"):
quality = ItemQuality.Set.value
elif _contains_color(first_row, "gold"):
quality = ItemQuality.Unique.value
elif _contains_color(first_row, "yellow"):
quality = ItemQuality.Rare.value
elif _contains_color(first_row, "blue"):
quality = ItemQuality.Magic.value
elif _contains_color(first_row, "orange"):
quality = ItemQuality.Crafted.value
elif _contains_color(first_row, "red"):
quality = ItemQuality.Crafted.value
elif _contains_color(first_row, "white"):
quality = ItemQuality.Normal.value
elif _contains_color(first_row, "gray"):
if "SUPERIOR" in res.ocr_result.text[:10]:
quality = ItemQuality.Superior.value
else:
quality = ItemQuality.Gray.value
else:
quality = ItemQuality.Normal.value
res.roi = [x, y, w, h]
res.img = cropped_item
break
return res, quality
def _contains_color(img: np.ndarray, color: str) -> bool:
mask = color_filter(img, Config().colors[color])[0]
return np.average(mask) > 0
def clean_img(inp_img: np.ndarray, black_thresh: int = 14) -> np.ndarray:
img = inp_img[:, :, :]
if img.shape[0] == get_hud_mask().shape[0] and img.shape[1] == get_hud_mask().shape[1]:
img = cv2.bitwise_and(img, img, mask=get_hud_mask())
# In order to not filter out highlighted items, change their color to black
highlight_mask = color_filter(img, Config().colors["item_highlight"])[0]
img[highlight_mask > 0] = (0, 0, 0)
img = erode_to_black(img, black_thresh)
return img
def get_items_by_quality(crop_result: list[ItemText]):
items_by_quality = {}
for quality in ItemQuality:
items_by_quality[quality.value] = []
for item in crop_result:
quality = None
if item.quality.value == ItemQuality.Orange.value:
is_rune = ' RUNE' in item.ocr_result.text
if is_rune:
quality = ItemQuality.Rune
else:
quality = ItemQuality.Crafted
elif item.quality.value == ItemQuality.Unique.value:
is_runeword = False
try:
Runeword(item.ocr_result.text)
is_runeword = True
except:
pass
quality = ItemQuality.Runeword if is_runeword else item.quality
elif item.quality.value == ItemQuality.Gray.value:
quality = ItemQuality.Gray
elif item.quality.value == ItemQuality.Red.value:
quality = ItemQuality.Crafted
else:
quality = item.quality
items_by_quality[quality.value].append({
'color': item.color,
'quality': quality,
'x': item.roi[0],
'y': item.roi[1],
'w': item.roi[2],
'h': item.roi[3],
'text': item.ocr_result.text
})
return items_by_quality
def consolidate_clusters(items_by_quality):
if len(items_by_quality) == 0:
return
cco_start = time.time()
consolidate_overlapping_names(items_by_quality)
cco_end = time.time()
cco = round(cco_end-cco_start, 2)
ccr_start = time.time()
consolidate_rares(items_by_quality)
ccr_end = time.time()
ccr = round(ccr_end-ccr_start, 2)
ccs_start = time.time()
consolidate_quality(
items_by_quality[ItemQuality.Set.value],
items_by_quality[ItemQuality.Set.value])
ccs_end = time.time()
ccs = round(ccs_end-ccs_start, 2)
ccu_start = time.time()
consolidate_quality(
items_by_quality[ItemQuality.Unique.value],
items_by_quality[ItemQuality.Unique.value])
ccu_end = time.time()
ccu = round(ccu_end-ccu_start, 2)
ccrw_start = time.time()
consolidate_quality(
items_by_quality[ItemQuality.Runeword.value],
items_by_quality[ItemQuality.Gray.value])
ccrw_end = time.time()
ccrw = round(ccrw_end-ccrw_start, 2)
# print(f'CCO: {cco}\tCCR: {ccr}\tCCS: {ccs}\tCCU: {ccu}\tCCRW: {ccrw}')
def consolidate_overlapping_names(items_by_quality):
items_to_remove = {}
for quality in items_by_quality:
for item in items_by_quality[quality]:
overlapping_item = None
for item_to_check in items_by_quality[quality]:
if item is item_to_check or abs(item['y'] - item_to_check['y']) > 3:
continue
if quality in items_to_remove:
if any(item is x for x in items_to_remove[quality]) or any(item_to_check is x for x in items_to_remove[quality]):
continue
if item['x'] < item_to_check['x'] + item_to_check['w'] and\
item['x'] + item['w'] > item_to_check['x'] and\
item['y'] < item_to_check['y'] + item_to_check['h'] and\
item['y'] + item['h'] > item_to_check['y']:
overlapping_item = item_to_check
break
if overlapping_item:
first_item = item if item['x'] < overlapping_item['x'] else overlapping_item
second_item = item if first_item is overlapping_item else overlapping_item
first_item_text = first_item['text'].strip().replace('\'', '')
second_item_text = second_item['text'].strip().replace('\'', '').lstrip()
new_text = f"{first_item_text}\' {second_item_text}"
if quality == ItemQuality.Set.value:
if not d2data_lookup.find_set_item_by_name(new_text):
break
elif quality == ItemQuality.Unique.value:
if not d2data_lookup.find_unique_item_by_name(new_text):
break
first_item['text'] = new_text
first_item['name'] = new_text
first_item['x'] = first_item['x'] if second_item['x'] > first_item['x'] else second_item['x']
first_item['y'] = first_item['y'] if second_item['y'] > first_item['y'] else second_item['y']
first_item['w'] = second_item['x'] + second_item['w'] - first_item['x']
first_item['h'] = first_item['h']
if quality not in items_to_remove:
items_to_remove[quality] = []
items_to_remove[quality].append(second_item)
for quality in items_to_remove:
for item in items_to_remove[quality]:
items_by_quality[quality] = [x for x in items_by_quality[quality] if x is not item]
def consolidate_rares(items_by_quality):
for item in items_by_quality[ItemQuality.Rare.value]:
closest_dist = 99999
closest_base = None
d2data_base = None
if not d2data_lookup.is_base(item['text']):
for base in items_by_quality[ItemQuality.Rare.value]:
if d2data_lookup.is_base(base['text']):
base_item = d2data_lookup.get_base(base['text'])
dist = round(math.dist(
(item['x'], item['y']),
(base['x'], base['y'])
), 0)
if dist < closest_dist:
closest_dist = dist
closest_base = base
d2data_base = base_item
if not closest_base:
return
item['x'] = item['x'] if closest_base['x'] > item['x'] else closest_base['x']
item['y'] = item['y'] if closest_base['y'] > item['y'] else closest_base['y']
item['w'] = item['w'] if closest_base['w'] < item['w'] else closest_base['w']
item['h'] = item['h'] + closest_base['h']
item['base'] = d2data_base
item['item'] = d2data_base
item['identified'] = True
items_by_quality[ItemQuality.Rare.value] = [x for x in items_by_quality[ItemQuality.Rare.value] if x is not closest_base]
def consolidate_quality(quality_items, potential_bases):
bases_to_remove = []
for item in quality_items:
if not d2data_lookup.is_base(item['text']):
# result = d2data_lookup.find_item_by_display_name(item['text'])
result = d2data_lookup.find_set_or_unique_item_by_name(item['text'], item['quality'])
if not result:
continue
closest_dist = 99999
closest_base = None
for base in potential_bases:
if base['y'] > item['y'] and d2data_lookup.is_base(base['text']):
dist = round(math.dist(
(item['x'], item['y']),
(base['x'], base['y'])
), 0)
if dist < closest_dist:
closest_dist = dist
closest_base = base
if not closest_base:
continue
item['x'] = item['x'] if closest_base['x'] > item['x'] else closest_base['x']
item['y'] = item['y'] if closest_base['y'] > item['y'] else closest_base['y']
item['w'] = item['w'] if closest_base['w'] < item['w'] else closest_base['w']
item['h'] = item['h'] + closest_base['h']
item['item'] = result
item['name'] = result['DisplayName']
item['base'] = d2data_lookup.get_base(closest_base['text'])
item['identified'] = True
bases_to_remove.append(closest_base)
for base_to_remove in bases_to_remove:
potential_bases[:] = [x for x in potential_bases if x is not base_to_remove]
def find_base_and_remove_items_without_a_base(items_by_quality) -> dict:
items_to_remove = {}
gray_normal_magic_removed = {}
items_to_add = {}
resolved_runewords = []
for quality in items_by_quality:
if quality in [ItemQuality.Gray.value, ItemQuality.Normal.value, ItemQuality.Magic]:
gray_normal_magic_removed.update(set_gray_and_normal_and_magic_base_items(items_by_quality))
for item in items_by_quality[quality]:
quality_keyword, normalized_text = get_normalized_normal_gray_item_text(item['text'])
if not normalized_text in base_items() and not any(chr.isdigit() for chr in item['text']) and not gold_regex.search(item['text']):
item['text'] = f"{quality_keyword} {fuzzy_base_item_match(normalized_text)}".strip()
if 'base' not in item:
if quality == ItemQuality.Magic.value:
base = d2data_lookup.get_base(item['text'])
if base:
item['base'] = base
elif quality == ItemQuality.Rune.value:
if d2data_lookup.is_rune(item['text']):
item['base'] = d2data_lookup.get_rune(item['text'])
item['item'] = d2data_lookup.get_rune(item['text'])
item['identified'] = True
elif d2data_lookup.is_base(item['text']):
item['base'] = d2data_lookup.get_base(item['text'])
item['name'] = item['base']['DisplayName']
else:
if quality not in items_to_remove:
items_to_remove[quality] = []
items_to_remove[quality].append(item)
for quality in items_to_remove:
for item in items_to_remove[quality]:
if quality == ItemQuality.Unique.value:
for unique_item in items_by_quality[quality]:
if 'item' not in unique_item and 'base' in unique_item and 'uniques' in unique_item['base']:
closest_dist = 99999
closest_base = None
closest_unique_name = None
for possible_unique in unique_item['base']['uniques']:
normalized_name = possible_unique.replace('_', ' ').upper()
if normalized_name in item['text']:
dist = round(math.dist(
(item['x'], item['y']),
(unique_item['x'], unique_item['y'])
), 0)
if dist < closest_dist:
closest_dist = dist
closest_base = unique_item
closest_unique_name = normalized_name
if closest_base:
unique_name_width = len(closest_unique_name) * 11
offset = item['text'].find(closest_unique_name) * 11
unique_item['x'] = item['x'] + offset
unique_item['y'] -= item['h']
unique_item['w'] = unique_name_width if unique_name_width > item['w'] else item['w'] if item['w'] < unique_name_width * 1.4 else unique_name_width
unique_item['h'] += item['h']
unique_item['text'] = closest_unique_name
unique_item['item'] = d2data_lookup.find_unique_item_by_name(closest_unique_name)
unique_item['identified'] = True
elif quality == ItemQuality.Runeword.value:
if 'item' not in item:
closest_dist = 99999
closest_base = None
for possible_base in items_by_quality[ItemQuality.Gray.value]:
dist = round(math.dist(
(item['x'], item['y']),
(possible_base['x'], possible_base['y'])
), 0)
if dist < closest_dist:
closest_dist = dist
closest_base = possible_base
if closest_base:
closest_base['quality'] = ItemQuality.Runeword
closest_base['name'] = item['text']
closest_base['x'] = item['x'] if item['x'] < closest_base['x'] else closest_base['x']
closest_base['y'] = item['y']
closest_base['w'] = item['w'] if item['w'] > closest_base['w'] else closest_base['w']
closest_base['h'] += item['h']
closest_base['item'] = {}
closest_base['identified'] = True
if quality not in items_to_add:
items_to_add[quality] = []
items_to_add[quality].append(closest_base)
items_by_quality[ItemQuality.Gray.value] = [x for x in items_by_quality[ItemQuality.Gray.value] if x is not closest_base]
resolved_runewords.append(item)
items_by_quality[quality] = [x for x in items_by_quality[quality] if x is not item]
for quality in items_to_add:
if quality not in items_by_quality:
items_by_quality[quality] = []
for item in items_to_add[quality]:
items_by_quality[quality].append(item)
for runeword in resolved_runewords:
items_to_remove[ItemQuality.Runeword.value] = [x for x in items_to_remove[ItemQuality.Runeword.value] if x is not runeword]
for quality in gray_normal_magic_removed:
for item in gray_normal_magic_removed[quality]:
if item['quality'].value not in items_to_remove:
items_to_remove[item['quality'].value] = []
items_to_remove[item['quality'].value].append(item)
# items_removed = items_to_remove.update(gray_normal_magic_removed)
return items_to_remove
def get_normalized_normal_gray_item_text(item_text):
found_keyword_text = None
if item_text.startswith('OW QUALITY'):
item_text = item_text.replace('OW QUALITY', ItemQualityKeyword.LowQuality.value)
if ItemQualityKeyword.LowQuality.value in item_text:
found_keyword_text = ItemQualityKeyword.LowQuality.value
elif ItemQualityKeyword.Cracked.value in item_text:
found_keyword_text = ItemQualityKeyword.Cracked.value
elif ItemQualityKeyword.Crude.value in item_text:
found_keyword_text = ItemQualityKeyword.Crude.value
elif ItemQualityKeyword.Damaged.value in item_text:
found_keyword_text = ItemQualityKeyword.Damaged.value
elif ItemQualityKeyword.Superior.value in item_text:
found_keyword_text = ItemQualityKeyword.Superior.value
if found_keyword_text:
return ItemQualityKeyword(found_keyword_text), item_text.replace(f'{found_keyword_text} ', '')
return None, item_text
def set_gray_and_normal_and_magic_base_items(items_by_quality):
items_to_remove = {
}
for quality in items_by_quality:
if quality in [ItemQuality.Gray.value, ItemQuality.Normal.value]:
for item in items_by_quality[quality]:
quality_keyword, normalized_text = get_normalized_normal_gray_item_text(item['text'])
result = d2data_lookup.get_base(normalized_text)
#print(f"{quality_keyword} {normalized_text} {result}")
if not result:
gold_match = gold_regex.search(item['text'])
if gold_match:
item['base'] = d2data_lookup.get_consumable('GOLD')
item['amount'] = gold_match.group(1)
continue
else:
# fuzzy match
new_string = ""
if quality_keyword:
new_string += f"{quality_keyword} "
new_string += f"{fuzzy_base_item_match(normalized_text)}"
item['text'] = new_string.strip()
quality_keyword, normalized_text = get_normalized_normal_gray_item_text(item['text'])
result = d2data_lookup.get_base(normalized_text)
if result:
item['base'] = result
item['item'] = result
item['identified'] = True
item['name'] = item['base']['DisplayName']
if quality_keyword:
item['quality'] = quality_keyword
else:
if d2data_lookup.is_consumable(item['text']):
item['base'] = d2data_lookup.get_consumable(item['text'])
item['name'] = item['base']['DisplayName']
item['identified'] = True
elif d2data_lookup.is_gem(item['text']):
item['base'] = d2data_lookup.get_gem(item['text'])
item['name'] = item['base']['DisplayName']
item['identified'] = True
else:
if quality not in items_to_remove:
items_to_remove[quality] = []
items_to_remove[quality].append(item)
Logger.debug(f"Discarding item {item['text']} (quality: {quality}) - no base found")
elif quality == ItemQuality.Magic.value:
for item in items_by_quality[quality]:
item_is_identified = d2data_lookup.magic_item_is_identified(item['text'])
base = d2data_lookup.find_base_item_from_magic_item_text(item['text'], item_is_identified)
if base:
item['base'] = base
if len(item['text'].lower().replace(base['DisplayName'].lower(), '').replace(' ', '')) > 0:
item['item'] = base
item['identified'] = True
else:
item['name'] = base['DisplayName']
else:
if quality not in items_to_remove:
items_to_remove[quality] = []
items_to_remove[quality].append(item)
for quality in items_to_remove:
for item in items_to_remove[quality]:
items_by_quality[quality] = [x for x in items_by_quality[quality] if x is not item]
return items_to_remove
def set_set_and_unique_base_items(items_by_quality):
for quality in items_by_quality:
for item in items_by_quality[quality]:
if 'item' in item:
continue
item['identified'] = False
if quality == ItemQuality.Unique.value and 'uniques' in item['base']:
if len(item['base']['uniques']) == 1:
unique_name = item['base']['uniques'][0].replace('_', ' ').upper()
item['item'] = d2data_lookup.find_unique_item_by_name(unique_name)
if item['item']:
item['name'] = item['item']['DisplayName']
else:
Logger.error(f"Failed to find unique {unique_name} for base {item['base']} in d2r data.")
else:
item['uniqueItems'] = []
for unique_item in item['base']['uniques']:
unique_name = unique_item.replace('_', ' ').upper()
rs = d2data_lookup.find_unique_item_by_name(unique_name)
if rs:
item['uniqueItems'].append(rs)
else:
Logger.error(f"Failed to find unique {unique_name} for base {item['base']} in d2r data.")
elif quality == ItemQuality.Set.value and 'sets' in item['base']:
if len(item['base']['sets']) == 1:
set_name = item['base']['sets'][0]
item['item'] = d2data_lookup.find_set_item_by_name(set_name)
if item['item']:
item['name'] = item['item']['DisplayName']
else:
Logger.error(f"Failed to find set {set_name} for base {item['base']} in d2r data.")
else:
item['setItems'] = []
for set_item in item['base']['sets']:
set_name = set_item.replace('_', ' ').upper()
rs = d2data_lookup.find_set_item_by_name(set_name)
if rs:
item['setItems'].append(rs)
else:
Logger.error(f"Failed to find set {set_name} for base {item['base']} in d2r data.")
def build_d2_items(items_by_quality: dict) -> GroundItemList | None:
ground_item_list = GroundItemList([])
d2_items = ground_item_list.items
for quality in items_by_quality:
for item in items_by_quality[quality]:
try:
bounding_box = [item['x'], item['y'], item['w'], item['h']]
bounding_box_monitor = [round(x) for x in [*convert_screen_to_monitor((item['x'], item['y'])), item['w'], item['h']]]
center = roi_center(bounding_box)
center_monitor = (round(x) for x in convert_screen_to_monitor(center))
new_item = GroundItem(
BoundingBox=dict(zip(["x", "y", "w", "h"], bounding_box)),
BoundingBoxMonitor=dict(zip(["x", "y", "w", "h"], bounding_box_monitor)),
Center=dict(zip(["x", "y"], center)),
CenterMonitor=dict(zip(["x", "y"], center_monitor)),
#The in game distance is the same for width and height. It takes the same time to move to the side of the screen as the top although less pixels.
#Therefore, when calculating distance we weigh the y location 1.7x to give same weight as the x location.
Distance=round(math.dist((item['x']+item['w']/2, (item['y']+item['h']+30)*1.777778), (Config().ui_pos["screen_width"] / 2, Config().ui_pos["screen_height"] / 2))),
Name=item['name'] if 'name' in item else item['text'],
Color=item['color'],
Quality=item['quality'].value,
Text=item['text'],
Amount=None if not "amount" in item else item['amount'],
BaseItem=item['base'],
Item=item['item'] if 'item' in item and item['item'] != item['base'] else None,
NTIPAliasType=basename_to_types(item['base']['DisplayName']),
# NTIPAliasType=None if item['base']['DisplayName'] not in BNIP_ITEM_TYPE_DATA else BNIP_ITEM_TYPE_DATA[item['base']['DisplayName']],
NTIPAliasClassID=item['base']['NTIPAliasClassID'],
NTIPAliasClass=item['base']['NTIPAliasClass'] if 'NTIPAliasClass' in item['base'] else None,
NTIPAliasQuality=NTIP_ALIAS_QUALITY_MAP[item['quality'].value],
NTIPAliasFlag={
'0x10': item['identified'],
'0x4000000': item['quality'] == ItemQuality.Runeword.value,
"0x400000": item['quality'] == ItemQuality.Gray.value,
}
)
new_item.ID = slugify(f"{new_item.Name}_{'_'.join([str(value) for _, value in new_item.as_dict().items()])}")
new_item.UID = f"{new_item.ID}_{'_'.join([str(value) for value in center])}"
if d2_items is None:
d2_items = []
d2_items.append(new_item)
except Exception as e:
Logger.error(f'failed on item: {item} with error {e}')
return ground_item_list
if __name__ == "__main__":
from input_layer import keyboard
import os
from screen import start_detecting_window, grab, stop_detecting_window
start_detecting_window()
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or stop_detecting_window() or os._exit(1))
print("Go to D2R window and press f11 to start")
keyboard.wait("f11")
from config import Config
# while 1:
# img_o = grab()
# img = img_o[:, :, :]
# # In order to not filter out highlighted items, change their color to black
# highlight_mask = color_filter(img, Config().colors["item_highlight"])[0]
# img[highlight_mask > 0] = (0, 0, 0)
# img = erode_to_black(img, 14)
# _, filtered_img = color_filter(img, Config().colors["gray"])
# filtered_img_gray = cv2.cvtColor(filtered_img, cv2.COLOR_BGR2GRAY)
# eroded_img_gray = cv2.erode(filtered_img_gray, np.ones((2, 1), 'uint8'), None, iterations=1)
# blured_img = np.clip(cv2.GaussianBlur(eroded_img_gray, (17, 1), cv2.BORDER_DEFAULT), 0, 255)
# cv2.imshow('test', blured_img)
# key = cv2.waitKey(3000)
while 1:
img_o = grab()
tooltip = crop_item_tooltip(img_o)
print(tooltip)
cv2.imshow('test', tooltip[0].img)
#cv2.imshow('test', tooltip[0].ocr_result.processed_img)
key = cv2.waitKey(20000)