|
42 | 42 | initialize_graph_write_plan, |
43 | 43 | write_graph_plan_frame, |
44 | 44 | ) |
| 45 | +from app.workers.frame_helpers import ( |
| 46 | + bbox_intersection, |
| 47 | + bbox_union, |
| 48 | + clip_frame_to_bbox, |
| 49 | + coerce_preview_frame, |
| 50 | + frame_bbox, |
| 51 | +) |
45 | 52 | from app.utils.media import ( |
46 | 53 | is_supported_image_file, |
47 | 54 | is_numbered_image_sequence, |
@@ -795,115 +802,23 @@ def _prepare_graph_write_targets( |
795 | 802 |
|
796 | 803 | @staticmethod |
797 | 804 | def _coerce_preview_frame(frame) -> np.ndarray | None: |
798 | | - arr = np.asarray(frame) |
799 | | - if arr.ndim == 2: |
800 | | - gray = arr.astype(np.float32) |
801 | | - min_val = float(np.nanmin(gray)) if gray.size else 0.0 |
802 | | - max_val = float(np.nanmax(gray)) if gray.size else 0.0 |
803 | | - if gray.dtype != np.uint8: |
804 | | - if InferenceWorker._is_normalized_float_range(min_val, max_val): |
805 | | - gray = np.clip(gray, 0.0, 1.0) |
806 | | - gray = np.power(gray, 1.0 / 2.2) * 255.0 |
807 | | - gray = np.clip(gray, 0.0, 255.0).astype(np.uint8) |
808 | | - else: |
809 | | - gray = arr |
810 | | - return np.stack([gray] * 3, axis=-1) |
811 | | - if arr.ndim == 3 and arr.shape[2] == 1: |
812 | | - return InferenceWorker._coerce_preview_frame(arr[:, :, 0]) |
813 | | - if arr.ndim == 3 and arr.shape[2] >= 3: |
814 | | - if arr.dtype != np.uint8 and arr.shape[2] >= 4: |
815 | | - # 4-channel float data = linear premultiplied RGBA (e.g. CorridorKey |
816 | | - # 'processed' output). Stripping alpha and gamma-correcting the |
817 | | - # premultiplied RGB produces false blue/dark on transparent areas |
818 | | - # (e.g. glass: premul_b > premul_r after gamma). |
819 | | - # Correct approach: composite over a neutral mid-grey so that |
820 | | - # transparent areas show as grey, not as dark premultiplied colour. |
821 | | - rgb_premul = arr[:, :, :3].astype(np.float32) |
822 | | - alpha_ch = np.clip(arr[:, :, 3:4].astype(np.float32), 0.0, 1.0) |
823 | | - min_val = float(np.nanmin(rgb_premul)) if rgb_premul.size else 0.0 |
824 | | - max_val = float(np.nanmax(rgb_premul)) if rgb_premul.size else 0.0 |
825 | | - if InferenceWorker._is_normalized_float_range(min_val, max_val): |
826 | | - # linear premul + neutral grey bg (linear 0.214 ≈ sRGB 0.5) |
827 | | - bg_lin = 0.214 |
828 | | - comp_lin = np.clip(rgb_premul + bg_lin * (1.0 - alpha_ch), 0.0, 1.0) |
829 | | - # linear → sRGB |
830 | | - comp_srgb = np.where(comp_lin <= 0.0031308, |
831 | | - comp_lin * 12.92, |
832 | | - 1.055 * np.power(np.clip(comp_lin, 1e-9, 1.0), 1.0 / 2.4) - 0.055) |
833 | | - return np.clip(comp_srgb * 255.0, 0.0, 255.0).astype(np.uint8) |
834 | | - else: |
835 | | - return np.clip(rgb_premul, 0.0, 255.0).astype(np.uint8) |
836 | | - |
837 | | - rgb = arr[:, :, :3].astype(np.float32) |
838 | | - if arr.dtype != np.uint8: |
839 | | - min_val = float(np.nanmin(rgb)) if rgb.size else 0.0 |
840 | | - max_val = float(np.nanmax(rgb)) if rgb.size else 0.0 |
841 | | - if InferenceWorker._is_normalized_float_range(min_val, max_val): |
842 | | - rgb = np.clip(rgb, 0.0, 1.0) |
843 | | - rgb = np.power(rgb, 1.0 / 2.2) * 255.0 |
844 | | - rgb = np.clip(rgb, 0.0, 255.0).astype(np.uint8) |
845 | | - else: |
846 | | - rgb = arr[:, :, :3] |
847 | | - return rgb |
848 | | - return None |
| 805 | + return coerce_preview_frame(frame) |
849 | 806 |
|
850 | 807 | @staticmethod |
851 | 808 | def _frame_bbox(frame) -> tuple[int, int, int, int]: |
852 | | - arr = np.asarray(frame) |
853 | | - if arr.ndim == 2: |
854 | | - coverage = np.asarray(arr, dtype=np.float32) > 1e-6 |
855 | | - elif arr.ndim == 3 and arr.shape[2] >= 4: |
856 | | - coverage = np.asarray(arr[:, :, 3], dtype=np.float32) > 1e-6 |
857 | | - elif arr.ndim == 3: |
858 | | - coverage = np.any(np.asarray(arr[:, :, :3], dtype=np.float32) > 1e-6, axis=2) |
859 | | - else: |
860 | | - return (0, 0, 0, 0) |
861 | | - |
862 | | - ys, xs = np.where(coverage) |
863 | | - if ys.size == 0 or xs.size == 0: |
864 | | - return (0, 0, 0, 0) |
865 | | - x0 = int(np.min(xs)) |
866 | | - y0 = int(np.min(ys)) |
867 | | - x1 = int(np.max(xs)) + 1 |
868 | | - y1 = int(np.max(ys)) + 1 |
869 | | - return (x0, y0, x1, y1) |
| 809 | + return frame_bbox(frame) |
870 | 810 |
|
871 | 811 | @staticmethod |
872 | 812 | def _bbox_union(a: tuple[int, int, int, int], b: tuple[int, int, int, int]) -> tuple[int, int, int, int]: |
873 | | - if a[2] <= a[0] or a[3] <= a[1]: |
874 | | - return b |
875 | | - if b[2] <= b[0] or b[3] <= b[1]: |
876 | | - return a |
877 | | - return (min(a[0], b[0]), min(a[1], b[1]), max(a[2], b[2]), max(a[3], b[3])) |
| 813 | + return bbox_union(a, b) |
878 | 814 |
|
879 | 815 | @staticmethod |
880 | 816 | def _bbox_intersection(a: tuple[int, int, int, int], b: tuple[int, int, int, int]) -> tuple[int, int, int, int]: |
881 | | - x0 = max(a[0], b[0]) |
882 | | - y0 = max(a[1], b[1]) |
883 | | - x1 = min(a[2], b[2]) |
884 | | - y1 = min(a[3], b[3]) |
885 | | - if x1 <= x0 or y1 <= y0: |
886 | | - return (0, 0, 0, 0) |
887 | | - return (x0, y0, x1, y1) |
| 817 | + return bbox_intersection(a, b) |
888 | 818 |
|
889 | 819 | @staticmethod |
890 | 820 | def _clip_frame_to_bbox(frame: np.ndarray, bbox: tuple[int, int, int, int]) -> np.ndarray: |
891 | | - out = np.asarray(frame).copy() |
892 | | - h, w = out.shape[:2] |
893 | | - x0 = max(0, min(w, int(bbox[0]))) |
894 | | - y0 = max(0, min(h, int(bbox[1]))) |
895 | | - x1 = max(0, min(w, int(bbox[2]))) |
896 | | - y1 = max(0, min(h, int(bbox[3]))) |
897 | | - if x1 <= x0 or y1 <= y0: |
898 | | - return np.zeros_like(out) |
899 | | - |
900 | | - mask = np.zeros((h, w), dtype=bool) |
901 | | - mask[y0:y1, x0:x1] = True |
902 | | - if out.ndim == 2: |
903 | | - out[~mask] = 0 |
904 | | - else: |
905 | | - out[~mask, ...] = 0 |
906 | | - return out |
| 821 | + return clip_frame_to_bbox(frame, bbox) |
907 | 822 |
|
908 | 823 | @staticmethod |
909 | 824 | def _to_u8_frame(src: np.ndarray) -> np.ndarray: |
|
0 commit comments