NovaImageTagObject.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. # -*- encoding: utf-8 -*-
  2. """
  3. @File : NovaImageTagObject.py
  4. @Time : 2025/8/29 09:03
  5. @Author : stephen
  6. @Email : zhangdongming@asj6.wecom.work
  7. @Software: PyCharm
  8. """
  9. import base64
  10. import imghdr
  11. import json
  12. import logging
  13. import re
  14. import boto3
  15. LOGGER = logging.getLogger('time')
  16. # --- 配置信息 ---
  17. MODEL_ID = "us.amazon.nova-lite-v1:0"
  18. class NovaImageTagObject(object):
  19. def __init__(self, aws_access_key_id, secret_access_key, region_name):
  20. self.bedrock = boto3.client(
  21. 'bedrock-runtime',
  22. aws_access_key_id=aws_access_key_id,
  23. aws_secret_access_key=secret_access_key,
  24. region_name=region_name
  25. )
  26. @staticmethod
  27. def safe_json_load(json_string):
  28. """
  29. 一个更健壮的JSON解析函数,尝试修复常见的模型输出格式问题。
  30. """
  31. try:
  32. # 寻找被代码块包围的JSON
  33. json_match = re.search(r'```json\s*([\s\S]*?)\s*```', json_string)
  34. if json_match:
  35. json_string = json_match.group(1)
  36. # 寻找常规的JSON对象或数组
  37. json_match = re.search(r'\{.*\}|\[.*\]', json_string, re.DOTALL)
  38. if json_match:
  39. json_string = json_match.group(0)
  40. return json.loads(json_string)
  41. except json.JSONDecodeError:
  42. LOGGER.error("JSON解析失败,尝试修复...")
  43. try:
  44. json_string = re.sub(r"(\w+):", r'"\1":', json_string)
  45. json_string = json_string.replace("'", '"')
  46. return json.loads(json_string)
  47. except Exception as e:
  48. LOGGER.error(f"无法解析模型返回的JSON: {e}")
  49. return None
  50. except Exception as e:
  51. LOGGER.error(f"发生未知解析错误: {e}")
  52. return None
  53. @staticmethod
  54. def format_and_convert_detections(nova_detections: list) -> list:
  55. """
  56. 将Nova模型返回的坐标转换为您指定的详细格式,包含原始坐标和Rekognition比例。
  57. """
  58. formatted_results = []
  59. if not isinstance(nova_detections, list):
  60. return []
  61. for item in nova_detections:
  62. if not isinstance(item, dict): continue
  63. label = list(item.keys())[0]
  64. nx1, ny1, nx2, ny2 = item[label]
  65. left = nx1 / 1000.0
  66. top = ny1 / 1000.0
  67. width = (nx2 - nx1) / 1000.0
  68. height = (ny2 - ny1) / 1000.0
  69. formatted_results.append({
  70. "x1": nx1, "x2": nx2, "y1": ny1, "y2": ny2,
  71. "Width": f"{width:.5f}", "Height": f"{height:.5f}",
  72. "Top": f"{top:.5f}", "Left": f"{left:.5f}",
  73. "class": label
  74. })
  75. return formatted_results
  76. @staticmethod
  77. def normalize_b64(b64_str: str) -> str:
  78. """清理并补齐base64字符串"""
  79. if not b64_str:
  80. return ""
  81. b64_str = re.sub(r"^data:image/[^;]+;base64,", "", b64_str)
  82. b64_str = b64_str.strip().replace("\n", "").replace(" ", "")
  83. padding = 4 - (len(b64_str) % 4)
  84. if padding and padding != 4:
  85. b64_str += "=" * padding
  86. return b64_str
  87. def process_image_batch(self, base64_images: list, categories: list, uid=''):
  88. """
  89. 通过单次API调用处理一批图片,并返回结构化的检测结果。
  90. """
  91. if not base64_images:
  92. LOGGER.error(f"{uid}错误: 未提供图片数据。")
  93. return {}
  94. image_contents = []
  95. img_bytes_list = []
  96. for idx, b64_image in enumerate(base64_images, start=1):
  97. try:
  98. # 规范化 base64
  99. b64_image = self.normalize_b64(b64_image)
  100. if not b64_image:
  101. raise ValueError("空的base64字符串")
  102. img_bytes = base64.b64decode(b64_image) # 原始二进制
  103. img_type = imghdr.what(None, h=img_bytes)
  104. if not img_type or img_type.lower() not in ["jpeg", "jpg", "png", "webp"]:
  105. raise ValueError(f"不支持的图片格式: {img_type}")
  106. # 直接传原始二进制
  107. image_contents.append({
  108. "image": {"format": img_type.lower(), "source": {"bytes": img_bytes}}
  109. })
  110. img_bytes_list.append(img_bytes)
  111. LOGGER.info(f"{uid} 第{idx}张图处理成功, 格式={img_type}, 大小={len(img_bytes)}B")
  112. except Exception as e:
  113. LOGGER.error(f"{uid} 第{idx}张图处理失败,已跳过: {repr(e)} (长度={len(b64_image) if b64_image else 0})")
  114. img_bytes_list.append(None) # 添加占位符以保持索引一致
  115. if not image_contents:
  116. LOGGER.error(f"{uid}错误: 所有图片均无法处理。")
  117. return {}
  118. category_str = ", ".join([f'"{cat.lower()}"' for cat in categories])
  119. num_images = len(image_contents)
  120. # --- 关键改动:为多图片设计的全新Prompt ---
  121. prompt = f"""
  122. You have been provided with {num_images} images. Analyze each image sequentially.
  123. For each image, detect bounding boxes of objects from the following categories: {category_str}.
  124. Your output MUST be a single, valid JSON object.
  125. The keys of this object should be "image_0", "image_1", ..., "image_{num_images - 1}", corresponding to the first, second, and subsequent images provided.
  126. The value for each key must be a list of detected objects for that specific image. If no objects are detected in an image, the value should be an empty list [].
  127. Use a 1000x1000 coordinate system for the bounding boxes.
  128. Example output format for {num_images} images:
  129. {{
  130. "image_0": [{{"person": [100, 150, 200, 350]}}, {{"car": [400, 500, 600, 700]}}],
  131. "image_1": [],
  132. "image_2": [{{"package": [300, 300, 400, 400]}}]
  133. }}
  134. """
  135. messages = [{"role": "user", "content": image_contents + [{"text": prompt}]}]
  136. try:
  137. response = self.bedrock.converse(
  138. modelId=MODEL_ID,
  139. messages=messages,
  140. inferenceConfig={"temperature": 0.0, "maxTokens": 4096, "topP": 1.0},
  141. )
  142. model_output = response["output"]["message"]["content"][0]["text"]
  143. LOGGER.info(f"\n--- {uid}模型对整个批次的原始输出 ---\n{model_output}")
  144. # 解析模型返回的包含所有图片结果的JSON对象
  145. batch_results = self.safe_json_load(model_output)
  146. if not batch_results or not isinstance(batch_results, dict):
  147. LOGGER.error(f"{uid}模型未返回预期的字典格式结果。")
  148. return {}
  149. # --- 核心逻辑:将批处理结果映射回您的格式 ---
  150. final_output_dict = {}
  151. for i in range(len(base64_images)):
  152. # 从批处理结果中获取当前图片的数据,如果不存在则默认为空列表
  153. nova_detections = batch_results.get(f"image_{i}", [])
  154. # 转换为您最终需要的格式
  155. detailed_results = self.format_and_convert_detections(nova_detections)
  156. final_output_dict[f"file_{i}"] = detailed_results
  157. return final_output_dict
  158. except Exception as e:
  159. LOGGER.error(f"{uid}调用Bedrock模型或处理过程中发生错误: {repr(e)}")
  160. return {}