|
@@ -30,7 +30,10 @@ class OCRLayoutManager:
|
|
|
self._rotated_image_cache = {}
|
|
self._rotated_image_cache = {}
|
|
|
self._cache_max_size = 10
|
|
self._cache_max_size = 10
|
|
|
self._orientation_cache = {} # 缓存方向检测结果
|
|
self._orientation_cache = {} # 缓存方向检测结果
|
|
|
- # self._auto_detected_angle = 0.0 # 自动检测的旋转角度缓存
|
|
|
|
|
|
|
+ self._auto_detected_angle = 0.0 # 自动检测的旋转角度缓存
|
|
|
|
|
+ self.show_all_boxes = False
|
|
|
|
|
+ self.fit_to_container = True
|
|
|
|
|
+ self.zoom_level = 1.0
|
|
|
|
|
|
|
|
def clear_image_cache(self):
|
|
def clear_image_cache(self):
|
|
|
"""清理所有图像缓存"""
|
|
"""清理所有图像缓存"""
|
|
@@ -375,17 +378,40 @@ class OCRLayoutManager:
|
|
|
self.create_aligned_image_display(zoom_level, "compact")
|
|
self.create_aligned_image_display(zoom_level, "compact")
|
|
|
|
|
|
|
|
def create_aligned_image_display(self, zoom_level: float = 1.0, layout_type: str = "aligned"):
|
|
def create_aligned_image_display(self, zoom_level: float = 1.0, layout_type: str = "aligned"):
|
|
|
- """创建与左侧对齐的图片显示 - 修复显示问题"""
|
|
|
|
|
|
|
+ """创建响应式图片显示"""
|
|
|
|
|
+
|
|
|
|
|
+ # # 添加响应式CSS
|
|
|
|
|
+ # st.markdown("""
|
|
|
|
|
+ # <style>
|
|
|
|
|
+ # .responsive-plot-container {
|
|
|
|
|
+ # width: 100%;
|
|
|
|
|
+ # max-width: 100%;
|
|
|
|
|
+ # overflow: hidden;
|
|
|
|
|
+ # }
|
|
|
|
|
+
|
|
|
|
|
+ # .responsive-plot-container .plotly-graph-div {
|
|
|
|
|
+ # width: 100% !important;
|
|
|
|
|
+ # max-width: 100% !important;
|
|
|
|
|
+ # }
|
|
|
|
|
+
|
|
|
|
|
+ # /* 确保Plotly图表响应式 */
|
|
|
|
|
+ # .js-plotly-plot .plotly .svg-container {
|
|
|
|
|
+ # width: 100% !important;
|
|
|
|
|
+ # height: auto !important;
|
|
|
|
|
+ # }
|
|
|
|
|
+ # </style>
|
|
|
|
|
+ # """, unsafe_allow_html=True)
|
|
|
|
|
+
|
|
|
st.header("🖼️ 原图标注")
|
|
st.header("🖼️ 原图标注")
|
|
|
|
|
|
|
|
# 图片控制选项
|
|
# 图片控制选项
|
|
|
col1, col2, col3, col4 = st.columns(4)
|
|
col1, col2, col3, col4 = st.columns(4)
|
|
|
with col1:
|
|
with col1:
|
|
|
# 判断{layout_type}_zoom_level是否有值,如果有值直接使用,否则使用传入的zoom_level
|
|
# 判断{layout_type}_zoom_level是否有值,如果有值直接使用,否则使用传入的zoom_level
|
|
|
- current_zoom = self.validator.zoom_level
|
|
|
|
|
|
|
+ current_zoom = self.zoom_level
|
|
|
current_zoom = st.slider("图片缩放", 0.3, 2.0, current_zoom, 0.1, key=f"{layout_type}_zoom_level")
|
|
current_zoom = st.slider("图片缩放", 0.3, 2.0, current_zoom, 0.1, key=f"{layout_type}_zoom_level")
|
|
|
- if current_zoom != self.validator.zoom_level:
|
|
|
|
|
- self.validator.zoom_level = current_zoom
|
|
|
|
|
|
|
+ if current_zoom != self.zoom_level:
|
|
|
|
|
+ self.zoom_level = current_zoom
|
|
|
with col2:
|
|
with col2:
|
|
|
# 判断{layout_type}_show_all_boxes是否有值,如果有值直接使用,否则默认False
|
|
# 判断{layout_type}_show_all_boxes是否有值,如果有值直接使用,否则默认False
|
|
|
# if f"{layout_type}_show_all_boxes" not in st.session_state:
|
|
# if f"{layout_type}_show_all_boxes" not in st.session_state:
|
|
@@ -394,20 +420,20 @@ class OCRLayoutManager:
|
|
|
show_all_boxes = st.checkbox(
|
|
show_all_boxes = st.checkbox(
|
|
|
"显示所有框",
|
|
"显示所有框",
|
|
|
# value=st.session_state[f"{layout_type}_show_all_boxes"],
|
|
# value=st.session_state[f"{layout_type}_show_all_boxes"],
|
|
|
- value = self.validator.show_all_boxes,
|
|
|
|
|
|
|
+ value = self.show_all_boxes,
|
|
|
key=f"{layout_type}_show_all_boxes"
|
|
key=f"{layout_type}_show_all_boxes"
|
|
|
)
|
|
)
|
|
|
- if show_all_boxes != self.validator.show_all_boxes:
|
|
|
|
|
- self.validator.show_all_boxes = show_all_boxes
|
|
|
|
|
|
|
+ if show_all_boxes != self.show_all_boxes:
|
|
|
|
|
+ self.show_all_boxes = show_all_boxes
|
|
|
with col3:
|
|
with col3:
|
|
|
# 判断{layout_type}_fit_to_container是否有值,如果有值直接使用,否则默认True
|
|
# 判断{layout_type}_fit_to_container是否有值,如果有值直接使用,否则默认True
|
|
|
fit_to_container = st.checkbox(
|
|
fit_to_container = st.checkbox(
|
|
|
"适应容器",
|
|
"适应容器",
|
|
|
- value=self.validator.fit_to_container,
|
|
|
|
|
|
|
+ value=self.fit_to_container,
|
|
|
key=f"{layout_type}_fit_to_container"
|
|
key=f"{layout_type}_fit_to_container"
|
|
|
)
|
|
)
|
|
|
- if fit_to_container != self.validator.fit_to_container:
|
|
|
|
|
- self.validator.fit_to_container = fit_to_container
|
|
|
|
|
|
|
+ if fit_to_container != self.fit_to_container:
|
|
|
|
|
+ self.fit_to_container = fit_to_container
|
|
|
with col4:
|
|
with col4:
|
|
|
# 显示当前角度状态
|
|
# 显示当前角度状态
|
|
|
current_angle = self.get_rotation_angle()
|
|
current_angle = self.get_rotation_angle()
|
|
@@ -426,7 +452,7 @@ class OCRLayoutManager:
|
|
|
# key=f"{layout_type}_manual_angle"
|
|
# key=f"{layout_type}_manual_angle"
|
|
|
)
|
|
)
|
|
|
# if st.button("应用手动角度", key=f"{layout_type}_apply_manual"):
|
|
# if st.button("应用手动角度", key=f"{layout_type}_apply_manual"):
|
|
|
- if not hasattr(self, '_auto_detected_angle') or self._auto_detected_angle != manual_angle:
|
|
|
|
|
|
|
+ if abs(self._auto_detected_angle - manual_angle) > 0.01 :
|
|
|
self._auto_detected_angle = float(manual_angle)
|
|
self._auto_detected_angle = float(manual_angle)
|
|
|
# st.success(f"已设置旋转角度为 {manual_angle}")
|
|
# st.success(f"已设置旋转角度为 {manual_angle}")
|
|
|
# 需要清除图片缓存,以及text_bbox_mapping中的bbox
|
|
# 需要清除图片缓存,以及text_bbox_mapping中的bbox
|
|
@@ -444,8 +470,7 @@ class OCRLayoutManager:
|
|
|
|
|
|
|
|
with col3:
|
|
with col3:
|
|
|
if st.button("🔄 重置角度", key=f"{layout_type}_reset_angle"):
|
|
if st.button("🔄 重置角度", key=f"{layout_type}_reset_angle"):
|
|
|
- if hasattr(self, '_auto_detected_angle'):
|
|
|
|
|
- delattr(self, '_auto_detected_angle')
|
|
|
|
|
|
|
+ self._auto_detected_angle = 0.0
|
|
|
st.success("已重置旋转角度")
|
|
st.success("已重置旋转角度")
|
|
|
# 需要清除图片缓存,以及text_bbox_mapping中的bbox
|
|
# 需要清除图片缓存,以及text_bbox_mapping中的bbox
|
|
|
self.clear_image_cache()
|
|
self.clear_image_cache()
|
|
@@ -503,6 +528,9 @@ class OCRLayoutManager:
|
|
|
|
|
|
|
|
if image:
|
|
if image:
|
|
|
try:
|
|
try:
|
|
|
|
|
+ # 使用响应式容器包装
|
|
|
|
|
+ # st.markdown('<div class="responsive-plot-container">', unsafe_allow_html=True)
|
|
|
|
|
+
|
|
|
# 根据缩放级别调整图片大小
|
|
# 根据缩放级别调整图片大小
|
|
|
new_width = int(image.width * current_zoom)
|
|
new_width = int(image.width * current_zoom)
|
|
|
new_height = int(image.height * current_zoom)
|
|
new_height = int(image.height * current_zoom)
|
|
@@ -532,74 +560,84 @@ class OCRLayoutManager:
|
|
|
'displayModeBar': True,
|
|
'displayModeBar': True,
|
|
|
'modeBarButtonsToRemove': ['zoom2d', 'select2d', 'lasso2d', 'autoScale2d'],
|
|
'modeBarButtonsToRemove': ['zoom2d', 'select2d', 'lasso2d', 'autoScale2d'],
|
|
|
'scrollZoom': True,
|
|
'scrollZoom': True,
|
|
|
- 'doubleClick': 'reset'
|
|
|
|
|
|
|
+ 'doubleClick': 'reset',
|
|
|
|
|
+ 'responsive': False, # 关键:禁用响应式,使用固定尺寸
|
|
|
|
|
+ 'toImageButtonOptions': {
|
|
|
|
|
+ 'format': 'png',
|
|
|
|
|
+ 'filename': 'ocr_image',
|
|
|
|
|
+ 'height': None, # 使用当前高度
|
|
|
|
|
+ 'width': None, # 使用当前宽度
|
|
|
|
|
+ 'scale': 1
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
st.plotly_chart(
|
|
st.plotly_chart(
|
|
|
fig,
|
|
fig,
|
|
|
- use_container_width=fit_to_container,
|
|
|
|
|
|
|
+ # use_container_width=fit_to_container,
|
|
|
|
|
+ use_container_width=False,
|
|
|
config=plot_config,
|
|
config=plot_config,
|
|
|
key=f"{layout_type}_plot"
|
|
key=f"{layout_type}_plot"
|
|
|
)
|
|
)
|
|
|
|
|
+ # st.markdown('</div>', unsafe_allow_html=True)
|
|
|
|
|
|
|
|
- # 显示选中文本的详细信息
|
|
|
|
|
- if st.session_state.selected_text and st.session_state.selected_text in self.validator.text_bbox_mapping:
|
|
|
|
|
- st.subheader("📍 选中文本详情")
|
|
|
|
|
|
|
+ # # 显示选中文本的详细信息
|
|
|
|
|
+ # if st.session_state.selected_text and st.session_state.selected_text in self.validator.text_bbox_mapping:
|
|
|
|
|
+ # st.subheader("📍 选中文本详情")
|
|
|
|
|
|
|
|
- info = self.validator.text_bbox_mapping[st.session_state.selected_text][0]
|
|
|
|
|
- bbox = info['bbox']
|
|
|
|
|
|
|
+ # info = self.validator.text_bbox_mapping[st.session_state.selected_text][0]
|
|
|
|
|
+ # bbox = info['bbox']
|
|
|
|
|
|
|
|
- info_col1, info_col2 = st.columns(2)
|
|
|
|
|
- with info_col1:
|
|
|
|
|
- st.write(f"**文本内容:** {st.session_state.selected_text[:30]}...")
|
|
|
|
|
- st.write(f"**类别:** {info['category']}")
|
|
|
|
|
- # 显示旋转信息
|
|
|
|
|
- rotation_angle = self.get_rotation_angle()
|
|
|
|
|
- if rotation_angle != 0:
|
|
|
|
|
- st.write(f"**旋转角度:** {rotation_angle}°")
|
|
|
|
|
|
|
+ # info_col1, info_col2 = st.columns(2)
|
|
|
|
|
+ # with info_col1:
|
|
|
|
|
+ # st.write(f"**文本内容:** {st.session_state.selected_text[:30]}...")
|
|
|
|
|
+ # st.write(f"**类别:** {info['category']}")
|
|
|
|
|
+ # # 显示旋转信息
|
|
|
|
|
+ # rotation_angle = self.get_rotation_angle()
|
|
|
|
|
+ # if rotation_angle != 0:
|
|
|
|
|
+ # st.write(f"**旋转角度:** {rotation_angle}°")
|
|
|
|
|
|
|
|
- with info_col2:
|
|
|
|
|
- st.write(f"**位置:** [{', '.join(map(str, bbox))}]")
|
|
|
|
|
- if len(bbox) >= 4:
|
|
|
|
|
- st.write(f"**大小:** {bbox[2] - bbox[0]} x {bbox[3] - bbox[1]} px")
|
|
|
|
|
|
|
+ # with info_col2:
|
|
|
|
|
+ # st.write(f"**位置:** [{', '.join(map(str, bbox))}]")
|
|
|
|
|
+ # if len(bbox) >= 4:
|
|
|
|
|
+ # st.write(f"**大小:** {bbox[2] - bbox[0]} x {bbox[3] - bbox[1]} px")
|
|
|
|
|
|
|
|
- # 错误标记功能
|
|
|
|
|
- col1, col2 = st.columns(2)
|
|
|
|
|
- with col1:
|
|
|
|
|
- if st.button("❌ 标记为错误", key=f"{layout_type}_mark_error"):
|
|
|
|
|
- st.session_state.marked_errors.add(st.session_state.selected_text)
|
|
|
|
|
- st.rerun()
|
|
|
|
|
|
|
+ # # 错误标记功能
|
|
|
|
|
+ # col1, col2 = st.columns(2)
|
|
|
|
|
+ # with col1:
|
|
|
|
|
+ # if st.button("❌ 标记为错误", key=f"{layout_type}_mark_error"):
|
|
|
|
|
+ # st.session_state.marked_errors.add(st.session_state.selected_text)
|
|
|
|
|
+ # st.rerun()
|
|
|
|
|
|
|
|
- with col2:
|
|
|
|
|
- if st.button("✅ 取消错误标记", key=f"{layout_type}_unmark_error"):
|
|
|
|
|
- st.session_state.marked_errors.discard(st.session_state.selected_text)
|
|
|
|
|
- st.rerun()
|
|
|
|
|
|
|
+ # with col2:
|
|
|
|
|
+ # if st.button("✅ 取消错误标记", key=f"{layout_type}_unmark_error"):
|
|
|
|
|
+ # st.session_state.marked_errors.discard(st.session_state.selected_text)
|
|
|
|
|
+ # st.rerun()
|
|
|
|
|
|
|
|
- # 增强的调试信息
|
|
|
|
|
- with st.expander("🔍 图像和坐标调试信息", expanded=False):
|
|
|
|
|
- rotation_angle = self.get_rotation_angle()
|
|
|
|
|
- rotation_config = get_ocr_tool_rotation_config(self.validator.ocr_data, self.config)
|
|
|
|
|
|
|
+ # # 增强的调试信息
|
|
|
|
|
+ # with st.expander("🔍 图像和坐标调试信息", expanded=False):
|
|
|
|
|
+ # rotation_angle = self.get_rotation_angle()
|
|
|
|
|
+ # rotation_config = get_ocr_tool_rotation_config(self.validator.ocr_data, self.config)
|
|
|
|
|
|
|
|
- col_debug1, col_debug2, col_debug3 = st.columns(3)
|
|
|
|
|
- with col_debug1:
|
|
|
|
|
- st.write("**图像信息:**")
|
|
|
|
|
- st.write(f"原始尺寸: {image.width} x {image.height}")
|
|
|
|
|
- st.write(f"缩放后尺寸: {resized_image.width} x {resized_image.height}")
|
|
|
|
|
- st.write(f"当前角度: {rotation_angle}°")
|
|
|
|
|
|
|
+ # col_debug1, col_debug2, col_debug3 = st.columns(3)
|
|
|
|
|
+ # with col_debug1:
|
|
|
|
|
+ # st.write("**图像信息:**")
|
|
|
|
|
+ # st.write(f"原始尺寸: {image.width} x {image.height}")
|
|
|
|
|
+ # st.write(f"缩放后尺寸: {resized_image.width} x {resized_image.height}")
|
|
|
|
|
+ # st.write(f"当前角度: {rotation_angle}°")
|
|
|
|
|
|
|
|
- with col_debug2:
|
|
|
|
|
- st.write("**坐标信息:**")
|
|
|
|
|
- if selected_bbox:
|
|
|
|
|
- st.write(f"选中框: {selected_bbox}")
|
|
|
|
|
- st.write(f"总框数: {len(all_boxes)}")
|
|
|
|
|
- st.write(f"文本框数: {len(self.validator.text_bbox_mapping)}")
|
|
|
|
|
|
|
+ # with col_debug2:
|
|
|
|
|
+ # st.write("**坐标信息:**")
|
|
|
|
|
+ # if selected_bbox:
|
|
|
|
|
+ # st.write(f"选中框: {selected_bbox}")
|
|
|
|
|
+ # st.write(f"总框数: {len(all_boxes)}")
|
|
|
|
|
+ # st.write(f"文本框数: {len(self.validator.text_bbox_mapping)}")
|
|
|
|
|
|
|
|
- with col_debug3:
|
|
|
|
|
- st.write("**配置信息:**")
|
|
|
|
|
- st.write(f"工具类型: {rotation_config.get('coordinates_are_pre_rotated', 'unknown')}")
|
|
|
|
|
- st.write(f"缓存状态: {len(self._rotated_image_cache)} 项")
|
|
|
|
|
- if hasattr(self, '_auto_detected_angle'):
|
|
|
|
|
- st.write(f"自动检测角度: {self._auto_detected_angle}°")
|
|
|
|
|
|
|
+ # with col_debug3:
|
|
|
|
|
+ # st.write("**配置信息:**")
|
|
|
|
|
+ # st.write(f"工具类型: {rotation_config.get('coordinates_are_pre_rotated', 'unknown')}")
|
|
|
|
|
+ # st.write(f"缓存状态: {len(self._rotated_image_cache)} 项")
|
|
|
|
|
+ # if hasattr(self, '_auto_detected_angle'):
|
|
|
|
|
+ # st.write(f"自动检测角度: {self._auto_detected_angle}°")
|
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
@@ -610,13 +648,10 @@ class OCRLayoutManager:
|
|
|
if self.validator.image_path:
|
|
if self.validator.image_path:
|
|
|
st.write(f"期望路径: {self.validator.image_path}")
|
|
st.write(f"期望路径: {self.validator.image_path}")
|
|
|
|
|
|
|
|
- st.markdown('</div>', unsafe_allow_html=True)
|
|
|
|
|
|
|
+ # st.markdown('</div>', unsafe_allow_html=True)
|
|
|
|
|
|
|
|
def create_resized_interactive_plot(self, image: Image.Image, selected_bbox: Optional[List[int]], zoom_level: float, all_boxes: list[tuple]) -> go.Figure:
|
|
def create_resized_interactive_plot(self, image: Image.Image, selected_bbox: Optional[List[int]], zoom_level: float, all_boxes: list[tuple]) -> go.Figure:
|
|
|
- """
|
|
|
|
|
- 创建可调整大小的交互式图片 - 修复图像显示和bbox对齐问题
|
|
|
|
|
- 图片,box坐标全部是已缩放,旋转后的坐标
|
|
|
|
|
- """
|
|
|
|
|
|
|
+ """创建可调整大小的交互式图片 - 修复容器溢出问题"""
|
|
|
fig = go.Figure()
|
|
fig = go.Figure()
|
|
|
|
|
|
|
|
# 添加图片 - Plotly坐标系,原点在左下角
|
|
# 添加图片 - Plotly坐标系,原点在左下角
|
|
@@ -674,29 +709,42 @@ class OCRLayoutManager:
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
# 修复:优化显示尺寸计算
|
|
# 修复:优化显示尺寸计算
|
|
|
- max_display_width = 800
|
|
|
|
|
- max_display_height = 600
|
|
|
|
|
|
|
+ max_display_width = 1200
|
|
|
|
|
+ max_display_height = 1200
|
|
|
|
|
|
|
|
# 计算合适的显示尺寸,保持宽高比
|
|
# 计算合适的显示尺寸,保持宽高比
|
|
|
aspect_ratio = image.width / image.height
|
|
aspect_ratio = image.width / image.height
|
|
|
|
|
|
|
|
- if aspect_ratio > 1: # 宽图
|
|
|
|
|
- display_width = min(max_display_width, image.width)
|
|
|
|
|
- display_height = int(display_width / aspect_ratio)
|
|
|
|
|
- else: # 高图
|
|
|
|
|
- display_height = min(max_display_height, image.height)
|
|
|
|
|
- display_width = int(display_height * aspect_ratio)
|
|
|
|
|
|
|
+ if self.fit_to_container:
|
|
|
|
|
+ # 自适应容器模式
|
|
|
|
|
+ if aspect_ratio > 1: # 宽图
|
|
|
|
|
+ display_width = min(max_display_width, image.width)
|
|
|
|
|
+ display_height = int(display_width / aspect_ratio)
|
|
|
|
|
+ else: # 高图
|
|
|
|
|
+ display_height = min(max_display_height, image.height)
|
|
|
|
|
+ display_width = int(display_height * aspect_ratio)
|
|
|
|
|
+
|
|
|
|
|
+ # 确保不会太小
|
|
|
|
|
+ display_width = max(display_width, 400)
|
|
|
|
|
+ display_height = max(display_height, 300)
|
|
|
|
|
+ else:
|
|
|
|
|
+ # 固定尺寸模式,但仍要考虑容器限制
|
|
|
|
|
+ display_width = min(image.width, max_display_width)
|
|
|
|
|
+ display_height = min(image.height, max_display_height)
|
|
|
|
|
|
|
|
- # 修复:设置合理的布局参数
|
|
|
|
|
|
|
+ # 设置布局 - 关键修改
|
|
|
fig.update_layout(
|
|
fig.update_layout(
|
|
|
- width=display_width,
|
|
|
|
|
- height=display_height,
|
|
|
|
|
- margin=dict(l=0, r=0, t=0, b=0), # 移除所有边距
|
|
|
|
|
|
|
+ width=display_width, # 注释掉固定宽度
|
|
|
|
|
+ height=display_height, # 注释掉固定高度
|
|
|
|
|
+
|
|
|
|
|
+ margin=dict(l=0, r=0, t=0, b=0),
|
|
|
showlegend=False,
|
|
showlegend=False,
|
|
|
plot_bgcolor='white',
|
|
plot_bgcolor='white',
|
|
|
dragmode="pan",
|
|
dragmode="pan",
|
|
|
|
|
|
|
|
- # 修复:X轴设置
|
|
|
|
|
|
|
+ # 关键:让图表自适应容器
|
|
|
|
|
+ # autosize=True, # 启用自动调整大小
|
|
|
|
|
+
|
|
|
xaxis=dict(
|
|
xaxis=dict(
|
|
|
visible=False,
|
|
visible=False,
|
|
|
range=[0, image.width],
|
|
range=[0, image.width],
|
|
@@ -704,7 +752,7 @@ class OCRLayoutManager:
|
|
|
fixedrange=False,
|
|
fixedrange=False,
|
|
|
autorange=False,
|
|
autorange=False,
|
|
|
showgrid=False,
|
|
showgrid=False,
|
|
|
- zeroline=False
|
|
|
|
|
|
|
+ zeroline=False,
|
|
|
),
|
|
),
|
|
|
|
|
|
|
|
# 修复:Y轴设置,确保范围正确
|
|
# 修复:Y轴设置,确保范围正确
|