Просмотр исходного кода

Merge branch 'feature-update' of slambb/LightGlueProject into master

合并改动
slambb 3 месяцев назад
Родитель
Сommit
f97a801a6f
20 измененных файлов с 1034 добавлено и 89 удалено
  1. 34 7
      LightGlue_Deployment/demo_lightglue_camera_position_async.py
  2. 3 4
      Unity2021.3.42f1-YejiDemo/Assets/BowArrow/Scripts/Entry.cs
  3. 4 4
      Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Prefabs/LightGlueCursorSettings.prefab
  4. 7 3
      Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Prefabs/LightGlueSystem.prefab
  5. 327 5
      Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scenes/LightGlueScene.unity
  6. 55 10
      Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Bridge/HardwareToPythonUdpBridge.cs
  7. 4 4
      Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Config/ImageTransmissionConfig.cs
  8. 3 0
      Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Config/NetworkConfig.cs
  9. 5 0
      Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Config/NetworkConfigManager.cs
  10. 11 8
      Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Demo/HardwareUdpJpegViewer.cs
  11. 4 4
      Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Game/LightGlueCursorSettings.cs
  12. 35 2
      Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Networking/UDPJpegReceiver.cs
  13. 49 10
      Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Python/PythonProcessController.cs
  14. 2 3
      Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/UI/ImageTransmissionUIController.cs
  15. 68 16
      Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/UI/NetworkConfigUIController.cs
  16. 5 5
      Unity2021.3.42f1-YejiDemo/ProjectSettings/ProjectSettings.asset
  17. 4 4
      Unity2021.3.42f1-YejiDemo/ProjectSettings/QualitySettings.asset
  18. 122 0
      docs/Win11游戏全屏时Python后台节流_设置与修改.md
  19. 218 0
      docs/YejiDemo_网络与图传修改说明.md
  20. 74 0
      docs/进入游戏即启动Python_分析.md

+ 34 - 7
LightGlue_Deployment/demo_lightglue_camera_position_async.py

