Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions lib/features/analysis/data/services/analysis_pipeline.dart
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,14 @@ class AnalysisPipeline {
required int impactFrame,
required int sampleFps,
}) {
final line = arrowLineFromDetections(detectArrows(releaseFrame));
// 실패가 "검출 0개"인지 "검출은 됐는데 선을 못 세움"인지 구분되지 않으면
// 튜닝 방향을 정할 수 없다 — 실기기 1회 실패에 빌드 1회를 쓰게 된다.
final arrows = detectArrows(releaseFrame);
final line = arrowLineFromDetections(arrows);
debugPrint('[Arrow] 입력 ${releaseFrame.width}x${releaseFrame.height}, '
'검출 ${arrows.length}개'
'${arrows.isEmpty ? "" : " ${arrows.map((a) => "(${a.nx.toStringAsFixed(3)},${a.ny.toStringAsFixed(3)})").join(" ")}"}, '
'선 ${line == null ? "실패" : "성립"}');
if (line == null) return null;
return estimateLandmarkSpeedKmh(
track: [for (final s in pixelTrack) (frame: s.frame, p: s.contact)],
Expand Down Expand Up @@ -197,7 +204,16 @@ class AnalysisPipeline {

/// [homography]는 이 영상 전용으로 산출된 호모그래피다(레퍼런스 = 영상 자체이므로
/// drift 개념이 존재하지 않는다 — spec §10 참조).
Future<AnalysisData> run(String videoPath, HomographyMatrix homography) async {
/// [landmarkFrame]은 조준 화살표 검출 전용 고해상도 프레임이다. 분석 프레임은
/// 폭 480 + jpeg q5로 뽑히는데, 화살표는 그 해상도에서 뭉개져 검출이 깨진다.
/// 화살표는 레인에 고정된 마킹이라 어느 프레임을 써도 무방하므로, 호출부가
/// 이미 원본 해상도로 뽑아둔 첫 프레임을 그대로 넘긴다. 없으면 릴리즈 프레임
/// 폴백.
Future<AnalysisData> run(
String videoPath,
HomographyMatrix homography, {
img.Image? landmarkFrame,
}) async {
final extracted = await frameExtractor.extract(videoPath);
final frames = extracted.frames;
if (frames.isEmpty) {
Expand Down Expand Up @@ -346,6 +362,19 @@ class AnalysisPipeline {
debugPrint('[Trajectory] 리본 소스: ${trajectorySource.label} '
'(${finalTrajectory.length}개 단면, 픽셀관측 ${pixelTrack.length}개'
'${pixelRibbon != null ? ", rms ${pixelRibbon.rms.toStringAsFixed(5)}" : ""})');
// 리본이 핀까지 닿는지는 끝점 ny를 핀존 ny와 직접 비교해야 판정된다.
// "선이 BP에서 끝난다"가 연장 실패인지 원근 압축인지 이 한 줄로 갈린다.
if (finalTrajectory.isNotEmpty) {
final head = finalTrajectory.first;
final tail = finalTrajectory.last;
final lastObserved = pixelTrack.isEmpty ? null : pixelTrack.last;
debugPrint('[Trajectory] 리본 f${head.frame}~f${tail.frame} '
'ny ${((head.left.ny + head.right.ny) / 2).toStringAsFixed(3)}'
'→${((tail.left.ny + tail.right.ny) / 2).toStringAsFixed(3)}, '
'마지막 관측 f${lastObserved?.frame} ny '
'${lastObserved?.contact.ny.toStringAsFixed(3) ?? "없음"}, '
'핀존 ny ${pinZone == null ? "없음" : "${pinZone.top.toStringAsFixed(3)}~${pinZone.bottom.toStringAsFixed(3)}"}');
}

final speed = speedEstimator.estimate(
release: release,
Expand All @@ -356,7 +385,7 @@ class AnalysisPipeline {

// 랜드마크 통과-시각 구속 — 조준 화살표가 검출되면 이쪽이 1순위.
final landmarkKmh = estimateLandmarkSpeed(
releaseFrame: frames[releaseFrameIdx],
releaseFrame: landmarkFrame ?? frames[releaseFrameIdx],
pixelTrack: pixelTrack,
impactFrame: impact.frame,
sampleFps: extracted.sampleFps,
Expand Down
22 changes: 21 additions & 1 deletion lib/features/analysis/presentation/pages/analysis_trim_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,10 @@ class _AnalysisTrimPageState extends State<AnalysisTrimPage> {
impactDetector: ImpactDetectorService(pinImpactDetector: PinImpactDetectorService()),
speedEstimator: SpeedEstimatorService(),
);
final analysisData = await pipeline.run(trimmedPath, homography);
// 화살표 검출은 분석 프레임(폭 480, jpeg q5)에선 뭉개진다 — 레인 확인용으로
// 이미 원본 해상도로 뽑아둔 첫 프레임을 그대로 넘긴다.
final analysisData =
await pipeline.run(trimmedPath, homography, landmarkFrame: frame);

// 성공 케이스도 남긴다 — 구속 두 코어 값이 매 투구 쌓여야 표본 1개가
// 아닌 분포로 정확도를 판단할 수 있다. await하지 않는다(결과 화면 지연 방지).
Expand All @@ -192,6 +195,23 @@ class _AnalysisTrimPageState extends State<AnalysisTrimPage> {
'trajectory_source': analysisData.trajectorySource?.name,
'trajectory_fit_rms': analysisData.trajectoryFitRms,
'trajectory_points': analysisData.trajectory.length,
// 리본이 핀까지 닿는지 SQL만으로 판정하기 위한 끝점(깊이축 정규화 좌표).
'trajectory_first_frame': analysisData.trajectory.isEmpty
? null
: analysisData.trajectory.first.frame,
'trajectory_last_frame': analysisData.trajectory.isEmpty
? null
: analysisData.trajectory.last.frame,
'trajectory_first_ny': analysisData.trajectory.isEmpty
? null
: (analysisData.trajectory.first.left.ny +
analysisData.trajectory.first.right.ny) /
2,
'trajectory_last_ny': analysisData.trajectory.isEmpty
? null
: (analysisData.trajectory.last.left.ny +
analysisData.trajectory.last.right.ny) /
2,
'entry_angle_deg': analysisData.entryAngleDeg,
'trim_start_sec': _startSec,
'trim_end_sec': _endSec,
Expand Down
Loading