Git: PySceneDetect/scenedetect/detectors/content_detector.py
를 알아보며 cv2가 어떻게 사용되는지를 간단하게 알아보자
def _mean_pixel_distance(left: numpy.ndarray, right: numpy.ndarray) -> float:
"""Return the mean average distance in pixel values between `left` and `right`.
Both `left and `right` should be 2 dimensional 8-bit images of the same shape.
"""
assert len(left.shape) == 2 and len(right.shape) == 2
assert left.shape == right.shape
num_pixels: float = float(left.shape[0] * left.shape[1])
return numpy.sum(numpy.abs(left.astype(numpy.int32) - right.astype(numpy.int32))) / num_pixels
두 이미지 간(두 프레임 간) 픽셀 값 차이를 평균을 반환하는 함수
def _estimated_kernel_size(frame_width: int, frame_height: int) -> int:
"""Estimate kernel size based on video resolution."""
# TODO: This equation is based on manual estimation from a few videos.
# Create a more comprehensive test suite to optimize against.
size: int = 4 + round(math.sqrt(frame_width * frame_height) / 192)
if size % 2 == 0:
size += 1
return size
filter(kernel)의 크기를 해상도에 맞기 설정
class Components(ty.NamedTuple):
"""Components that make up a frame's score, and their default values."""
delta_hue: float = 1.0
"""Difference between pixel hue values of adjacent frames."""
delta_sat: float = 1.0
"""Difference between pixel saturation values of adjacent frames."""
delta_lum: float = 1.0
"""Difference between pixel luma (brightness) values of adjacent frames."""
delta_edges: float = 0.0
"""Difference between calculated edges of adjacent frames.
ContentDetector의 경우에는 BGR값을 HSV로 변환해서 작동함
def _calculate_frame_score(self, timecode: FrameTimecode, frame_img: numpy.ndarray) -> float:
"""Calculate score representing relative amount of motion in `frame_img` compared to
the last time the function was called (returns 0.0 on the first call)."""
# TODO: Add option to enable motion estimation before calculating score components.
# TODO: Investigate methods of performing cheaper alternatives, e.g. shifting or resizing
# the frame to simulate camera movement, using optical flow, etc...
# Convert image into HSV colorspace.
hue, sat, lum = cv2.split(cv2.cvtColor(frame_img, cv2.COLOR_BGR2HSV))
# Performance: Only calculate edges if we have to.
calculate_edges: bool = (self._weights.delta_edges > 0.0) or self.stats_manager is not None
edges = self._detect_edges(lum) if calculate_edges else None
if self._last_frame is None:
# Need another frame to compare with for score calculation.
self._last_frame = ContentDetector._FrameData(hue, sat, lum, edges)
return 0.0
score_components = ContentDetector.Components(
delta_hue=_mean_pixel_distance(hue, self._last_frame.hue),
delta_sat=_mean_pixel_distance(sat, self._last_frame.sat),
delta_lum=_mean_pixel_distance(lum, self._last_frame.lum),
delta_edges=(
0.0 if edges is None else _mean_pixel_distance(edges, self._last_frame.edges)
),
)
frame_score: float = sum(
component * weight
for (component, weight) in zip(score_components, self._weights, strict=True)
) / sum(abs(weight) for weight in self._weights)
# Record components and frame score if needed for analysis.
if self.stats_manager is not None:
metrics = {self.FRAME_SCORE_KEY: frame_score}
metrics.update(score_components._asdict())
self.stats_manager.set_metrics(timecode, metrics)
# Store all data required to calculate the next frame's score.
self._last_frame = ContentDetector._FrameData(hue, sat, lum, edges)
return frame_score
이전 프레임과 비교해서 얼마나 움직임이 있었는지를 계산하는 함수
def process_frame(
self, timecode: FrameTimecode, frame_img: numpy.ndarray
) -> ty.List[FrameTimecode]:
"""Process the next frame. `frame_num` is assumed to be sequential.
Args:
frame_num (int): Frame number of frame that is being passed. Can start from any value
but must remain sequential.
frame_img (numpy.ndarray or None): Video frame corresponding to `frame_img`.
Returns:
ty.List[int]: List of frames where scene cuts have been detected. There may be 0
or more frames in the list, and not necessarily the same as frame_num.
"""
self._frame_score = self._calculate_frame_score(timecode, frame_img)
if self._frame_score is None:
return []
above_threshold: bool = self._frame_score >= self._threshold
return self._flash_filter.filter(timecode=timecode, above_threshold=above_threshold)
계산한 score가 threshold를 넘기는지 확인 + filter 처리
def _detect_edges(self, lum: numpy.ndarray) -> numpy.ndarray:
"""Detect edges using the luma channel of a frame.
Arguments:
lum: 2D 8-bit image representing the luma channel of a frame.
Returns:
2D 8-bit image of the same size as the input, where pixels with values of 255
represent edges, and all other pixels are 0.
"""
# Initialize kernel.
if self._kernel is None:
kernel_size = _estimated_kernel_size(lum.shape[1], lum.shape[0])
self._kernel = numpy.ones((kernel_size, kernel_size), numpy.uint8)
# Estimate levels for thresholding.
# TODO: Add config file entries for sigma, aperture/kernel size, etc.
sigma: float = 1.0 / 3.0
median = numpy.median(lum)
low = int(max(0, (1.0 - sigma) * median))
high = int(min(255, (1.0 + sigma) * median))
# Calculate edges using Canny algorithm, and reduce noise by dilating the edges.
# This increases edge overlap leading to improved robustness against noise and slow
# camera movement. Note that very large kernel sizes can negatively affect accuracy.
edges = cv2.Canny(lum, low, high)
return cv2.dilate(edges, self._kernel)
edge를 별도로 구분하는 이유
코드 설명
Detection에 있어서 HSV > RGB
색 정보만이 아닌 edge 검출을 별도로 이용