@@ -8,6 +8,35 @@ reduce blocking caused by `cv2.VideoCapture.read()` and keep the downstream
 pipeline busy with the most recent frame available.
 """
 
+# 强制关闭 Windows 11 的后台限制(效率模式 / EcoQoS),避免游戏全屏时 Python 被系统挂起
+import ctypes
+import sys
+
+def _disable_win11_efficiency_mode():
+    if sys.platform != "win32":
+        return
+    try:
+        handle = ctypes.windll.kernel32.GetCurrentProcess()
+        # PROCESS_POWER_THROTTLING_STATE: Version=1, ControlMask=1(Speed), StateMask=0(Disable limit)
+        class ProcessPowerThrottlingState(ctypes.Structure):
+            _fields_ = [
+                ("Version", ctypes.c_ulong),
+                ("ControlMask", ctypes.c_ulong),
+                ("StateMask", ctypes.c_ulong),
+            ]
+        state = ProcessPowerThrottlingState(1, 1, 0)
+        ProcessPowerThrottling = 4  # ProcessPowerThrottling
+        if ctypes.windll.kernel32.SetProcessInformation(
+            handle, ProcessPowerThrottling, ctypes.byref(state), ctypes.sizeof(state)
+        ):
+            print("Win11: 已禁用进程效率模式限制 (EcoQoS)", flush=True)
+        else:
+            print("Win11: 设置效率模式失败 (非致命)", flush=True)
+    except Exception as e:  # pylint: disable=broad-except
+        print(f"Win11 效率模式设置异常: {e}", flush=True)
+
+_disable_win11_efficiency_mode()
+
 import argparse
 import queue
 import threading
@@ -708,7 +737,7 @@ def main():
     
     # 处理UDP模式
     if hasattr(streamer, "is_udp_jpeg") and streamer.is_udp_jpeg:
-        print("UDP JPEG mode: receiver started in background thread")
+        print("UDP JPEG mode: receiver started in background thread", flush=True)
     # 处理摄像头模式
     elif hasattr(streamer, "cap") and streamer.cap is not None:
         is_local_cam = False
@@ -808,8 +837,8 @@ def main():
     else:
         print("[UDP] UDP result sender not available (udp_result_sender.py not found)")
 
-    # Create async visualizer (handles all CPU operations)
-    async_visualizer = AsyncVisualizer(queue_size=2, min_matches=opt.min_matches, result_sender=result_sender) if not opt.no_display else None
+    # Create async visualizer: 无窗口时也需创建以便通过 result_sender 向 Unity 回传结果;仅 no_display 时不再弹窗
+    async_visualizer = AsyncVisualizer(queue_size=2, min_matches=opt.min_matches, result_sender=result_sender) if (not opt.no_display or result_sender is not None) else None
 
     # 可选:启动来自 Unity 的 UDP 控制监听(用于刷新参考图等简单指令)
     control_sock = None
@@ -982,13 +1011,11 @@ def main():
                     fps_display  # Now this is the current frame's FPS
                 )
 
-            # Try to get visualization result (non-blocking, with minimal timeout)
-            # Only check every few frames to reduce overhead
+            # Try to get visualization result (non-blocking); no_display 时仍取结果以消费队列,但不弹窗
             if async_visualizer is not None:
                 viz_result = async_visualizer.get_result(timeout=0.0)  # Non-blocking
-                if viz_result is not None:
+                if viz_result is not None and not opt.no_display:
                     display_frame, reference_view, num_matches, current_H = viz_result
-                    # Only show the reference view window
                     cv2.imshow(window_name_ref, reference_view)
 
             # 处理来自 Unity 的“刷新参考图”控制指令(等价于按键 n)

+ 3 - 4
Unity2021.3.42f1-YejiDemo/Assets/BowArrow/Scripts/Entry.cs

@@ -30,10 +30,9 @@ public class Entry : MonoBehaviour
         Screen.autorotateToPortrait = false;                //不允许自动旋转到纵向
         Screen.autorotateToPortraitUpsideDown = false;      //不允许自动旋转到纵向上下
         Screen.sleepTimeout = SleepTimeout.NeverSleep;      //睡眠时间为从不睡眠
-        //游戏帧率设置,需要在项目设置的Quality中关闭对应画质的垂直同步(VSync选项)
-        // QualitySettings.vSyncCount = 0;
-        // Application.targetFrameRate = 60;
-        QualitySettings.vSyncCount = 1;
+        // 超过 60 帧:关闭垂直同步并取消帧率上限(-1=不限制,可按显示器高刷跑满)
+        QualitySettings.vSyncCount = 0;
+        Application.targetFrameRate = -1;
         // SetTip("Loading", Color.white, 56);
         //SetTip("正在检测对比软件版本", Color.white);
         StartCoroutine(CheckAppVersion());

+ 4 - 4
Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Prefabs/LightGlueCursorSettings.prefab

@@ -1578,7 +1578,7 @@ MonoBehaviour:
   m_Script: {fileID: 11500000, guid: f8f7746c490e08a47830cf327280541b, type: 3}
   m_Name: 
   m_EditorClassIdentifier: 
-  markerEnableSmoothFollow: 1
+  markerEnableSmoothFollow: 0
   markerPositionLerpSpeed: 100
   markerJitterThresholdPixels: 2
   markerSmoothCurve:
@@ -1605,7 +1605,7 @@ MonoBehaviour:
     m_PreInfinity: 2
     m_PostInfinity: 2
     m_RotationOrder: 4
-  cameraAimEnableSmoothRotation: 1
+  cameraAimEnableSmoothRotation: 0
   cameraAimRotationLerpSpeed: 100
   cameraAimRotationJitterThresholdDeg: 0.5
   cameraAimSmoothCurve:
@@ -3214,7 +3214,7 @@ MonoBehaviour:
   onValueChanged:
     m_PersistentCalls:
       m_Calls: []
-  m_IsOn: 1
+  m_IsOn: 0
 --- !u!1 &2383674556729495909
 GameObject:
   m_ObjectHideFlags: 0
@@ -3381,7 +3381,7 @@ MonoBehaviour:
   onValueChanged:
     m_PersistentCalls:
       m_Calls: []
-  m_IsOn: 1
+  m_IsOn: 0
 --- !u!1 &2383674556835811946
 GameObject:
   m_ObjectHideFlags: 0

+ 7 - 3
Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Prefabs/LightGlueSystem.prefab

@@ -63,15 +63,16 @@ MonoBehaviour:
   referenceCaptureWidth: 640
   referenceCaptureHeight: 480
   pythonProcessController: {fileID: 5298912318736373780}
+  startPythonOnFirstImageReceived: 1
   maxQueuedFrames: 2
   enableResultReceiver: 1
   pythonResultPort: 12348
   pythonResultBindIp: 127.0.0.1
   maxResultQueueSize: 10
   transmissionConfig:
-    resolution: 1
-    quality: 60
-    reportIntervalMs: 50
+    resolution: 2
+    quality: 20
+    reportIntervalMs: 10
     enableImageTransmission: 1
 --- !u!1 &5298912318736373781
 GameObject:
@@ -126,8 +127,11 @@ MonoBehaviour:
     0.015 --nms_radius 5 --show_fps
   autoLoadNetworkConfig: 1
   autoStartOnEnable: 1
+  startOnFirstImageReceived: 1
   killProcessTreeOnStop: 1
+  addNoDisplayWhenLaunching: 1
   usePackagedExe: 0
+  readyTimeoutSeconds: 120
 --- !u!1 &5298912319190324695
 GameObject:
   m_ObjectHideFlags: 0

+ 327 - 5
Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scenes/LightGlueScene.unity

@@ -836,6 +836,93 @@ CanvasRenderer:
   m_PrefabAsset: {fileID: 0}
   m_GameObject: {fileID: 102823388}
   m_CullTransparentMesh: 1
+--- !u!1 &120360568
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 120360569}
+  - component: {fileID: 120360570}
+  m_Layer: 5
+  m_Name: NoDisplayToggle
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &120360569
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 120360568}
+  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 1167236134}
+  - {fileID: 703311651}
+  m_Father: {fileID: 344605506}
+  m_RootOrder: 2
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0, y: 1}
+  m_AnchorMax: {x: 0, y: 1}
+  m_AnchoredPosition: {x: 314.17, y: -495.0145}
+  m_SizeDelta: {x: 628.34, y: 60}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &120360570
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 120360568}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 9085046f02f69544eb97fd06b6048fe2, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Navigation:
+    m_Mode: 3
+    m_WrapAround: 0
+    m_SelectOnUp: {fileID: 0}
+    m_SelectOnDown: {fileID: 0}
+    m_SelectOnLeft: {fileID: 0}
+    m_SelectOnRight: {fileID: 0}
+  m_Transition: 1
+  m_Colors:
+    m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
+    m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+    m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+    m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+    m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
+    m_ColorMultiplier: 1
+    m_FadeDuration: 0.1
+  m_SpriteState:
+    m_HighlightedSprite: {fileID: 0}
+    m_PressedSprite: {fileID: 0}
+    m_SelectedSprite: {fileID: 0}
+    m_DisabledSprite: {fileID: 0}
+  m_AnimationTriggers:
+    m_NormalTrigger: Normal
+    m_HighlightedTrigger: Highlighted
+    m_PressedTrigger: Pressed
+    m_SelectedTrigger: Selected
+    m_DisabledTrigger: Disabled
+  m_Interactable: 1
+  m_TargetGraphic: {fileID: 1167236135}
+  toggleTransition: 1
+  graphic: {fileID: 1714561901}
+  m_Group: {fileID: 0}
+  onValueChanged:
+    m_PersistentCalls:
+      m_Calls: []
+  m_IsOn: 0
 --- !u!1 &136599810
 GameObject:
   m_ObjectHideFlags: 0
@@ -2131,6 +2218,7 @@ RectTransform:
   m_Children:
   - {fileID: 1942171397}
   - {fileID: 860056050}
+  - {fileID: 120360569}
   m_Father: {fileID: 1644453809}
   m_RootOrder: 1
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
@@ -3661,9 +3749,9 @@ MonoBehaviour:
   applyConfigButton: {fileID: 1797064460}
   resetConfigButton: {fileID: 488035347}
   config:
-    resolution: 1
+    resolution: 2
     quality: 20
-    reportIntervalMs: 20
+    reportIntervalMs: 10
     enableImageTransmission: 1
   loadFromLocalOnStart: 1
   bridge: {fileID: 0}
@@ -4256,6 +4344,86 @@ CanvasRenderer:
   m_PrefabAsset: {fileID: 0}
   m_GameObject: {fileID: 702530509}
   m_CullTransparentMesh: 1
+--- !u!1 &703311650
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 703311651}
+  - component: {fileID: 703311653}
+  - component: {fileID: 703311652}
+  m_Layer: 5
+  m_Name: Label
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &703311651
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 703311650}
+  m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 120360569}
+  m_RootOrder: 1
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0, y: 0}
+  m_AnchorMax: {x: 1, y: 1}
+  m_AnchoredPosition: {x: 39, y: -0.5}
+  m_SizeDelta: {x: -88, y: -3}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &703311652
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 703311650}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 1, b: 1, a: 1}
+  m_RaycastTarget: 1
+  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+  m_Maskable: 1
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+  m_FontData:
+    m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
+    m_FontSize: 35
+    m_FontStyle: 1
+    m_BestFit: 0
+    m_MinSize: 2
+    m_MaxSize: 40
+    m_Alignment: 3
+    m_AlignByGeometry: 0
+    m_RichText: 1
+    m_HorizontalOverflow: 0
+    m_VerticalOverflow: 0
+    m_LineSpacing: 1
+  m_Text: "\u662F\u5426\u5728\u542F\u52A8\u65F6\uFF1A\u5173\u95EDPython\u7A97\u53E3"
+--- !u!222 &703311653
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 703311650}
+  m_CullTransparentMesh: 1
 --- !u!1 &704632988
 GameObject:
   m_ObjectHideFlags: 0
@@ -4931,7 +5099,7 @@ RectTransform:
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_AnchorMin: {x: 0, y: 1}
   m_AnchorMax: {x: 0, y: 1}
-  m_AnchoredPosition: {x: 314.17, y: -495.0145}
+  m_AnchoredPosition: {x: 314.17, y: -470.029}
   m_SizeDelta: {x: 628.34, y: 60}
   m_Pivot: {x: 0.5, y: 0.5}
 --- !u!114 &860056051
@@ -4981,7 +5149,7 @@ MonoBehaviour:
   onValueChanged:
     m_PersistentCalls:
       m_Calls: []
-  m_IsOn: 0
+  m_IsOn: 1
 --- !u!1 &861774504
 GameObject:
   m_ObjectHideFlags: 0
@@ -7023,6 +7191,83 @@ MonoBehaviour:
   m_EditorClassIdentifier: 
   m_Padding: {x: -8, y: -5, z: -8, w: -5}
   m_Softness: {x: 0, y: 0}
+--- !u!1 &1167236133
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 1167236134}
+  - component: {fileID: 1167236136}
+  - component: {fileID: 1167236135}
+  m_Layer: 5
+  m_Name: Background
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &1167236134
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1167236133}
+  m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 1714561900}
+  m_Father: {fileID: 120360569}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0, y: 1}
+  m_AnchorMax: {x: 0, y: 1}
+  m_AnchoredPosition: {x: 30, y: -30}
+  m_SizeDelta: {x: 60, y: 60}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &1167236135
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1167236133}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 1, b: 1, a: 1}
+  m_RaycastTarget: 1
+  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+  m_Maskable: 1
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+  m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
+  m_Type: 1
+  m_PreserveAspect: 0
+  m_FillCenter: 1
+  m_FillMethod: 4
+  m_FillAmount: 1
+  m_FillClockwise: 1
+  m_FillOrigin: 0
+  m_UseSpriteMesh: 0
+  m_PixelsPerUnitMultiplier: 1
+--- !u!222 &1167236136
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1167236133}
+  m_CullTransparentMesh: 1
 --- !u!1 &1169405981
 GameObject:
   m_ObjectHideFlags: 0
@@ -8689,6 +8934,7 @@ MonoBehaviour:
   loadConfigButton: {fileID: 136599812}
   applyConfigButton: {fileID: 1777617641}
   autoStartToggle: {fileID: 860056051}
+  noDisplayToggle: {fileID: 120360570}
   configPanelRoot: {fileID: 152512333}
   bridge: {fileID: 0}
   sdkBridge: {fileID: 0}
@@ -9643,6 +9889,82 @@ MonoBehaviour:
     m_VerticalOverflow: 0
     m_LineSpacing: 1
   m_Text: New Text
+--- !u!1 &1714561899
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 1714561900}
+  - component: {fileID: 1714561902}
+  - component: {fileID: 1714561901}
+  m_Layer: 5
+  m_Name: Checkmark
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &1714561900
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1714561899}
+  m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 1167236134}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0.5, y: 0.5}
+  m_AnchorMax: {x: 0.5, y: 0.5}
+  m_AnchoredPosition: {x: 0, y: 0}
+  m_SizeDelta: {x: 60, y: 60}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &1714561901
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1714561899}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 1, b: 1, a: 1}
+  m_RaycastTarget: 1
+  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+  m_Maskable: 1
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+  m_Sprite: {fileID: 10901, guid: 0000000000000000f000000000000000, type: 0}
+  m_Type: 0
+  m_PreserveAspect: 0
+  m_FillCenter: 1
+  m_FillMethod: 4
+  m_FillAmount: 1
+  m_FillClockwise: 1
+  m_FillOrigin: 0
+  m_UseSpriteMesh: 0
+  m_PixelsPerUnitMultiplier: 1
+--- !u!222 &1714561902
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1714561899}
+  m_CullTransparentMesh: 1
 --- !u!1 &1717220473
 GameObject:
   m_ObjectHideFlags: 0
@@ -11237,7 +11559,7 @@ MonoBehaviour:
     m_HorizontalOverflow: 0
     m_VerticalOverflow: 0
     m_LineSpacing: 1
-  m_Text: "\u662F\u5426\u81EA\u52A8\u8FD0\u884C\uFF0C\u8DF3\u8FC7\u8BBE\u7F6E\u53C2\u6570\u754C\u9762"
+  m_Text: "\u662F\u5426\u81EA\u52A8\u8FD0\u884C\u65F6\uFF1A\u8DF3\u8FC7\u8BBE\u7F6E\u53C2\u6570\u754C\u9762"
 --- !u!222 &1874801697
 CanvasRenderer:
   m_ObjectHideFlags: 0

+ 55 - 10
Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Bridge/HardwareToPythonUdpBridge.cs

@@ -87,6 +87,8 @@ namespace LightGlue.Unity.Bridge
         [Header("Python Process (stdin mode: Unity -> Python)")]
         [Tooltip("Python进程控制器(用于stdin模式,直接传递图片数据)")]
         public PythonProcessController pythonProcessController;
+        [Tooltip("为 true 时:收到首帧图像后再启动 Python(需 PythonProcessController.startOnFirstImageReceived 同时为 true),避免无图时启动算法。")]
+        public bool startPythonOnFirstImageReceived = true;
 
         [Tooltip("Max queued frames in receiver (latest N only).")]
         public int maxQueuedFrames = 2;
@@ -152,6 +154,7 @@ namespace LightGlue.Unity.Bridge
 
         private bool _autoHardwareConfigApplied;
         private bool _autoHardwareConfigReady;
+        private bool _pythonStartedByFirstFrame;
         
         /// <summary>
         /// 获取最新的JPEG图片数据(供Viewer等组件使用)
@@ -254,10 +257,17 @@ namespace LightGlue.Unity.Bridge
 
         private void OnEnable()
         {
+            // 游戏全屏时保持收发 UDP/进程通信,避免因失去焦点被系统节流导致 Python 端卡顿
+            Application.runInBackground = true;
             _lastReferenceRefreshTime = Time.time;
             _autoHardwareConfigApplied = false;
             _autoHardwareConfigReady = false;
             _autoHardwareConfigApplied = false;
+            // 创建 receiver 前先加载网络配置,保证首次运行与“应用配置重启”使用同一绑定地址,避免预览在重启后因 IP 不一致断流
+            if (autoLoadNetworkConfig)
+            {
+                LoadNetworkConfig();
+            }
             // Hardware -> Unity receiver(端口占用时捕获异常,避免崩溃)
             _receiver = new UDPJpegReceiver(hardwareBindIp, hardwarePort, hardwareTimeoutSeconds, maxQueuedFrames);
             try
@@ -396,6 +406,24 @@ namespace LightGlue.Unity.Bridge
 
             try { _udpSender?.Close(); } catch { /* ignore */ }
             _udpSender = null;
+
+            _pythonStartedByFirstFrame = false;
+        }
+
+        /// <summary>
+        /// 收到首帧图像时调用:若启用「首帧后再启动 Python」则启动一次,之后不再重复。
+        /// </summary>
+        private void TryStartPythonOnFirstFrame()
+        {
+            if (_pythonStartedByFirstFrame) return;
+            if (!startPythonOnFirstImageReceived || pythonProcessController == null || pythonProcessController.IsRunning)
+                return;
+            if (pythonProcessController.startOnFirstImageReceived)
+            {
+                pythonProcessController.StartPython();
+                _pythonStartedByFirstFrame = true;
+                Debug.Log("[Bridge] 已收到首帧图像,启动 Python 进程。");
+            }
         }
 
         // ---------- 硬件控制:下发 0x40 参数设置(供 ImageTransmissionUIController 等调用) ----------
@@ -537,9 +565,14 @@ namespace LightGlue.Unity.Bridge
 
             if (!shouldTransmit)
             {
-                bool gotFrame = false;
-                while (_receiver.TryDequeueJpeg(out _)) { gotFrame = true; }
-                if (gotFrame) TryAutoApplyHardwareConfigOnce();
+                byte[] last = null;
+                while (_receiver.TryDequeueJpeg(out var j)) { last = j; }
+                if (last != null)
+                {
+                    lock (_latestJpegLock) { _latestJpeg = last; }
+                    TryStartPythonOnFirstFrame();
+                    TryAutoApplyHardwareConfigOnce();
+                }
                 return;
             }
 
@@ -550,9 +583,14 @@ namespace LightGlue.Unity.Bridge
                 float intervalMs = transmissionConfig.reportIntervalMs;
                 if (intervalMs > 0 && (currentTime - _lastSendTime) < intervalMs)
                 {
-                    bool gotFrame = false;
-                    while (_receiver.TryDequeueJpeg(out _)) { gotFrame = true; }
-                    if (gotFrame) TryAutoApplyHardwareConfigOnce();
+                    byte[] last = null;
+                    while (_receiver.TryDequeueJpeg(out var j)) { last = j; }
+                    if (last != null)
+                    {
+                        lock (_latestJpegLock) { _latestJpeg = last; }
+                        TryStartPythonOnFirstFrame();
+                        TryAutoApplyHardwareConfigOnce();
+                    }
                     return;
                 }
             }
@@ -569,6 +607,7 @@ namespace LightGlue.Unity.Bridge
                 while (_receiver.TryDequeueJpeg(out var jpeg))
                     latest = jpeg;
                 if (latest == null) return;
+                TryStartPythonOnFirstFrame();
                 TryAutoApplyHardwareConfigOnce();
                 toSend = latest;
                 lock (_latestJpegLock)
@@ -579,21 +618,23 @@ namespace LightGlue.Unity.Bridge
 
             byte[] processedImage = ProcessImage(toSend);
 
+            // 仅在 Python 已就绪接收时发送,避免 TensorRT 编译期间发图导致 UDP 缓冲堆积、后续解码损坏
+            if (pythonProcessController != null && !pythonProcessController.IsReadyToReceiveFrames)
+                return;
+
             try
             {
                 if (transferMode == TransferMode.Stdin)
                 {
-                    // Stdin模式:直接写入二进制数据
                     if (_stdinWriter != null)
                     {
                         _stdinWriter.BaseStream.Write(processedImage, 0, processedImage.Length);
-                        _stdinWriter.BaseStream.Flush();  // 立即刷新,减少延迟
+                        _stdinWriter.BaseStream.Flush();
                         _lastSendTime = currentTime;
                     }
                 }
                 else
                 {
-                    // UDP模式(传统)
                     _udpSender.Send(processedImage, processedImage.Length, _pythonEndpoint);
                     _lastSendTime = currentTime;
                 }
@@ -605,7 +646,6 @@ namespace LightGlue.Unity.Bridge
             catch (IOException ex)
             {
                 Debug.LogWarning($"[Bridge] Stdin write error: {ex.Message}");
-                // Stdin写入失败,可能是Python进程已退出
                 _stdinWriter = null;
             }
         }
@@ -663,6 +703,11 @@ namespace LightGlue.Unity.Bridge
             transmissionConfig = config;
             _configEnabled = config != null && config.enableImageTransmission;
             Debug.Log($"[Bridge] 传输配置已更新: 分辨率={config?.GetResolutionString()}, 质量={config?.quality}, 间隔={config?.reportIntervalMs}ms, 启用={_configEnabled}");
+            // 配置一旦设置就立即下发 0x40,避免硬件用默认参数发图导致首段画面不完整、闪烁,直到用户手动点「应用」才正常
+            if (config != null && _hwControlClient != null && _hwControlEndpoint != null)
+            {
+                ApplyConfig(config);
+            }
         }
 
         private void SendControlCommand(char cmd)

+ 4 - 4
Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Config/ImageTransmissionConfig.cs

@@ -14,14 +14,14 @@ namespace LightGlue.Unity.Config
         [Tooltip("图片分辨率选择(硬件分辨率枚举:QQVGA/QVGA/VGA/SVGA/XGA/HD/SXGA/UXGA)")]
         public ImageResolution resolution = ImageResolution.HD_1280x720;
 
-        [Tooltip("图片质量 (0-63,硬件直接使用该值,十进制输入,内部按十六进制发送)")]
-        [Range(0, 63)]
-        public int quality = 32;
+        [Tooltip("图片质量 (0-60,硬件直接使用该值,十进制输入,内部按十六进制发送)")]
+        [Range(0, 60)]
+        public int quality = 20;
 
         [Header("传输参数")]
         [Tooltip("上报时间间隔 (毫秒)")]
         [Min(0)]
-        public int reportIntervalMs = 1000;
+        public int reportIntervalMs = 10;
 
         [Tooltip("开启图片传输开关")]
         public bool enableImageTransmission = true;

+ 3 - 0
Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Config/NetworkConfig.cs

@@ -57,6 +57,9 @@ namespace LightGlue.Unity.Config
         [Tooltip("是否在Play时自动启动所有相关组件(如果为false,需要手动启动)")]
         public bool autoStartOnPlay = false;
 
+        [Tooltip("是否以无窗口方式启动 Python(--no_display),避免绘制窗口在游戏背后时卡顿;取消勾选可保留 Python 窗口便于调试")]
+        public bool pythonNoDisplay = true;
+
         /// <summary>
         /// 验证IP地址格式
         /// </summary>

+ 5 - 0
Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Config/NetworkConfigManager.cs

@@ -44,6 +44,11 @@ namespace LightGlue.Unity.Config
                 string json = File.ReadAllText(configPath);
                 _cachedConfig = JsonUtility.FromJson<NetworkConfig>(json);
 
+                if (_cachedConfig != null && !json.Contains("pythonNoDisplay"))
+                {
+                    _cachedConfig.pythonNoDisplay = true;
+                }
+
                 if (_cachedConfig == null)
                 {
                     Debug.LogWarning($"[NetworkConfigManager] 配置文件解析失败,使用默认配置");

+ 11 - 8
Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Demo/HardwareUdpJpegViewer.cs

@@ -47,17 +47,18 @@ namespace LightGlue.Unity.Demo
             // Shared 模式下不管理 Bridge 内部的接收器,这里无需额外清理
         }
 
+        private float _lastDecodeFailLogTime = -1f;
+
         private void Update()
         {
             byte[] latest = null;
 
-            // 共享模式:从Bridge获取最新图片
             if (bridge == null) return;
-            
-            // 从Bridge获取最新缓存的图片
             latest = bridge.GetLatestJpeg();
-
-            if (latest == null) return;
+            if (latest == null || latest.Length < 4) return;
+            // 只尝试解码形似完整 JPEG 的缓冲(SOI+EOI),避免对 UDP 组帧残片反复解码并刷日志
+            if (latest[0] != 0xFF || latest[1] != 0xD8 || latest[latest.Length - 2] != 0xFF || latest[latest.Length - 1] != 0xD9)
+                return;
 
             if (target == null) return;
 
@@ -70,12 +71,14 @@ namespace LightGlue.Unity.Demo
                 target.texture = _tex;
             }
 
-            // Decode on main thread (Unity API)
             bool ok = _tex.LoadImage(latest, markNonReadable: false);
             if (!ok)
             {
-                Debug.LogWarning($"[Viewer] Failed to decode JPEG (size: {latest.Length} bytes)");
-                return;
+                if (Time.unscaledTime - _lastDecodeFailLogTime >= 1f)
+                {
+                    _lastDecodeFailLogTime = Time.unscaledTime;
+                    Debug.LogWarning($"[Viewer] Failed to decode JPEG (size: {latest.Length} bytes)");
+                }
             }
         }
     }

+ 4 - 4
Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Game/LightGlueCursorSettings.cs

@@ -21,7 +21,7 @@ namespace LightGlue.Unity.Game
 
         [Header("标记器跟随(SimpleCameraPositionMarker)")]
         [Tooltip("是否对 UI 标记位置做平滑插值")]
-        public bool markerEnableSmoothFollow = true;
+        public bool markerEnableSmoothFollow = false;
 
         [Tooltip("位置平滑插值速度,值越大响应越快")]
         public float markerPositionLerpSpeed = 100f;
@@ -36,7 +36,7 @@ namespace LightGlue.Unity.Game
 
         [Header("相机朝向跟随(LightGlueCameraAimController)")]
         [Tooltip("是否对 CameraToLook 的旋转做平滑插值")]
-        public bool cameraAimEnableSmoothRotation = true;
+        public bool cameraAimEnableSmoothRotation = false;
 
         [Tooltip("旋转平滑插值速度,值越大响应越快")]
         public float cameraAimRotationLerpSpeed = 100f;
@@ -120,10 +120,10 @@ namespace LightGlue.Unity.Game
         /// </summary>
         public void ResetToDefaultAndSave()
         {
-            markerEnableSmoothFollow = true;
+            markerEnableSmoothFollow = false;
             markerPositionLerpSpeed = 100f;
             markerJitterThresholdPixels = 2f;
-            cameraAimEnableSmoothRotation = true;
+            cameraAimEnableSmoothRotation = false;
             cameraAimRotationLerpSpeed = 100f;
             cameraAimRotationJitterThresholdDeg = 0.5f;
             SaveToLocal();

+ 35 - 2
Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Networking/UDPJpegReceiver.cs

@@ -10,6 +10,7 @@ namespace LightGlue.Unity.Networking
     /// <summary>
     /// UDP JPEG receiver (transparent mode).
     /// - No protocol header; reassembles a JPEG by searching for SOI (FFD8) and EOI (FFD9).
+    /// - Ignores false SOI: in JPEG scan data 0xFF is byte-stuffed as 0xFF 0x00, so 0xFF 0xD8 after 0xFF 0x00 is not a real SOI.
     /// - Receives on a background thread; enqueues complete JPEG byte[] for main-thread decoding.
     /// </summary>
     public sealed class UDPJpegReceiver : IDisposable
@@ -137,7 +138,7 @@ namespace LightGlue.Unity.Networking
 
         private void ProcessData(byte[] data)
         {
-            int startIdx = IndexOf(data, JpegStart, 0);
+            int startIdx = IndexOfRealJpegSoi(data, 0, data.Length);
             if (startIdx >= 0 && !_receiving)
             {
                 _receiving = true;
@@ -164,7 +165,39 @@ namespace LightGlue.Unity.Networking
                 EnqueueLatest(jpeg);
             }
 
-            ResetState();
+            // 检查剩余缓冲区是否包含下一帧 SOI,避免丢弃下一帧开头导致后续损坏
+            int remainCount = _bufferLen - jpegLen;
+            int nextSoi = remainCount >= 2 ? IndexOfRealJpegSoi(_buffer, jpegLen, remainCount) : -1;
+            if (nextSoi >= jpegLen)
+            {
+                int remain = _bufferLen - nextSoi;
+                Buffer.BlockCopy(_buffer, nextSoi, _buffer, 0, remain);
+                _bufferLen = remain;
+                _startTimeUtc = DateTime.UtcNow;
+            }
+            else
+            {
+                ResetState();
+            }
+        }
+
+        /// <summary>
+        /// 查找“真实”的 JPEG SOI (FF D8)。JPEG 熵编码中 0xFF 会被填充为 0xFF 0x00,
+        /// 故 0xFF 0x00 0xD8 中的 FFD8 是假 SOI,应跳过继续找下一个。
+        /// </summary>
+        private static int IndexOfRealJpegSoi(byte[] data, int startIndex, int count)
+        {
+            if (data == null || count < 2 || startIndex + count > data.Length)
+                return -1;
+            int limit = startIndex + count - 2;
+            for (int i = startIndex; i <= limit; i++)
+            {
+                if (data[i] != 0xFF || data[i + 1] != 0xD8) continue;
+                if (i >= 2 && data[i - 2] == 0xFF && data[i - 1] == 0x00)
+                    continue;
+                return i;
+            }
+            return -1;
         }
 
         private void CheckTimeout()

+ 49 - 10
Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Python/PythonProcessController.cs

@@ -32,10 +32,16 @@ namespace LightGlue.Unity.Python
         public bool autoLoadNetworkConfig = true;
 
         [Header("Lifecycle")]
-        [Tooltip("是否在OnEnable时自动启动Python进程(推荐启用)")]
-        public bool autoStartOnEnable = true;  // 默认启用,方便直接运行
+        [Tooltip("是否在OnEnable时自动启动Python进程。若启用下面的「首帧后再启动」,则 OnEnable 时不启动,由 Bridge 在收到首帧图像后调用 StartPython()。")]
+        public bool autoStartOnEnable = true;
+        [Tooltip("为 true 时:不在一启动就拉 Python,等收到首帧图像后再启动(首次启动或应用配置重启后均生效),避免无图时白跑 TensorRT 等。")]
+        public bool startOnFirstImageReceived = true;
         public bool killProcessTreeOnStop = true;
 
+        [Header("无窗口运行(避免后台卡顿)")]
+        [Tooltip("勾选后自动在启动参数中加上 --no_display,Python 不创建 OpenCV 窗口;结果仍通过 UDP 回 Unity。可避免「窗口在游戏背后时被系统节流导致卡顿」。取消勾选可保留 Python 绘制窗口便于调试。")]
+        public bool addNoDisplayWhenLaunching = true;
+
         [Header("打包模式")]
         [Tooltip("勾选后在打包客户端中自动查找同级目录下的 LightGlue_Deployment/lightglue_runtime.exe," +
                  "无需在 Inspector 中手动配置 pythonExe / workingDirectory / scriptPath。开发阶段建议不勾选,使用本机 Python + .py 脚本。")]
@@ -43,9 +49,19 @@ namespace LightGlue.Unity.Python
 
         private Process _proc;
         private StreamWriter _stdinWriter;
+        private volatile bool _readyToReceiveFrames;
+        private float _processStartTime = -1f;
+
+        [Tooltip("若在此秒数内未收到 Python 就绪输出,则视为就绪并开始发图(防止 stdout 缓冲导致窗口一直不弹出)")]
+        public float readyTimeoutSeconds = 120f;
 
         public bool IsRunning => _proc != null && !_proc.HasExited;
-        
+
+        /// <summary>
+        /// Python 是否已进入接收循环(收到脚本 stdout 中的就绪标记后才为 true,避免 TensorRT 编译期间发图导致缓冲堆积、后续解码损坏)。
+        /// </summary>
+        public bool IsReadyToReceiveFrames => _readyToReceiveFrames;
+
         /// <summary>
         /// 获取Python进程的stdin写入流(用于直接发送图片数据)
         /// </summary>
@@ -87,7 +103,8 @@ namespace LightGlue.Unity.Python
 
         private void OnEnable()
         {
-            if (autoStartOnEnable) StartPython();
+            if (autoStartOnEnable && !startOnFirstImageReceived)
+                StartPython();
         }
 
         private void OnDisable()
@@ -95,6 +112,16 @@ namespace LightGlue.Unity.Python
             StopPython();
         }
 
+        private void Update()
+        {
+            if (IsRunning && !_readyToReceiveFrames && _processStartTime >= 0f && readyTimeoutSeconds > 0f
+                && (UnityEngine.Time.time - _processStartTime) >= readyTimeoutSeconds)
+            {
+                _readyToReceiveFrames = true;
+                Debug.Log("[Python] 未检测到就绪输出,超时后视为就绪并开始发送。");
+            }
+        }
+
         public void StartPython()
         {
             if (IsRunning) return;
@@ -162,15 +189,24 @@ namespace LightGlue.Unity.Python
                 RedirectStandardOutput = true,
                 RedirectStandardError = true,
                 RedirectStandardInput = true,  // 启用stdin重定向
-                // 打包模式:直接执行独立 exe,仅传入 scriptArgs
-                // 开发模式:python + 脚本.py + 参数
-                Arguments = usePackagedExe
-                    ? (scriptArgs ?? string.Empty).Trim()
-                    : $"{Quote(script)} {scriptArgs}".Trim()
             };
+            string args = (scriptArgs ?? string.Empty).Trim();
+            if (addNoDisplayWhenLaunching && !args.Contains("--no_display"))
+                args = args + " --no_display";
+            psi.Arguments = usePackagedExe ? args : $"{Quote(script)} {args}";
 
             _proc = new Process { StartInfo = psi, EnableRaisingEvents = true };
-            _proc.OutputDataReceived += (_, e) => { if (!string.IsNullOrEmpty(e.Data)) Debug.Log($"[Python] {e.Data}"); };
+            _readyToReceiveFrames = false;
+            _proc.OutputDataReceived += (_, e) =>
+            {
+                if (string.IsNullOrEmpty(e.Data)) return;
+                if (!_readyToReceiveFrames && (e.Data.Contains("UDP JPEG mode: receiver started") || e.Data.Contains("First frame received and processed")))
+                {
+                    _readyToReceiveFrames = true;
+                    Debug.Log("[Python] 已就绪接收图像,开始向 Python 发送帧。");
+                }
+                Debug.Log($"[Python] {e.Data}");
+            };
             _proc.ErrorDataReceived += (_, e) =>
             {
                 if (string.IsNullOrEmpty(e.Data)) return;
@@ -205,6 +241,7 @@ namespace LightGlue.Unity.Python
                 _stdinWriter = _proc.StandardInput;
                 _stdinWriter.AutoFlush = true;  // 自动刷新,减少延迟
                 
+                _processStartTime = UnityEngine.Time.time;
                 _proc.BeginOutputReadLine();
                 _proc.BeginErrorReadLine();
                 Debug.Log($"[Python] Started successfully! pid={_proc.Id}\n" +
@@ -241,6 +278,8 @@ namespace LightGlue.Unity.Python
             }
             finally
             {
+                _readyToReceiveFrames = false;
+                _processStartTime = -1f;
                 try 
                 { 
                     _stdinWriter?.Close(); 

+ 2 - 3
Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/UI/ImageTransmissionUIController.cs

@@ -87,12 +87,11 @@ namespace LightGlue.Unity.UI
             InitializeUI();
             SetupEventListeners();
             
-            // 初始化时同步配置到 Python Bridge(保证 Python 链路与当前 UI 状态一致
+            // 初始化时同步配置到 Bridge(SetTransmissionConfig 内部会立即下发 0x40,保证硬件从第一帧起就用正确参数
             if (bridge != null)
             {
                 bridge.SetTransmissionConfig(config);
-                // 通知 Bridge:此时图像传输配置已就绪,后续收到硬件首帧时允许执行“一次性自动下发 0x40”
-                //bridge.MarkHardwareAutoApplyReady();
+                bridge.MarkHardwareAutoApplyReady();
             }
 
             // 同步到新 SDK Bridge,便于对比测试

+ 68 - 16
Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/UI/NetworkConfigUIController.cs

@@ -50,6 +50,9 @@ namespace LightGlue.Unity.UI
         [Tooltip("自动启动Toggle(选中后下次Play时自动启动所有组件)")]
         public Toggle autoStartToggle;
 
+        [Tooltip("无窗口运行Toggle(选中时 Python 加 --no_display,不弹绘制窗口,避免在游戏背后卡顿)")]
+        public Toggle noDisplayToggle;
+
         [Header("UI面板控制")]
         [Tooltip("网络配置面板的根节点GameObject(用于显示/隐藏整个面板)")]
         public GameObject configPanelRoot;
@@ -146,6 +149,12 @@ namespace LightGlue.Unity.UI
                 autoStartToggle.isOn = _currentConfig != null ? _currentConfig.autoStartOnPlay : false;
             }
 
+            // 初始化无窗口运行Toggle(优先从配置,否则从 PythonProcessController)
+            if (noDisplayToggle != null)
+            {
+                noDisplayToggle.isOn = _currentConfig != null ? _currentConfig.pythonNoDisplay : (pythonProcessController != null && pythonProcessController.addNoDisplayWhenLaunching);
+            }
+
             // 从配置更新UI显示(初始化时只在输入框为空时填入默认值)
             UpdateUIFromConfig(forceUpdate: false);
         }
@@ -175,6 +184,11 @@ namespace LightGlue.Unity.UI
             {
                 autoStartToggle.onValueChanged.AddListener(OnAutoStartToggleChanged);
             }
+
+            if (noDisplayToggle != null)
+            {
+                noDisplayToggle.onValueChanged.AddListener(OnNoDisplayToggleChanged);
+            }
         }
 
         /// <summary>
@@ -352,6 +366,16 @@ namespace LightGlue.Unity.UI
             {
                 autoStartToggle.isOn = _currentConfig.autoStartOnPlay;
             }
+
+            if (noDisplayToggle != null)
+            {
+                noDisplayToggle.isOn = _currentConfig.pythonNoDisplay;
+            }
+
+            if (pythonProcessController != null && noDisplayToggle != null)
+            {
+                pythonProcessController.addNoDisplayWhenLaunching = noDisplayToggle.isOn;
+            }
         }
 
         /// <summary>
@@ -367,11 +391,11 @@ namespace LightGlue.Unity.UI
                 return;
             }
 
-            // 保存自动启动选项
+            // 保存自动启动与无窗口选项
             if (autoStartToggle != null)
-            {
                 config.autoStartOnPlay = autoStartToggle.isOn;
-            }
+            if (noDisplayToggle != null)
+                config.pythonNoDisplay = noDisplayToggle.isOn;
 
             if (NetworkConfigManager.SaveConfig(config))
             {
@@ -401,13 +425,23 @@ namespace LightGlue.Unity.UI
             {
                 _currentConfig.autoStartOnPlay = value;
                 Debug.Log($"[NetworkConfigUI] 自动启动选项已更改为: {value}");
-                
-                // 自动保存配置(包括自动启动选项)
-                // 注意:这里只保存自动启动选项,其他配置需要点击"保存配置"或"应用配置"按钮
                 if (NetworkConfigManager.SaveConfig(_currentConfig))
-                {
                     Debug.Log($"[NetworkConfigUI] 自动启动选项已保存到配置文件");
-                }
+            }
+        }
+
+        private void OnNoDisplayToggleChanged(bool value)
+        {
+            if (pythonProcessController != null)
+            {
+                pythonProcessController.addNoDisplayWhenLaunching = value;
+                Debug.Log($"[NetworkConfigUI] 无窗口运行已更改为: " + value);
+            }
+            if (_currentConfig != null)
+            {
+                _currentConfig.pythonNoDisplay = value;
+                if (NetworkConfigManager.SaveConfig(_currentConfig))
+                    Debug.Log($"[NetworkConfigUI] 无窗口运行选项已保存到配置文件");
             }
         }
 
@@ -426,10 +460,15 @@ namespace LightGlue.Unity.UI
 
             _currentConfig = config;
 
-            // 保存自动启动选项到配置
             if (autoStartToggle != null)
-            {
                 config.autoStartOnPlay = autoStartToggle.isOn;
+            if (noDisplayToggle != null)
+                config.pythonNoDisplay = noDisplayToggle.isOn;
+
+            // 应用到 PythonProcessController(无窗口选项立即生效,下次启动 Python 时使用)
+            if (pythonProcessController != null && noDisplayToggle != null)
+            {
+                pythonProcessController.addNoDisplayWhenLaunching = noDisplayToggle.isOn;
             }
 
             // 应用到 Bridge(含硬件接收、Python 发送、硬件控制、结果接收;统一由下方全局重启生效)
@@ -489,7 +528,7 @@ namespace LightGlue.Unity.UI
         {
             Debug.Log("[NetworkConfigUI] 开始启动所有相关组件...");
 
-            // 1. 启动 Python 进程(重启或首次启动时显式拉起,确保新端口/参数生效
+            // 1. 启动 Python 进程(若为「首帧后再启动」则仅启用组件,由 Bridge 收到首帧后启动
             if (pythonProcessController != null)
             {
                 if (!pythonProcessController.IsRunning)
@@ -498,10 +537,17 @@ namespace LightGlue.Unity.UI
                         pythonProcessController.enabled = true;
                     if (!pythonProcessController.gameObject.activeInHierarchy)
                         pythonProcessController.gameObject.SetActive(true);
-                    // 始终显式启动,使应用配置后的重启能正确用新 scriptArgs 拉起进程
-                    pythonProcessController.StartPython();
+                    bool deferToFirstFrame = bridge != null && bridge.startPythonOnFirstImageReceived && pythonProcessController.startOnFirstImageReceived;
+                    if (!deferToFirstFrame)
+                    {
+                        pythonProcessController.StartPython();
+                        Debug.Log("[NetworkConfigUI] PythonProcessController 已启动");
+                    }
+                    else
+                        Debug.Log("[NetworkConfigUI] 将等首帧图像到达后由 Bridge 启动 Python");
                 }
-                Debug.Log("[NetworkConfigUI] PythonProcessController 已启动");
+                else
+                    Debug.Log("[NetworkConfigUI] PythonProcessController 已运行");
             }
 
             // 2. 启动 Bridge(含硬件接收、硬件控制、结果接收;最后启动,依赖 Python)
@@ -516,6 +562,12 @@ namespace LightGlue.Unity.UI
                     bridge.gameObject.SetActive(true);
                 }
                 Debug.Log("[NetworkConfigUI] HardwareToPythonUdpBridge 已启动");
+                // 重启后重新向硬件下发 0x40 图传参数,否则硬件可能停止发图导致画面无更新
+                if (bridge.transmissionConfig != null)
+                {
+                    bridge.ApplyConfig(bridge.transmissionConfig);
+                    Debug.Log("[NetworkConfigUI] 已重新下发 0x40 图传参数到硬件,摄像头图像应恢复");
+                }
             }
 
             // 3. 启动 SDK Bridge(如果有),用于与旧 Bridge 同场对比
@@ -678,9 +730,9 @@ namespace LightGlue.Unity.UI
                 applyConfigButton.onClick.RemoveListener(OnApplyConfigClicked);
             }
             if (autoStartToggle != null)
-            {
                 autoStartToggle.onValueChanged.RemoveListener(OnAutoStartToggleChanged);
-            }
+            if (noDisplayToggle != null)
+                noDisplayToggle.onValueChanged.RemoveListener(OnNoDisplayToggleChanged);
         }
 
         private void OnApplicationQuit()

+ 5 - 5
Unity2021.3.42f1-YejiDemo/ProjectSettings/ProjectSettings.asset

@@ -13,7 +13,7 @@ PlayerSettings:
   useOnDemandResources: 0
   accelerometerFrequency: 60
   companyName: Slambb
-  productName: LightGlueDemo1
+  productName: LightGlueDemo
   defaultCursor: {fileID: 0}
   cursorHotspot: {x: 0, y: 0}
   m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1}
@@ -42,8 +42,8 @@ PlayerSettings:
   m_SplashScreenLogos: []
   m_VirtualRealitySplashScreen: {fileID: 0}
   m_HolographicTrackingLossScreen: {fileID: 0}
-  defaultScreenWidth: 1024
-  defaultScreenHeight: 768
+  defaultScreenWidth: 1920
+  defaultScreenHeight: 1080
   defaultScreenWidthWeb: 960
   defaultScreenHeightWeb: 600
   m_StereoRenderingPath: 0
@@ -142,7 +142,7 @@ PlayerSettings:
     16:10: 1
     16:9: 1
     Others: 1
-  bundleVersion: 0.1
+  bundleVersion: 0.2
   preloadedAssets: []
   metroInputSource: 0
   wsaTransparentSwapchain: 0
@@ -164,7 +164,7 @@ PlayerSettings:
   androidMaxAspectRatio: 2.1
   applicationIdentifier:
     Android: com.xmjssvr.BowArrowInfraredDoubleTest
-    Standalone: com.Slambb.LightGlueDemo1
+    Standalone: com.Slambb.LightGlueDemo
   buildNumber:
     Bratwurst: 0
     Standalone: 0

+ 4 - 4
Unity2021.3.42f1-YejiDemo/ProjectSettings/QualitySettings.asset

@@ -100,7 +100,7 @@ QualitySettings:
     softVegetation: 0
     realtimeReflectionProbes: 0
     billboardsFaceCameraPosition: 0
-    vSyncCount: 1
+    vSyncCount: 0
     realtimeGICPUUsage: 25
     lodBias: 0.7
     maximumLODLevel: 0
@@ -137,7 +137,7 @@ QualitySettings:
     softVegetation: 1
     realtimeReflectionProbes: 1
     billboardsFaceCameraPosition: 1
-    vSyncCount: 1
+    vSyncCount: 0
     realtimeGICPUUsage: 50
     lodBias: 1
     maximumLODLevel: 0
@@ -174,7 +174,7 @@ QualitySettings:
     softVegetation: 1
     realtimeReflectionProbes: 1
     billboardsFaceCameraPosition: 1
-    vSyncCount: 1
+    vSyncCount: 0
     realtimeGICPUUsage: 50
     lodBias: 1.5
     maximumLODLevel: 0
@@ -211,7 +211,7 @@ QualitySettings:
     softVegetation: 1
     realtimeReflectionProbes: 1
     billboardsFaceCameraPosition: 1
-    vSyncCount: 1
+    vSyncCount: 0
     realtimeGICPUUsage: 100
     lodBias: 2
     maximumLODLevel: 0

+ 122 - 0
docs/Win11游戏全屏时Python后台节流_设置与修改.md

@@ -0,0 +1,122 @@
+# Win11 游戏全屏时 Python 后台节流:设置与修改说明
+
+本文档单独说明「游戏全屏到前台后,Python 算法在后台几乎不动」的成因、解决步骤,以及项目内已做的代码修改。通常**步骤 1**即可明显改善,步骤 2、3 作为补充。
+
+---
+
+## 一、现象与原因
+
+### 现象
+
+- **Python 窗口在前台**:算法运行流畅。
+- **游戏全屏到前台后**:Python 在后台几乎不动或明显变慢。
+
+### 原因简述
+
+- Windows 11 会把「无窗口或处于后台」的进程标记为**效率模式 (EcoQoS)**,限制其 CPU/GPU 使用。
+- 当 Unity 以全屏/最大化在前台时,系统会优先保证前台帧率,对后台进程(如由 Unity 拉起的 Python 子进程)进行调度限制甚至挂起。
+- 因此卡顿主要来自**进程被系统节流**,而不是 UDP 被单独限速;禁用效率模式并配合下面设置可缓解。
+
+---
+
+## 二、解决步骤(推荐顺序)
+
+### 步骤 1:禁用 Python 的「效率模式」(EcoQoS)(项目内已做)
+
+通过 Windows API 告知系统:该 Python 进程不应被电源/效率策略限制。
+
+**项目内实现**:在 **`LightGlue_Deployment/demo_lightglue_camera_position_async.py`** 脚本**最开头**(其他业务 import 之前)已加入以下逻辑:
+
+```python
+# 强制关闭 Windows 11 的后台限制(效率模式 / EcoQoS),避免游戏全屏时 Python 被系统挂起
+import ctypes
+import sys
+
+def _disable_win11_efficiency_mode():
+    if sys.platform != "win32":
+        return
+    try:
+        handle = ctypes.windll.kernel32.GetCurrentProcess()
+        # PROCESS_POWER_THROTTLING_STATE: Version=1, ControlMask=1(Speed), StateMask=0(Disable limit)
+        class ProcessPowerThrottlingState(ctypes.Structure):
+            _fields_ = [
+                ("Version", ctypes.c_ulong),
+                ("ControlMask", ctypes.c_ulong),
+                ("StateMask", ctypes.c_ulong),
+            ]
+        state = ProcessPowerThrottlingState(1, 1, 0)
+        ProcessPowerThrottling = 4  # ProcessPowerThrottling
+        if ctypes.windll.kernel32.SetProcessInformation(
+            handle, ProcessPowerThrottling, ctypes.byref(state), ctypes.sizeof(state)
+        ):
+            print("Win11: 已禁用进程效率模式限制 (EcoQoS)", flush=True)
+        else:
+            print("Win11: 设置效率模式失败 (非致命)", flush=True)
+    except Exception as e:
+        print(f"Win11 效率模式设置异常: {e}", flush=True)
+
+_disable_win11_efficiency_mode()
+```
+
+- 仅当 `sys.platform == "win32"` 时执行;失败或异常只打日志,不中断脚本。
+- **请谨慎使用此类系统 API**,仅在需要保证后台 Python 算法不被节流的场景下保留。
+
+---
+
+### 步骤 2:Unity 端保持 Run In Background(项目内已做)
+
+确保游戏在前台全屏时,Unity 仍持续更新、收发 UDP,不因失去逻辑焦点而降频。
+
+**项目内实现**:在 **`Unity2021.3.42f1-YejiDemo/Assets/LightGlue/Scripts/Bridge/HardwareToPythonUdpBridge.cs`** 的 **OnEnable()** 开头增加:
+
+```csharp
+// 游戏全屏时保持收发 UDP/进程通信,避免因失去焦点被系统节流导致 Python 端卡顿
+Application.runInBackground = true;
+```
+
+**可选检查**:
+
+- 在 Unity 菜单:**Edit → Project Settings → Player → Resolution and Presentation**,确认 **Run In Background** 已勾选(项目默认已开启)。
+- 若代码中有 `Application.targetFrameRate`,建议设为 `-1` 或 60 以上,避免限制帧率导致更新变慢。
+
+---
+
+### 步骤 3:关闭 Windows「硬件加速 GPU 计划」(HAGS)(需用户手动)
+
+若算法使用 GPU(如 OpenCV CUDA、TensorRT),Win11 的 GPU 调度可能把显存资源大量分配给前台 Unity,导致 Python 端 GPU 不足。
+
+**操作**:
+
+1. 打开 **Win11 设置 → 系统 → 屏幕 → 图形 → 更改默认图形设置**。
+2. 将 **「硬件加速 GPU 计划」(HAGS)** 设为 **关闭**。
+3. **必须重启电脑** 后生效。
+
+---
+
+## 三、涉及文件一览
+
+| 位置 | 文件 | 修改内容 |
+|------|------|----------|
+| LightGlue_Deployment | `demo_lightglue_camera_position_async.py` | 脚本最开头增加 `_disable_win11_efficiency_mode()` 并调用 |
+| YejiDemo | `Assets/LightGlue/Scripts/Bridge/HardwareToPythonUdpBridge.cs` | `OnEnable()` 开头设置 `Application.runInBackground = true` |
+
+系统侧 HAGS 关闭无代码变更,需在系统中手动设置并重启。
+
+---
+
+## 四、若仍不如 Python 在前台流畅(可选)
+
+- **任务管理器提优先级**:运行游戏后,在任务管理器中找到 `python.exe` 或 `lightglue_runtime.exe`,右键 → **转到详细信息** → 右键进程 → **设置优先级** → 选「高于正常」或「高」。仅当次有效,进程重启后需重新设置。
+- **说明**:UDP 本身一般不会被系统单独限速;观感上的“不流畅”多来自**进程/GPU 被调度限制**,步骤 1~3 已针对此做缓解。
+
+---
+
+## 五、小结
+
+| 步骤 | 内容 | 状态 |
+|------|------|------|
+| 1 | Python 脚本开头禁用 Win11 效率模式 (EcoQoS) | 已写入 `demo_lightglue_camera_position_async.py` |
+| 2 | Unity Bridge OnEnable 中 `Application.runInBackground = true` | 已写入 `HardwareToPythonUdpBridge.cs` |
+| 3 | 系统关闭「硬件加速 GPU 计划」(HAGS) 并重启 | 需用户本机手动设置 |
+
+本说明与 `YejiDemo_网络与图传修改说明.md` 中「八、游戏全屏时 Python 被 Win11 节流」对应,此处为单独成档便于查阅与分享。

+ 218 - 0
docs/YejiDemo_网络与图传修改说明.md

@@ -0,0 +1,218 @@
+# YejiDemo 网络与图传修改说明
+
+本文档记录为修复「应用配置后预览断流」「JPEG 损坏/闪烁」「Python 启动期间错乱」「绘制窗口不弹出」等问题所做的修改,便于后续维护与排查。
+
+---
+
+## 一、应用配置后摄像头预览断流
+
+### 现象
+点击「应用配置」并重启组件后,Unity 端摄像头预览无画面,需重新启动整个应用才恢复。
+
+### 原因
+1. 重启后未向硬件重新下发 **0x40 图传参数**,硬件可能停止或按错误参数发图。
+2. 首次运行 Bridge 用预制体上的 `hardwareBindIp` 建 receiver,`Start()` 才从配置文件加载;应用配置重启时用文件里的 IP 建 receiver,若与首次不一致(如 192.168.0.109 vs 198.18.0.1),会导致收不到包。
+
+### 修改
+
+**1. NetworkConfigUIController(YejiDemo / LightGlue_Unity)**
+- 在 `StartAllComponents()` 中,启用 Bridge 后若 `bridge.transmissionConfig != null`,调用 `bridge.ApplyConfig(bridge.transmissionConfig)`,**重启后重新下发 0x40**。
+- `StartAllComponents()` 中当 `!pythonProcessController.IsRunning` 时**始终显式** `StartPython()`(YejiDemo 已为始终启动;LightGlue_Unity 原逻辑在 `autoStartOnEnable==true` 时不调,已改为始终调)。
+
+**2. HardwareToPythonUdpBridge(YejiDemo)**
+- 在 **OnEnable()** 中,**创建 UDPJpegReceiver 之前**先执行 `LoadNetworkConfig()`(当 `autoLoadNetworkConfig` 为 true),保证**首次运行与「应用配置重启」使用同一绑定地址**,避免因 IP 不一致断流。
+
+**3. Bridge:排空队列时也更新 _latestJpeg(YejiDemo)**
+- 在 `Update()` 中,当「未启用图传」或「发送间隔节流」仅排空队列时,用**最后一帧**更新 `_latestJpeg` 并加锁,保证 Viewer 在节流/未发图时仍能显示最新收到的一帧。
+
+---
+
+## 二、首帧后再启动 Python(避免无图时白跑 TensorRT)
+
+### 现象 / 需求
+希望首次启动或应用配置重启后,**等收到首帧图像再启动 Python**,避免无图时也拉 TensorRT 等。
+
+### 修改
+
+**1. PythonProcessController(YejiDemo)**
+- 新增 `startOnFirstImageReceived`(默认 true):为 true 时 **OnEnable() 不调用 StartPython()**,由 Bridge 在收到首帧后调用。
+- 新增 `readyTimeoutSeconds`、`Update()`:若在此秒数内未收到 Python 就绪输出,则视为就绪并开始发图(防 stdout 缓冲导致窗口一直不弹出)。
+
+**2. HardwareToPythonUdpBridge(YejiDemo)**
+- 新增 `startPythonOnFirstImageReceived`(默认 true)、内部 `_pythonStartedByFirstFrame`。
+- 在任意「收到一帧图像」的分支中调用 `TryStartPythonOnFirstFrame()`:若启用且 Python 未运行且 `pythonProcessController.startOnFirstImageReceived`,则调用 `StartPython()` 一次并打日志「已收到首帧图像,启动 Python 进程」。
+- **OnDisable()** 时重置 `_pythonStartedByFirstFrame`,以便下次启用重新等首帧。
+
+**3. NetworkConfigUIController(YejiDemo)**
+- `StartAllComponents()` 中:当 Bridge 与 PythonProcessController 均启用「首帧后再启动」时,**不在此处调用 StartPython()**,只启用组件,由 Bridge 在首帧到达后启动。
+
+---
+
+## 三、图传参数 0x40 下发时机(解决画面不完整/闪烁)
+
+### 现象
+一开始画面不完整或闪烁,**重新下发一次软件指令(0x40)后才正常**。
+
+### 原因
+- `MarkHardwareAutoApplyReady()` 曾被注释,首帧时从未自动下发 0x40。
+- `SetTransmissionConfig()` 只更新内存配置,未向硬件发送 0x40,硬件一直用默认/错误参数发图。
+
+### 修改
+
+**1. HardwareToPythonUdpBridge(YejiDemo)**
+- 在 **SetTransmissionConfig()** 末尾:当 `config != null` 且 `_hwControlClient`、`_hwControlEndpoint` 已就绪时,**立即调用 ApplyConfig(config)**,配置一旦设置就下发 0x40。
+
+**2. ImageTransmissionUIController(YejiDemo)**
+- 恢复对 **bridge.MarkHardwareAutoApplyReady()** 的调用,保证「首帧时自动下发 0x40」的备用路径有效。
+
+---
+
+## 四、UDP JPEG 组帧与假 SOI(解决 Corrupt JPEG / 不完整/闪烁)
+
+### 现象
+- Viewer:`Failed to decode JPEG (size: xxx bytes)`。
+- Python:`Corrupt JPEG data: premature end of data segment`、`extraneous bytes before marker 0xd9` 等。
+
+### 原因
+1. **假 SOI**:JPEG 熵编码中 0xFF 会填充为 0xFF 0x00,流中可能出现 0xFF 0x00 0xD8,原逻辑把其中的 0xFF 0xD8 当成新帧 SOI,从上一帧中间开始组帧,导致「多余字节/截断」。
+2. **取到第一个 FFD9 后丢弃剩余数据**:缓冲中若为 `[JPEG1 FFD9][FFD8 JPEG2...]`,丢弃后半段会导致下一帧丢失 SOI 或从中间开始,后续帧错乱。
+
+### 修改(UDPJpegReceiver,YejiDemo)
+
+**1. 只认「真实」SOI**
+- 新增 `IndexOfRealJpegSoi()`:查找 0xFF 0xD8 时,若前两字节为 0xFF 0x00(填充)则跳过,继续找下一个。
+- 在 **ProcessData()** 中,开始新帧时用 `IndexOfRealJpegSoi()` 替代原来的 `IndexOf(..., JpegStart, ...)`。
+
+**2. 取出完整一帧后保留下一帧头**
+- 在找到第一个 FFD9 并拷贝出一帧后,在**剩余缓冲**(`jpegLen` 到 `_bufferLen`)中查找下一个真实 SOI;若存在,将该段移到 `_buffer` 头部、更新 `_bufferLen`、保持 `_receiving = true`,不整段丢弃。
+
+**3. 越界修复**
+- 查找下一帧 SOI 时第三参数传**剩余长度** `_bufferLen - jpegLen`,且仅在 `remainCount >= 2` 时调用。
+- `IndexOfRealJpegSoi()` 内增加防护:`data == null` 或 `count < 2` 或 `startIndex + count > data.Length` 时直接返回 -1。
+
+**4. HardwareUdpJpegViewer(YejiDemo)**
+- 仅当 `latest` 以 0xFF 0xD8 开头且以 0xFF 0xD9 结尾时才调用 `LoadImage`,避免对 UDP 残片反复解码。
+- 解码失败日志限流:同一 Viewer 约 1 秒最多打一次。
+
+---
+
+## 五、Python 启动期间不发图(避免 TensorRT 编译时缓冲堆积)
+
+### 现象
+一开始正常,**Python 启动(TensorRT 编译)过程中开始出现大量 Corrupt JPEG**,之后只能重发指令才恢复。
+
+### 原因
+Python 在 TensorRT 编译期间几乎不读 UDP,Unity 持续发图导致系统接收缓冲堆积;Python 开始读时读到乱序/拼接数据,出现 premature end、extraneous bytes。
+
+### 修改
+
+**1. PythonProcessController(YejiDemo)**
+- 新增 **IsReadyToReceiveFrames**:仅当从 stdout 检测到就绪标记后才为 true。
+- 在 **OutputDataReceived** 中:若当前未就绪且行内容包含 `"UDP JPEG mode: receiver started"` 或 `"First frame received and processed"`,则置 `_readyToReceiveFrames = true` 并打日志「已就绪接收图像,开始向 Python 发送帧」。
+- 进程退出时(StopPython)将该标志重置为 false。
+- 新增 **readyTimeoutSeconds**(默认 120)与 **Update()**:若进程已运行超过该时间仍未就绪,则视为就绪并打日志「未检测到就绪输出,超时后视为就绪并开始发送」。
+
+**2. HardwareToPythonUdpBridge(YejiDemo)**
+- 在**实际向 Python 发图**(UDP 或 Stdin)前增加判断:`if (pythonProcessController != null && !pythonProcessController.IsReadyToReceiveFrames) return;`。
+- 未就绪时仍会排空队列、更新 `_latestJpeg`,Viewer 预览不受影响,仅不向 Python 发送。
+
+---
+
+## 六、Python 绘制窗口在游戏背后很卡(无窗口运行选项)
+
+### 现象
+启动 Python 后,若把 Python 的绘制窗口放在游戏窗口背后,Python 侧会明显卡顿;切到 Python 窗口才流畅。
+
+### 原因
+**不是**“后台进程被限速”,而是 **Windows 对非前台窗口的绘制/刷新做了节流**。Python 用 OpenCV 的 `cv2.imshow()` 更新窗口,当该窗口不是前台窗口时,系统会降低其刷新优先级或合并/延迟重绘,看起来就像卡顿。切到前台后恢复正常。
+
+### 修改
+
+**1. PythonProcessController(YejiDemo)**
+- 新增 **addNoDisplayWhenLaunching**(默认 **true**):勾选后,在拼最终启动参数时若尚未包含 `--no_display`,则自动追加 `--no_display`。
+- Python 脚本支持 `--no_display` 时**不创建 OpenCV 窗口**,只做推理并通过 UDP 把结果回传给 Unity,由 Unity 显示。这样没有“在背后的窗口”,也就不会被系统节流,与游戏同屏时更流畅。
+- 需要看 Python 侧绘制窗口做调试时,可在 Inspector 中**取消勾选**,并保证启动参数里没有 `--no_display`。
+
+**2. Python 脚本(demo_lightglue_camera_position_async.py)**
+- `--no_display` 时原先会把 `async_visualizer` 设为 `None`,而**向 Unity 回传结果**的逻辑在 `AsyncVisualizer` 内(`result_sender.send_result`),导致无窗口时算法跑但不发结果。已改为:只要有 `result_sender`(与 Unity 联机)就创建 `AsyncVisualizer`,无窗口时仅不弹窗(不调用 `cv2.imshow`),仍正常发结果。
+
+**3. 使用方式**
+- 默认勾选「无窗口运行」:与 Unity 联调时推荐,游戏在前台时 Python 全速跑,结果照常回 Unity。
+- 取消勾选或手动在参数中去掉 `--no_display`:保留 Python 绘制窗口,适合单独调试 Python;此时若窗口在游戏背后会卡是系统行为,可把窗口置前或接受节流。
+
+---
+
+## 七、Python 绘制窗口不弹出(stdout 缓冲 + 超时回退)
+
+### 现象
+改了「Python 就绪后再发图」逻辑后,Python 程序启动后**绘制窗口不弹出**。
+
+### 原因
+Python 在非 TTY(管道)下 stdout 常为**全缓冲**,「UDP JPEG mode: receiver started in background thread」未及时送到 Unity,`IsReadyToReceiveFrames` 一直为 false,Unity 不发首帧,Python 卡在 “Waiting for first UDP frame...”,无法进入主循环创建窗口。
+
+### 修改
+
+**1. demo_lightglue_camera_position_async.py(LightGlue_Deployment)**
+- 将  
+  `print("UDP JPEG mode: receiver started in background thread")`  
+  改为  
+  `print("UDP JPEG mode: receiver started in background thread", flush=True)`  
+  使该行立即送往 Unity。
+
+**2. PythonProcessController(YejiDemo)**
+- 记录 ** _processStartTime**(进程启动时),在 **Update()** 中:若进程在跑、未就绪、且已超过 **readyTimeoutSeconds**,则置 `_readyToReceiveFrames = true` 并打日志,作为未收到就绪输出时的回退。
+
+---
+
+## 八、游戏全屏时 Python 被 Win11 节流(效率模式 / HAGS)
+
+### 现象
+Python 窗口在前台时算法流畅;**游戏全屏到前台后,Python 在后台几乎不动**。原因是 Win11 把“无窗口或后台”的进程标成效率模式 (EcoQoS),并配合前台全屏应用(如 Unity)做 CPU/GPU 调度限制。
+
+### 已做修改
+
+**1. Python:禁用效率模式 (EcoQoS)**  
+- 在 **demo_lightglue_camera_position_async.py** 最开头(其他 import 之前)调用 Windows API,关闭当前进程的「进程电源节流」:  
+  `SetProcessInformation(..., ProcessPowerThrottling, Speed, Disable limit)`  
+- 仅当 `sys.platform == "win32"` 时执行;失败或异常只打日志,不中断脚本。
+
+**2. Unity:强制 Run In Background**  
+- 在 **HardwareToPythonUdpBridge.OnEnable()** 中设置 `Application.runInBackground = true`,确保游戏在前台全屏时仍持续收发 UDP/与 Python 通信,不因“失去逻辑焦点”被 Unity 自身降频。
+
+**3. 系统设置(需用户手动)**  
+- 关闭 **「硬件加速 GPU 计划」(HAGS)**:  
+  Win11 设置 → 系统 → 屏幕 → 图形 → 更改默认图形设置 → 关闭 HAGS → **重启电脑**。  
+- 若只关 HAGS 仍不如 Python 在前台流畅,优先确认上述 1、2 已生效;UDP 本身一般不会被系统单独限速,卡顿主要来自**进程被节流导致 Python 处理变慢**,禁用 EcoQoS + Run In Background 可缓解。
+
+### 涉及文件
+- `LightGlue_Deployment/demo_lightglue_camera_position_async.py`:脚本开头 `_disable_win11_efficiency_mode()`。  
+- `YejiDemo/.../HardwareToPythonUdpBridge.cs`:OnEnable 中 `Application.runInBackground = true`。
+
+---
+
+## 九、涉及文件一览
+
+| 位置 | 文件 | 修改要点 |
+|------|------|----------|
+| YejiDemo | `NetworkConfigUIController.cs` | 重启后重发 0x40;首帧后再启动时不在 StartAllComponents 中启 Python;启 Python 时考虑「首帧后再启动」 |
+| YejiDemo | `HardwareToPythonUdpBridge.cs` | OnEnable 先 LoadNetworkConfig、runInBackground=true;排空队列时更新 _latestJpeg;SetTransmissionConfig 内立即 ApplyConfig;TryStartPythonOnFirstFrame;仅 IsReadyToReceiveFrames 时发图 |
+| YejiDemo | `PythonProcessController.cs` | startOnFirstImageReceived;IsReadyToReceiveFrames + stdout 检测;readyTimeoutSeconds + Update 超时回退;addNoDisplayWhenLaunching(无窗口运行) |
+| YejiDemo | `UDPJpegReceiver.cs` | IndexOfRealJpegSoi;取出帧后保留下一帧 SOI;剩余长度与越界防护 |
+| YejiDemo | `HardwareUdpJpegViewer.cs` | 仅对形如完整 JPEG 的缓冲解码;解码失败日志限流 |
+| YejiDemo | `ImageTransmissionUIController.cs` | 恢复 MarkHardwareAutoApplyReady() |
+| LightGlue_Unity | `NetworkConfigUIController.cs` | 重启后重发 0x40;StartAllComponents 中始终显式 StartPython(当 !IsRunning) |
+| LightGlue_Deployment | `demo_lightglue_camera_position_async.py` | 就绪行 print 增加 flush=True;脚本开头禁用 Win11 效率模式 (EcoQoS) |
+
+---
+
+## 十、配置项速查(Inspector)
+
+- **Bridge**  
+  - `startPythonOnFirstImageReceived`:为 true 时收到首帧后再启动 Python 且仅在 Python 就绪后发图。  
+  - `autoLoadNetworkConfig`:OnEnable 时先加载网络配置再建 receiver。
+- **PythonProcessController**  
+  - `startOnFirstImageReceived`:为 true 时 OnEnable 不启 Python,由 Bridge 首帧时启。  
+  - `readyTimeoutSeconds`:未收到就绪输出时,超过该秒数视为就绪并开始发图(默认 120)。  
+  - **`addNoDisplayWhenLaunching`**:为 true(默认)时自动在参数中加 `--no_display`,Python 不弹绘制窗口,避免「窗口在游戏背后被系统节流导致卡顿」;取消勾选可保留 Python 窗口便于调试。
+
+若需恢复「一启动就拉 Python、立即发图」,将上述两个「首帧/就绪」相关选项设为 false 即可。

+ 74 - 0
docs/进入游戏即启动Python_分析.md

@@ -0,0 +1,74 @@
+# 「进入游戏就启动 Python,后续再连接图像端口」分析
+
+## 你想达成的效果
+
+- **进入游戏** → 立刻启动 Python(不等首帧图像)。
+- **图像端口** → 等 Python 就绪后再开始往 Python 发图(“后续再连接”)。
+
+这样 Python 的初始化(加载模型、TensorRT 编译等)和游戏初始化**并行**进行,不会因为“等首帧才拉 Python”而让程序在首帧到来时才卡一下;图像传输仍然在“Python 就绪”之后才开始,所以**不会因为程序初始化卡顿去影响图像传输**(不会在 TensorRT 期间堆 UDP 导致损坏)。
+
+---
+
+## 和当前逻辑的对比
+
+| 项目 | 当前(首帧后再启动 Python) | 改成「进游戏就启动」 |
+|------|-----------------------------|----------------------|
+| Python 何时启动 | 收到首帧图像时由 Bridge 调 `StartPython()` | 进游戏 / 自动启动时立刻调 `StartPython()` |
+| 图像何时发到 Python | 仍是「Python 就绪后才发」(`IsReadyToReceiveFrames`) | **不变**,还是就绪后才发 |
+| 程序初始化卡顿 | 首帧到来时才起 Python,可能在那一下卡 | Python 提前在后台跑,和游戏 init 并行,不卡在首帧 |
+| 无图时是否起 Python | 不起,省 TensorRT 时间 | 会起,无图也会跑完 TensorRT |
+
+结论:  
+- **“后续再连接图像”** 已经由现在的「就绪后才发图」保证,不用再改。  
+- 唯一要改的是:**把“何时调用 StartPython()”从“首帧时”改成“进游戏/自动启动时”**。
+
+---
+
+## 改动是否麻烦:不麻烦
+
+### 需要动的地方(思路)
+
+1. **PythonProcessController**
+   - 当前:`startOnFirstImageReceived == true` 时,OnEnable 不调 `StartPython()`,由 Bridge 在首帧时调。
+   - 改法:增加一个选项,例如 **「进游戏即启动 Python」**(或直接让“自动启动”时不等首帧):
+     - 若为 true:在 **OnEnable**(或由 NetworkConfigUIController 在自动启动时)**直接调用 `StartPython()`**,不再等首帧。
+     - 若为 false:保持现状,首帧时再由 Bridge 调 `StartPython()`。
+   - 实现量:多一个 bool + 一两处 if,约 5 分钟级别。
+
+2. **NetworkConfigUIController.StartAllComponents()**
+   - 当前:若 `startOnFirstImageReceived` 为 true,这里**不**调 `StartPython()`,等 Bridge 首帧再启。
+   - 改法:若改为「进游戏即启动」,则在这里(自动启动时)**一律调一次 `StartPython()`**,不再依赖首帧。
+   - 实现量:改一个条件分支即可。
+
+3. **Bridge.TryStartPythonOnFirstFrame()**
+   - 若采用「进游戏即启动」,这里可以:
+     - 要么不再调用(Python 已提前启好),要么保留为“若还没启则首帧时补启”的兜底。
+   - 实现量:保留或加一句 `if (pythonProcessController.IsRunning) return;` 即可。
+
+4. **图像传输逻辑**
+   - **不用改**。继续依赖 `IsReadyToReceiveFrames`,就绪前不发图,就绪后才发,即“后续再连接图像端口”。
+
+### 小结
+
+- **逻辑改动**:只改“何时第一次调用 `StartPython()`”(从首帧 → 进游戏/自动启动)。
+- **已有保护**:「就绪后才发图」已经避免初始化阶段影响图像传输,无需再动。
+- **代码量**:小,集中在 1~2 个类、几处分支,不涉及协议或新线程。
+
+---
+
+## 利弊简表
+
+| 好处 | 代价 |
+|------|------|
+| 进游戏后 Python 立刻在后台初始化,首帧来时通常已就绪,减少“首帧卡一下” | 无图也会起 Python,会跑 TensorRT(无硬件时多耗一点时间) |
+| 图像传输仍然在“就绪后”才连上,不会因初始化卡顿破坏图像 | 若希望“无图绝不启 Python”,需要保留当前模式或再加一个开关 |
+
+---
+
+## 建议
+
+- 若你更在意「进游戏就顺、不因等 Python 卡顿」:改成「进游戏就启动 Python,后续再连接图像」**不麻烦**,按上面几处改即可。
+- 若希望保留「无图不启 Python」:可以加一个选项(例如「进游戏即启动 Python」勾选 = 当前新逻辑,不勾选 = 保持首帧再启),两套行为并存,改动量仍然不大。
+
+如果你愿意按其中一种方案落代码,我可以按你选的方案给出具体改法(含要改的类名、方法名和条件判断)。  
+你可以先看下上面分析,再决定是「直接改成进游戏就启动」还是「加一个开关两种模式都保留」。