diff --git a/.gitignore b/.gitignore index 2641667..1c20eca 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.mp4 +*.csv diff --git a/gui_app.py b/gui_app.py new file mode 100644 index 0000000..d3fd882 --- /dev/null +++ b/gui_app.py @@ -0,0 +1,9 @@ +from tkinter import * + +master = Tk() +w = Scale(master, from_=0, to=42) +w.pack() +w = Scale(master, from_=0, to=200, orient=HORIZONTAL) +w.pack() + +mainloop() diff --git a/lumen_profiler.py b/lumen_profiler.py index 54c8e7e..52d6e35 100644 --- a/lumen_profiler.py +++ b/lumen_profiler.py @@ -1,152 +1,180 @@ import csv +import sys import cv2 import numpy as np -area_ratio = 80 -enable_display = True +class lumen_profiler: + def __init__(self): + self.area_ratio = 0.08 -def on_slider(pos): - global area_ratio - area_ratio = pos + def load_movie(self, filename): + self.frames = [] + cap = cv2.VideoCapture(filename) + while True: + # 画像読み込み + ret, frame = cap.read() + if not ret: + break + self.frames.append(frame) + self.frame_count = len(self.frames) + cap.release() + def profiling(self, step=1): + self.results = [] + for idx in range(0, len(self.frames), step): + frame = self.frames[idx] + mask = self.lumen_mask(frame) + circle_level, contour = self.calc_circle_level(mask) + result = { + "idx": idx, + "frame": frame, + "mask": mask, + "circle_level": circle_level, + "contour": contour, + "ratio": self.area_ratio, + } + self.results.append(result) -win_org = "original image" -win_value = "value image" - -cap = cv2.VideoCapture("サンプル動画_気管支鏡.mp4") - -if enable_display: - cv2.namedWindow(win_org, cv2.WINDOW_AUTOSIZE) - cv2.namedWindow(win_value, cv2.WINDOW_AUTOSIZE) - cv2.createTrackbar("area", win_org, area_ratio, 300, on_slider) - -frame_count = 0 -csv_data = [] -while True: - # 画像読み込み - ret, frame = cap.read() - if not ret: - # break - cap.set(cv2.CAP_PROP_POS_FRAMES, 0) - frame_count = 0 - print("rewind") - continue - # 輝度画像生成 - hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) - val_img = hsv[:, :, 2] - # 累積ヒストグラム算出 - hist = cv2.calcHist([val_img], [0], None, [256], [0, 256]) - # acc_hist = np.zeros(256, np.float32) - # acc_hist[0] = hist[0] - # しきい値決定 - thres = -1 - sum = 0 - for i in range(0, 256): - sum += hist[i] - # acc_hist[i] = acc_hist[i - 1] + hist[i] - if thres < 0 and sum > (val_img.size * area_ratio / 1000): - thres = i - break - # 気道のマスク生成 - val_img = cv2.GaussianBlur(val_img, (13, 13), 5.0) - mask = cv2.threshold(val_img, thres, 255, cv2.THRESH_BINARY_INV)[1] - # mask = cv2.threshold(val_img, area_ratio, 255, cv2.THRESH_BINARY_INV)[1] - # kernel = np.ones((5, 5), np.uint8) - # mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) - - retval, labels, stats, centroids = cv2.connectedComponentsWithStats(mask) - target_label = -1 - min_dist = 0 - center = [frame.shape[1] / 2, frame.shape[0] / 2] - # print([stats[i, cv2.CC_STAT_AREA] for i in range(retval)]) - if retval > 1: - for i in range(1, retval): - dist = np.linalg.norm(centroids[i] - center, 2) - if stats[i, cv2.CC_STAT_AREA] > 200 and ( - dist < min_dist or target_label < 0 - ): - min_dist = dist - target_label = i - - selected_mask = np.zeros(mask.shape, np.uint8) - selected_mask[labels == target_label] = 255 - - contours, hierarchy = cv2.findContours( - selected_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE - ) - - etime = frame_count * 1.0 / 30.0 - cv2.putText( - frame, - "frame%4d time %.3fs" % (frame_count, etime), - (10, 25), - cv2.FONT_HERSHEY_TRIPLEX, - 0.7, - (255, 0, 0), - 1, - ) - cv2.putText( - frame, - "area ratio=%.1f %%" % (area_ratio / 10), - (10, 50), - cv2.FONT_HERSHEY_TRIPLEX, - 0.7, - (255, 0, 0), - 1, - ) - - if len(contours) > 0: - # for i in range(len(contours)): - cv2.drawContours(frame, contours, 0, (0, 255, 255), 3) - - area = cv2.contourArea(contours[0]) - perimeter = cv2.arcLength(contours[0], True) - if perimeter > 0: - circle_level = 4.0 * np.pi * area / (perimeter * perimeter) - else: - circle_level = 0 - # print(circle_level) + def draw(self, rid): + disp = self.results[rid]["frame"].copy() + etime = self.results[rid]["idx"] * 1.0 / 30.0 cv2.putText( - frame, - "circle level=%.1f %%" % (circle_level * 100), - (10, 75), + disp, + "frame%4d time %.3fs" % (self.results[rid]["idx"], etime), + (10, 25), cv2.FONT_HERSHEY_TRIPLEX, 0.7, (255, 0, 0), 1, ) cv2.putText( - hsv, - "threshold=%d" % (thres), - (10, 30), + disp, + "area ratio=%.1f %%" % (self.area_ratio * 100), + (10, 50), cv2.FONT_HERSHEY_TRIPLEX, 0.7, - (0, 0, 0), + (255, 0, 0), 1, ) + cv2.putText( + disp, + "circle level=%.1f %%" % (self.results[rid]["circle_level"] * 100), + (10, 75), + cv2.FONT_HERSHEY_TRIPLEX, + 0.7, + (255, 0, 0), + 1, + ) + # disp[self.results[rid]["mask"] > 0] = (0, 0, 255) + cv2.drawContours(disp, [self.results[rid]["contour"]], 0, (0, 255, 255), 3) - # frame[labels == target_label] = [0, 0, 0] + return disp - # cv2.imwrite("output/cl_%04d.jpg" % frame_count, frame) + def lumen_mask(self, frame, sigma=5.0, min_area=200): + # 輝度画像生成 + hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) + val_img = hsv[:, :, 2] - csv_data.append([frame_count, etime, area_ratio / 10, thres, circle_level * 100]) + # ヒストグラムから閾値決定 + hist = cv2.calcHist([val_img], [0], None, [256], [0, 256]) + thres = -1 + sum = 0 + for i in range(0, 256): + sum += hist[i] + if thres < 0 and sum > (val_img.size * self.area_ratio): + thres = i + break + # 気道のマスク生成 + val_img = cv2.GaussianBlur(val_img, (13, 13), sigma) + mask = cv2.threshold(val_img, thres, 255, cv2.THRESH_BINARY_INV)[1] - if enable_display: - cv2.imshow(win_org, frame) - cv2.imshow(win_value, hsv[:, :, 2]) + # 連結部の解析 + retval, labels, stats, centroids = cv2.connectedComponentsWithStats(mask) + # 一定面積以上で中心に近い部分を選択 + target_label = 0 + min_dist = -1 + center = [frame.shape[1] / 2, frame.shape[0] / 2] + if retval > 1: + for i in range(1, retval): + dist = np.linalg.norm(centroids[i] - center, 2) + if stats[i, cv2.CC_STAT_AREA] > min_area and ( + dist < min_dist or min_dist < 0 + ): + min_dist = dist + target_label = i + # 選択部分のマスク生成 + selected_mask = np.zeros(mask.shape, np.uint8) + selected_mask[labels == target_label] = 255 + return selected_mask + + def calc_circle_level(self, mask): + contours, hierarchy = cv2.findContours( + mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) + circle_level = 0 + contour = None + if len(contours) > 0: + contour = contours[0] + area = cv2.contourArea(contour) + perimeter = cv2.arcLength(contour, True) + if perimeter > 0: + circle_level = 4.0 * np.pi * area / (perimeter * perimeter) + + return circle_level, contour + + def csv_output(self, filename): + with open(filename, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow( + ["frame", "time(s)", "area ratio(%)", "threshold", "circle level(%)"] + ) + for result in self.results: + etime = result["idx"] * 1.0 / 30.0 + row_data = [ + result["idx"], + etime, + result["ratio"], + 0, + result["circle_level"] * 100, + ] + writer.writerow(row_data) + + +# def func(): +# cv2.putText( +# hsv, +# "threshold=%d" % (thres), +# (10, 30), +# cv2.FONT_HERSHEY_TRIPLEX, +# 0.7, +# (0, 0, 0), +# 1, +# ) + + +if __name__ == "__main__": + args = sys.argv + movie_file = "bs_sample_20250212.mp4" + if len(args) > 1: + movie_file = args[1] + + # 解析 + lp = lumen_profiler() + lp.load_movie(movie_file) + lp.profiling(2) + lp.csv_output("output/analysis.csv") + + # 表示 + rid = 0 + while True: + disp = lp.draw(rid) + cv2.imshow("frame", disp) if cv2.waitKey(30) & 0xFF == 27: break - frame_count += 1 - -# with open("output/analysis.csv", "w", newline="") as f: -# writer = csv.writer(f) -# writer.writerow( -# ["frame", "time(s)", "area ratio(%)", "threshold", "circle level(%)"] -# ) -# writer.writerows(csv_data) - -cap.release() -cv2.destroyAllWindows() + rid += 1 + if rid >= len(lp.results): + rid = 0 + cv2.destroyAllWindows()