Breast Masking
왜 마스킹이 먼저인가
대부분의 mammography 전처리는 "유방 영역에만 적용"이 전제다. 배경(공기·라벨·검은 직사각형) 픽셀을 함께 처리하면 다음과 같은 일이 벌어진다.
- 히스토그램 균등화·매칭이 배경의 큰 검은 영역에 끌려가 유방 콘트라스트가 사라진다
- CLAHE 타일 중 일부가 거의 균일한 배경만 보고 가짜 노이즈를 증폭한다
- 평가 지표(SSIM/PSNR)가 배경 일치에 의해 부풀려진다
따라서 다른 모든 전처리에 앞서 유방 영역 마스크(binary mask) 를 만들어야 한다. 마스크의 정확도는 곧 파이프라인 전체 품질의 상한선이다.
1단계 — Otsu 이진화
가장 단순한 방법은 Otsu의 임계값 자동 결정이다. 히스토그램이 "배경(어두움) vs 유방(밝음)" 두 봉우리로 나뉜다는 가정 아래, 클래스 간 분산을 최대화하는 임계값을 찾는다.
| otsu_mask.py |
|---|
| import cv2
import numpy as np
def otsu_mask(img_u8: np.ndarray) -> np.ndarray:
_, binary = cv2.threshold(
img_u8, 0, 255,
cv2.THRESH_BINARY + cv2.THRESH_OTSU
)
# 형태학적 닫힘으로 작은 구멍 메우기
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (20, 20))
cleaned = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, k)
cleaned = cv2.morphologyEx(cleaned, cv2.MORPH_OPEN, k)
# 가장 큰 연결 컴포넌트 = 유방
n, lab, st, _ = cv2.connectedComponentsWithStats(cleaned)
if n <= 1:
return np.zeros_like(cleaned)
largest = 1 + np.argmax(st[1:, cv2.CC_STAT_AREA])
return (lab == largest).astype(np.uint8)
|
한계
- RAW에서 배경이 밝고 유방이 어두운 경우 부등호를 뒤집어야 한다 (
THRESH_BINARY_INV).
- 히스토그램이 단봉(unimodal) 인 일부 디바이스/검사에서는 임계값이 엉뚱한 위치에 잡힌다.
2단계 — Multi-Otsu (3-class)
유방 내부에도 지방 조직과 선조직이 섞여 있다. 이를 한 클래스로 묶는 2-class Otsu는 치밀유방에서 마스크 경계가 흔들린다.
skimage.filters.threshold_multiotsu는 두 개의 임계값으로 이미지를 배경 / 지방조직 / 선조직+병변 3-class로 분리한다.
| multi_otsu_mask.py |
|---|
| from skimage.filters import threshold_multiotsu
import numpy as np
def multi_otsu_mask(img: np.ndarray):
t = threshold_multiotsu(img, classes=3)
breast_mask = img > t[0] # 배경 제외 전체 유방
dense_mask = img > t[1] # 선조직 + 병변 영역
return breast_mask.astype(np.uint8), dense_mask.astype(np.uint8)
|
breast_mask: 일반 전처리·평가 마스크
dense_mask: 밀도 분류·병변 후보 영역 좁히기에 활용
3단계 — 적응형 마스킹 (백분위 + 그래디언트 + Convex Hull)
Otsu가 흔드릴 때를 대비한 폴백. 단일 임계값에 매달리지 않고 세 가지 단서를 결합한다.
| adaptive_mask.py |
|---|
| import cv2
import numpy as np
def adaptive_breast_mask(img: np.ndarray) -> np.ndarray:
# 1) 백분위 기반 임계값 (RAW 분포 적응)
p_low, p_high = np.percentile(img, [5, 95])
thresh = p_low + (p_high - p_low) * 0.3
binary = (img < thresh).astype(np.uint8)
# 2) 그래디언트로 에지 단서 확보 (참고만, 결합은 선택적)
grad = cv2.Sobel(img.astype(np.float32), cv2.CV_32F, 1, 0)
# 3) 형태학으로 안정화
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (30, 30))
binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, k)
# 4) 가장 큰 컨투어의 볼록 껍질 → 매끄러운 윤곽
cnts, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
if not cnts:
return binary
hull = cv2.convexHull(max(cnts, key=cv2.contourArea))
mask = np.zeros_like(binary)
cv2.fillPoly(mask, [hull], 1)
return mask
|
볼록 껍질은 유방 윤곽의 작은 들쭉날쭉함을 흡수해 후속 처리(특히 패치 추출)의 경계를 안정적으로 만든다. 실측에서 Otsu 단독 9.7~98% 변동이 안정적인 85% 이상으로 좁혀졌다.
RAW 직접 분할 — 히스토그램 밸리 임계값
RAW→DCM 복원처럼 윈도잉 전 16비트 RAW를 직접 다룰 때는, RAW 히스토그램이 "유방(낮은 강도) vs 배경(높은 강도)"으로 갈리는 밸리(valley) 를 임계값으로 쓰면 단순하고 안정적이다. 배경이 밝은 RAW에서는 임계값보다 작은 값이 유방이다.
| histogram_valley.py |
|---|
| def find_breast_threshold(raw_img, search_low=10000, search_high=30000, n_bins=1000):
hist, bins = np.histogram(raw_img.ravel(), bins=n_bins)
centers = (bins[:-1] + bins[1:]) / 2
rng = (centers > search_low) & (centers < search_high) # 두 봉우리 사이만 탐색
valley = np.argmin(hist[rng]) # 골짜기 = 임계값
return centers[rng][valley]
mask = raw_img < find_breast_threshold(raw_img) # 배경이 밝은 경우
|
이후 형태학적 정리 + 최대 연결요소 + 구멍 메우기는 Otsu 경로와 동일하게 적용한다. 탐색 구간(search_low/high)은 디바이스 강도 분포에 맞춰 조정한다.
평가 — 마스크 겹침 (IoU)
마스크 자체의 품질은 보통 IoU (Intersection over Union) 로 평가한다.
\[
\mathrm{IoU}(A, B) = \frac{|A \cap B|}{|A \cup B|}
\]
| iou.py |
|---|
| def iou(a: np.ndarray, b: np.ndarray) -> float:
a, b = a.astype(bool), b.astype(bool)
inter = np.logical_and(a, b).sum()
union = np.logical_or(a, b).sum()
return float(inter / union) if union else 0.0
|
DCM 참조 마스크가 있는 경우(보통 임상 DICOM의 (0028,1300) BreastImplantPresent와 별도로 자체 생성한 ground truth 마스크 사용) IoU > 0.9를 기준선으로 본다.