mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-31 08:09:02 +03:00
Compare commits
2
Commits
dev
...
603d9f7d27
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
603d9f7d27 | ||
|
|
0f36422b35 |
@@ -499,6 +499,10 @@ def _extract_keyframes(
|
|||||||
"""
|
"""
|
||||||
Extract keyframes from recordings at specified timestamps and crop to specified regions.
|
Extract keyframes from recordings at specified timestamps and crop to specified regions.
|
||||||
|
|
||||||
|
This implementation batches work by running multiple ffmpeg snapshot commands
|
||||||
|
concurrently, which significantly reduces total runtime compared to
|
||||||
|
processing each timestamp serially.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ffmpeg_path: Path to ffmpeg binary
|
ffmpeg_path: Path to ffmpeg binary
|
||||||
timestamps: List of timestamp dicts from _select_balanced_timestamps
|
timestamps: List of timestamp dicts from _select_balanced_timestamps
|
||||||
@@ -508,15 +512,21 @@ def _extract_keyframes(
|
|||||||
Returns:
|
Returns:
|
||||||
List of paths to successfully extracted and cropped keyframe images
|
List of paths to successfully extracted and cropped keyframe images
|
||||||
"""
|
"""
|
||||||
keyframe_paths = []
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
for idx, ts_info in enumerate(timestamps):
|
if not timestamps:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Limit the number of concurrent ffmpeg processes so we don't overload the host.
|
||||||
|
max_workers = min(5, len(timestamps))
|
||||||
|
|
||||||
|
def _process_timestamp(idx: int, ts_info: dict) -> tuple[int, str | None]:
|
||||||
camera = ts_info["camera"]
|
camera = ts_info["camera"]
|
||||||
timestamp = ts_info["timestamp"]
|
timestamp = ts_info["timestamp"]
|
||||||
|
|
||||||
if camera not in camera_crops:
|
if camera not in camera_crops:
|
||||||
logger.warning(f"No crop coordinates for camera {camera}")
|
logger.warning(f"No crop coordinates for camera {camera}")
|
||||||
continue
|
return idx, None
|
||||||
|
|
||||||
norm_x1, norm_y1, norm_x2, norm_y2 = camera_crops[camera]
|
norm_x1, norm_y1, norm_x2, norm_y2 = camera_crops[camera]
|
||||||
|
|
||||||
@@ -533,7 +543,7 @@ def _extract_keyframes(
|
|||||||
.get()
|
.get()
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
return idx, None
|
||||||
|
|
||||||
relative_time = timestamp - recording.start_time
|
relative_time = timestamp - recording.start_time
|
||||||
|
|
||||||
@@ -547,38 +557,57 @@ def _extract_keyframes(
|
|||||||
height=None,
|
height=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if image_data:
|
if not image_data:
|
||||||
nparr = np.frombuffer(image_data, np.uint8)
|
return idx, None
|
||||||
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
|
||||||
|
|
||||||
if img is not None:
|
nparr = np.frombuffer(image_data, np.uint8)
|
||||||
height, width = img.shape[:2]
|
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
||||||
|
|
||||||
x1 = int(norm_x1 * width)
|
if img is None:
|
||||||
y1 = int(norm_y1 * height)
|
return idx, None
|
||||||
x2 = int(norm_x2 * width)
|
|
||||||
y2 = int(norm_y2 * height)
|
|
||||||
|
|
||||||
x1_clipped = max(0, min(x1, width))
|
height, width = img.shape[:2]
|
||||||
y1_clipped = max(0, min(y1, height))
|
|
||||||
x2_clipped = max(0, min(x2, width))
|
|
||||||
y2_clipped = max(0, min(y2, height))
|
|
||||||
|
|
||||||
if x2_clipped > x1_clipped and y2_clipped > y1_clipped:
|
x1 = int(norm_x1 * width)
|
||||||
cropped = img[y1_clipped:y2_clipped, x1_clipped:x2_clipped]
|
y1 = int(norm_y1 * height)
|
||||||
resized = cv2.resize(cropped, (224, 224))
|
x2 = int(norm_x2 * width)
|
||||||
|
y2 = int(norm_y2 * height)
|
||||||
|
|
||||||
output_path = os.path.join(output_dir, f"frame_{idx:04d}.jpg")
|
x1_clipped = max(0, min(x1, width))
|
||||||
cv2.imwrite(output_path, resized)
|
y1_clipped = max(0, min(y1, height))
|
||||||
keyframe_paths.append(output_path)
|
x2_clipped = max(0, min(x2, width))
|
||||||
|
y2_clipped = max(0, min(y2, height))
|
||||||
|
|
||||||
|
if x2_clipped <= x1_clipped or y2_clipped <= y1_clipped:
|
||||||
|
return idx, None
|
||||||
|
|
||||||
|
cropped = img[y1_clipped:y2_clipped, x1_clipped:x2_clipped]
|
||||||
|
resized = cv2.resize(cropped, (224, 224))
|
||||||
|
|
||||||
|
output_path = os.path.join(output_dir, f"frame_{idx:04d}.jpg")
|
||||||
|
cv2.imwrite(output_path, resized)
|
||||||
|
return idx, output_path
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Failed to extract frame from {recording.path} at {relative_time}s: {e}"
|
f"Failed to extract frame from {recording.path} at {relative_time}s: {e}"
|
||||||
)
|
)
|
||||||
continue
|
return idx, None
|
||||||
|
|
||||||
return keyframe_paths
|
keyframes_with_index: list[tuple[int, str]] = []
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
|
future_to_idx = {
|
||||||
|
executor.submit(_process_timestamp, idx, ts_info): idx
|
||||||
|
for idx, ts_info in enumerate(timestamps)
|
||||||
|
}
|
||||||
|
|
||||||
|
for future in as_completed(future_to_idx):
|
||||||
|
_, path = future.result()
|
||||||
|
if path:
|
||||||
|
keyframes_with_index.append((future_to_idx[future], path))
|
||||||
|
|
||||||
|
keyframes_with_index.sort(key=lambda item: item[0])
|
||||||
|
return [path for _, path in keyframes_with_index]
|
||||||
|
|
||||||
|
|
||||||
def _select_distinct_images(
|
def _select_distinct_images(
|
||||||
|
|||||||
+13
-7
@@ -14,6 +14,7 @@ import ProtectedRoute from "@/components/auth/ProtectedRoute";
|
|||||||
import { AuthProvider } from "@/context/auth-context";
|
import { AuthProvider } from "@/context/auth-context";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FrigateConfig } from "./types/frigateConfig";
|
import { FrigateConfig } from "./types/frigateConfig";
|
||||||
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
|
|
||||||
const Live = lazy(() => import("@/pages/Live"));
|
const Live = lazy(() => import("@/pages/Live"));
|
||||||
const Events = lazy(() => import("@/pages/Events"));
|
const Events = lazy(() => import("@/pages/Events"));
|
||||||
@@ -50,6 +51,13 @@ function DefaultAppView() {
|
|||||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Compute required roles for main routes, ensuring we have config first
|
||||||
|
// to prevent race condition where custom roles are temporarily unavailable
|
||||||
|
const mainRouteRoles = config?.auth?.roles
|
||||||
|
? Object.keys(config.auth.roles)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="size-full overflow-hidden">
|
<div className="size-full overflow-hidden">
|
||||||
{isDesktop && <Sidebar />}
|
{isDesktop && <Sidebar />}
|
||||||
@@ -68,13 +76,11 @@ function DefaultAppView() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute
|
mainRouteRoles ? (
|
||||||
requiredRoles={
|
<ProtectedRoute requiredRoles={mainRouteRoles} />
|
||||||
config?.auth.roles
|
) : (
|
||||||
? Object.keys(config.auth.roles)
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
: ["admin", "viewer"]
|
)
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Route index element={<Live />} />
|
<Route index element={<Live />} />
|
||||||
|
|||||||
Reference in New Issue
Block a user