|
|
@@ -30,9 +30,9 @@ class OCRLayoutManager:
|
|
|
self._rotated_image_cache = {}
|
|
|
self._cache_max_size = 10
|
|
|
self._orientation_cache = {} # 缓存方向检测结果
|
|
|
- self._auto_detected_angle = 0.0 # 自动检测的旋转角度缓存
|
|
|
+ self.rotated_angle = 0.0 # 自动检测的旋转角度缓存
|
|
|
self.show_all_boxes = False
|
|
|
- self.fit_to_container = True
|
|
|
+ self.fit_to_container = False
|
|
|
self.zoom_level = 1.0
|
|
|
|
|
|
def clear_image_cache(self):
|
|
|
@@ -81,8 +81,8 @@ class OCRLayoutManager:
|
|
|
return item['rotation_angle']
|
|
|
|
|
|
# 如果没有预设角度,尝试自动检测
|
|
|
- if hasattr(self, '_auto_detected_angle'):
|
|
|
- return self._auto_detected_angle
|
|
|
+ if hasattr(self, 'rotated_angle'):
|
|
|
+ return self.rotated_angle
|
|
|
|
|
|
return 0.0
|
|
|
|
|
|
@@ -342,11 +342,10 @@ class OCRLayoutManager:
|
|
|
zoom_level = layout.get('default_zoom', 1.0) # 默认缩放级别
|
|
|
layout_type = "compact"
|
|
|
|
|
|
- left_col, right_col = st.columns([layout['content_width'], layout['sidebar_width']], vertical_alignment='top')
|
|
|
+ left_col, right_col = st.columns([layout['content_width'], layout['sidebar_width']], vertical_alignment='top', border=True)
|
|
|
|
|
|
with left_col:
|
|
|
- self.render_content_section(layout_type)
|
|
|
-
|
|
|
+ # self.render_content_section(layout_type)
|
|
|
# 快速定位文本选择器(使用不同的key)
|
|
|
if self.validator.text_bbox_mapping:
|
|
|
text_options = ["请选择文本..."] + list(self.validator.text_bbox_mapping.keys())
|
|
|
@@ -354,6 +353,7 @@ class OCRLayoutManager:
|
|
|
"快速定位文本",
|
|
|
range(len(text_options)),
|
|
|
format_func=lambda x: text_options[x][:30] + "..." if len(text_options[x]) > 30 else text_options[x],
|
|
|
+ label_visibility="collapsed",
|
|
|
key="compact_quick_text_selector" # 使用不同的key
|
|
|
)
|
|
|
|
|
|
@@ -380,39 +380,12 @@ class OCRLayoutManager:
|
|
|
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, vertical_alignment="center", border= False)
|
|
|
+
|
|
|
with col1:
|
|
|
- # 判断{layout_type}_zoom_level是否有值,如果有值直接使用,否则使用传入的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")
|
|
|
- if current_zoom != self.zoom_level:
|
|
|
- self.zoom_level = current_zoom
|
|
|
- with col2:
|
|
|
# 判断{layout_type}_show_all_boxes是否有值,如果有值直接使用,否则默认False
|
|
|
# if f"{layout_type}_show_all_boxes" not in st.session_state:
|
|
|
# st.session_state[f"{layout_type}_show_all_boxes"] = False
|
|
|
@@ -425,137 +398,40 @@ class OCRLayoutManager:
|
|
|
)
|
|
|
if show_all_boxes != self.show_all_boxes:
|
|
|
self.show_all_boxes = show_all_boxes
|
|
|
+
|
|
|
+ with col2:
|
|
|
+ # if st.button("应用手动角度", key=f"{layout_type}_apply_manual"):
|
|
|
+ if st.button("🔄 旋转90度", type="secondary", key=f"{layout_type}_manual_angle"):
|
|
|
+ self.rotated_angle = (self.rotated_angle + 90) % 360
|
|
|
+ # st.success(f"已设置旋转角度为 {manual_angle}")
|
|
|
+ # 需要清除图片缓存,以及text_bbox_mapping中的bbox
|
|
|
+ self.clear_image_cache()
|
|
|
+ self.validator.process_data()
|
|
|
+ st.rerun()
|
|
|
+
|
|
|
with col3:
|
|
|
- # 判断{layout_type}_fit_to_container是否有值,如果有值直接使用,否则默认True
|
|
|
- fit_to_container = st.checkbox(
|
|
|
- "适应容器",
|
|
|
- value=self.fit_to_container,
|
|
|
- key=f"{layout_type}_fit_to_container"
|
|
|
- )
|
|
|
- if fit_to_container != self.fit_to_container:
|
|
|
- self.fit_to_container = fit_to_container
|
|
|
+ if st.button("↺ 重置角度", key=f"{layout_type}_reset_angle"):
|
|
|
+ self.rotated_angle = 0.0
|
|
|
+ st.success("已重置旋转角度")
|
|
|
+ # 需要清除图片缓存,以及text_bbox_mapping中的bbox
|
|
|
+ self.clear_image_cache()
|
|
|
+ self.validator.process_data()
|
|
|
+ st.rerun()
|
|
|
+
|
|
|
with col4:
|
|
|
# 显示当前角度状态
|
|
|
current_angle = self.get_rotation_angle()
|
|
|
st.metric("当前角度", f"{current_angle}°", label_visibility="collapsed")
|
|
|
|
|
|
- # 方向检测控制面板
|
|
|
- with st.expander("🔄 图片方向检测", expanded=False):
|
|
|
- col1, col2, col3 = st.columns([1, 1, 1], width='stretch')
|
|
|
-
|
|
|
- with col1:
|
|
|
- manual_angle = st.selectbox(
|
|
|
- "设置角度",
|
|
|
- [0, 90, 180, 270],
|
|
|
- index = 0,
|
|
|
- label_visibility="collapsed",
|
|
|
- # key=f"{layout_type}_manual_angle"
|
|
|
- )
|
|
|
- # if st.button("应用手动角度", key=f"{layout_type}_apply_manual"):
|
|
|
- if abs(self._auto_detected_angle - manual_angle) > 0.01 :
|
|
|
- self._auto_detected_angle = float(manual_angle)
|
|
|
- # st.success(f"已设置旋转角度为 {manual_angle}")
|
|
|
- # 需要清除图片缓存,以及text_bbox_mapping中的bbox
|
|
|
- self.clear_image_cache()
|
|
|
- self.validator.process_data()
|
|
|
- st.rerun()
|
|
|
-
|
|
|
- with col2:
|
|
|
- if st.button("🔍 自动检测方向", key=f"{layout_type}_detect_orientation"):
|
|
|
- if self.validator.image_path:
|
|
|
- with st.spinner("正在检测图片方向..."):
|
|
|
- detection_result = self.detect_and_suggest_rotation(self.validator.image_path)
|
|
|
- st.session_state[f'{layout_type}_detection_result'] = detection_result
|
|
|
- st.rerun()
|
|
|
-
|
|
|
- with col3:
|
|
|
- if st.button("🔄 重置角度", key=f"{layout_type}_reset_angle"):
|
|
|
- self._auto_detected_angle = 0.0
|
|
|
- st.success("已重置旋转角度")
|
|
|
- # 需要清除图片缓存,以及text_bbox_mapping中的bbox
|
|
|
- self.clear_image_cache()
|
|
|
- self.validator.process_data()
|
|
|
- st.rerun()
|
|
|
-
|
|
|
- # 显示检测结果
|
|
|
- if f'{layout_type}_detection_result' in st.session_state:
|
|
|
- result = st.session_state[f'{layout_type}_detection_result']
|
|
|
-
|
|
|
- st.markdown("### 🎯 检测结果")
|
|
|
-
|
|
|
- # 结果概览
|
|
|
- result_col1, result_col2, result_col3 = st.columns(3)
|
|
|
- with result_col1:
|
|
|
- st.metric("建议角度", f"{result['detected_angle']}°")
|
|
|
- with result_col2:
|
|
|
- st.metric("置信度", f"{result['confidence']:.2%}")
|
|
|
- with result_col3:
|
|
|
- confidence_color = "🟢" if result['confidence'] > 0.7 else "🟡" if result['confidence'] > 0.4 else "🔴"
|
|
|
- st.metric("可信度", f"{confidence_color}")
|
|
|
-
|
|
|
- # 详细信息
|
|
|
- st.write(f"**检测信息:** {result['message']}")
|
|
|
-
|
|
|
- if 'method_details' in result:
|
|
|
- st.write("**方法详情:**")
|
|
|
- for detail in result['method_details']:
|
|
|
- st.write(f"• {detail}")
|
|
|
-
|
|
|
- # 应用建议角度
|
|
|
- if result['confidence'] > 0.3 and result['detected_angle'] != 0:
|
|
|
- if st.button(f"✅ 应用建议角度 {result['detected_angle']}°", key=f"{layout_type}_apply_suggested"):
|
|
|
- self._auto_detected_angle = result['detected_angle']
|
|
|
- st.success(f"已应用建议角度 {result['detected_angle']}°")
|
|
|
- # 需要清除图片缓存,以及text_bbox_mapping中的bbox
|
|
|
- self.clear_image_cache()
|
|
|
- self.validator.process_data()
|
|
|
- st.rerun()
|
|
|
-
|
|
|
- # 显示个别方法的结果
|
|
|
- if 'individual_results' in result and len(result['individual_results']) > 1:
|
|
|
- with st.expander("📊 各方法检测详情", expanded=False):
|
|
|
- for i, individual in enumerate(result['individual_results']):
|
|
|
- st.write(f"**方法 {i+1}: {individual['method']}**")
|
|
|
- st.write(f"角度: {individual['detected_angle']}°, 置信度: {individual['confidence']:.2f}")
|
|
|
- st.write(f"信息: {individual['message']}")
|
|
|
- if 'error' in individual:
|
|
|
- st.error(f"错误: {individual['error']}")
|
|
|
- st.write("---")
|
|
|
-
|
|
|
-
|
|
|
# 使用增强的图像加载方法
|
|
|
image = self.load_and_rotate_image(self.validator.image_path)
|
|
|
|
|
|
if image:
|
|
|
try:
|
|
|
- # 使用响应式容器包装
|
|
|
- # st.markdown('<div class="responsive-plot-container">', unsafe_allow_html=True)
|
|
|
-
|
|
|
- # 根据缩放级别调整图片大小
|
|
|
- new_width = int(image.width * current_zoom)
|
|
|
- new_height = int(image.height * current_zoom)
|
|
|
- resized_image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
|
|
-
|
|
|
- # 计算选中的bbox
|
|
|
- selected_bbox = None
|
|
|
- if st.session_state.selected_text and st.session_state.selected_text in self.validator.text_bbox_mapping:
|
|
|
- info = self.validator.text_bbox_mapping[st.session_state.selected_text][0]
|
|
|
- bbox = info['bbox']
|
|
|
- selected_bbox = [int(coord * current_zoom) for coord in bbox]
|
|
|
-
|
|
|
- # 收集所有框
|
|
|
- all_boxes = []
|
|
|
- if show_all_boxes:
|
|
|
- for text, info_list in self.validator.text_bbox_mapping.items():
|
|
|
- for info in info_list:
|
|
|
- bbox = info['bbox']
|
|
|
- if len(bbox) >= 4:
|
|
|
- scaled_bbox = [coord * current_zoom for coord in bbox]
|
|
|
- all_boxes.append(scaled_bbox)
|
|
|
-
|
|
|
+ resized_image, all_boxes, selected_bbox = self.zoom_image(image, self.zoom_level)
|
|
|
# 创建交互式图片
|
|
|
- fig = self.create_resized_interactive_plot(resized_image, selected_bbox, current_zoom, all_boxes)
|
|
|
-
|
|
|
+ fig = self.create_resized_interactive_plot(resized_image, selected_bbox, self.zoom_level, all_boxes)
|
|
|
+
|
|
|
plot_config = {
|
|
|
'displayModeBar': True,
|
|
|
'modeBarButtonsToRemove': ['zoom2d', 'select2d', 'lasso2d', 'autoScale2d'],
|
|
|
@@ -636,8 +512,8 @@ class OCRLayoutManager:
|
|
|
# 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}°")
|
|
|
+ # if hasattr(self, 'rotated_angle'):
|
|
|
+ # st.write(f"自动检测角度: {self.rotated_angle}°")
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
@@ -649,8 +525,34 @@ class OCRLayoutManager:
|
|
|
st.write(f"期望路径: {self.validator.image_path}")
|
|
|
|
|
|
# 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 zoom_image(self, image: Image.Image, current_zoom: float) -> Tuple[Image.Image, List[List[int]], Optional[List[int]]]:
|
|
|
+ """缩放图像"""
|
|
|
+ # 根据缩放级别调整图片大小
|
|
|
+ new_width = int(image.width * current_zoom)
|
|
|
+ new_height = int(image.height * current_zoom)
|
|
|
+ resized_image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
|
|
+
|
|
|
+ # 计算选中的bbox
|
|
|
+ selected_bbox = None
|
|
|
+ if st.session_state.selected_text and st.session_state.selected_text in self.validator.text_bbox_mapping:
|
|
|
+ info = self.validator.text_bbox_mapping[st.session_state.selected_text][0]
|
|
|
+ bbox = info['bbox']
|
|
|
+ selected_bbox = [int(coord * current_zoom) for coord in bbox]
|
|
|
+
|
|
|
+ # 收集所有框
|
|
|
+ all_boxes = []
|
|
|
+ if self.show_all_boxes:
|
|
|
+ for text, info_list in self.validator.text_bbox_mapping.items():
|
|
|
+ for info in info_list:
|
|
|
+ bbox = info['bbox']
|
|
|
+ if len(bbox) >= 4:
|
|
|
+ scaled_bbox = [coord * current_zoom for coord in bbox]
|
|
|
+ all_boxes.append(scaled_bbox)
|
|
|
+
|
|
|
+ return resized_image, all_boxes, selected_bbox
|
|
|
+
|
|
|
+ def create_resized_interactive_plot(self, image: Image.Image, selected_bbox: Optional[List[int]], zoom_level: float, all_boxes: List[List[int]]) -> go.Figure:
|
|
|
"""创建可调整大小的交互式图片 - 修复容器溢出问题"""
|
|
|
fig = go.Figure()
|
|
|
|