| from pathlib import Path |
| import sys |
| import time |
| from time import perf_counter |
| import argparse |
| from loguru import logger |
| import os |
|
|
| from predict import Model |
|
|
| from datetime import datetime |
| from scipy import signal |
| import plotly.graph_objects as go |
| import numpy as np |
| import io |
| from PIL import Image |
|
|
| import cv2 |
| from PySide6.QtCore import Qt, QThread, Signal, Slot |
| from PySide6.QtGui import QImage, QPixmap |
| from PySide6.QtWidgets import ( |
| QApplication, |
| QHBoxLayout, |
| QLabel, |
| QMainWindow, |
| QPushButton, |
| QSizePolicy, |
| QVBoxLayout, |
| QWidget, |
| ) |
|
|
| |
| import matplotlib.pyplot as plt |
| import ctypes |
| from ctypes import * |
|
|
| |
| video_w = 1280 |
| video_h = 720 |
|
|
|
|
| |
| class Telemed: |
| def __init__(self): |
| |
|
|
| |
| |
| |
| w = 640 |
| h = 640 |
|
|
| |
| |
| usgfw2 = cdll.LoadLibrary("./usgfw2wrapper.dll") |
|
|
| |
| usgfw2.on_init() |
| ERR = usgfw2.init_ultrasound_usgfw2() |
|
|
| |
| if ERR == 2: |
| logger.error("Main Usgfw2 library object not created") |
| usgfw2.Close_and_release() |
| sys.exit() |
|
|
| ERR = usgfw2.find_connected_probe() |
|
|
| if ERR != 101: |
| logger.error("Probe not detected") |
| usgfw2.Close_and_release() |
| sys.exit() |
|
|
| ERR = usgfw2.data_view_function() |
|
|
| if ERR < 0: |
| logger.error( |
| "Main ultrasound scanning object for selected probe not created" |
| ) |
| sys.exit() |
|
|
| ERR = usgfw2.mixer_control_function(0, 0, w, h, 0, 0, 0) |
| if ERR < 0: |
| logger.error("B mixer control not returned") |
| sys.exit() |
|
|
| |
| res_X = ctypes.c_float(0.0) |
| res_Y = ctypes.c_float(0.0) |
| usgfw2.get_resolution(ctypes.pointer(res_X), ctypes.pointer(res_Y)) |
|
|
| X_axis = np.zeros(shape=(w)) |
| Y_axis = np.zeros(shape=(h)) |
| if w % 2 == 0: |
| k = 0 |
| for i in range(-w // 2, w // 2 + 1): |
| if i < 0: |
| j = i + 0.5 |
| X_axis[k] = j * res_X.value |
| k = k + 1 |
| else: |
| if i > 0: |
| j = i - 0.5 |
| X_axis[k] = j * res_X.value |
| k = k + 1 |
|
|
| else: |
| for i in range(-w // 2, w // 2): |
| X_axis[i + w / 2 + 1] = i * res_X.value |
|
|
| for i in range(0, h - 1): |
| Y_axis[i] = i * res_Y.value |
|
|
| old_resolution_x = res_X.value |
| old_resolution_y = res_X.value |
|
|
| |
| p_array = (ctypes.c_uint * w * h * 4)() |
|
|
| fig, ax = plt.subplots() |
| usgfw2.return_pixel_values(ctypes.pointer(p_array)) |
| buffer_as_numpy_array = np.frombuffer(p_array, np.uint) |
| reshaped_array = np.reshape(buffer_as_numpy_array, (w, h, 4)) |
|
|
| img = ax.imshow( |
| reshaped_array[:, :, 0:3], |
| cmap="gray", |
| vmin=0, |
| vmax=255, |
| origin="lower", |
| extent=[np.amin(X_axis), np.amax(X_axis), np.amax(Y_axis), np.amin(Y_axis)], |
| ) |
|
|
| |
| self.w = w |
| self.h = h |
|
|
| ( |
| self.usgfw2, |
| self.p_array, |
| self.res_X, |
| self.res_Y, |
| self.old_resolution_x, |
| self.old_resolution_y, |
| self.X_axis, |
| self.Y_axis, |
| self.img, |
| ) = ( |
| usgfw2, |
| p_array, |
| res_X, |
| res_Y, |
| old_resolution_x, |
| old_resolution_y, |
| X_axis, |
| Y_axis, |
| img, |
| ) |
|
|
| |
| def imaging(self): |
| self.usgfw2.return_pixel_values(ctypes.pointer(self.p_array)) |
| buffer_as_numpy_array = np.frombuffer(self.p_array, np.uint) |
| reshaped_array = np.reshape(buffer_as_numpy_array, (self.w, self.h, 4)) |
|
|
| self.usgfw2.get_resolution( |
| ctypes.pointer(self.res_X), ctypes.pointer(self.res_Y) |
| ) |
| if ( |
| self.res_X.value != self.old_resolution_x |
| or self.res_Y.value != self.old_resolution_y |
| ): |
| if self.w % 2 == 0: |
| k = 0 |
| for i in range(-self.w // 2, self.w // 2 + 1): |
| if i < 0: |
| j = i + 0.5 |
| self.X_axis[k] = j * self.res_X.value |
| k = k + 1 |
| else: |
| if i > 0: |
| j = i - 0.5 |
| self.X_axis[k] = j * self.res_X.value |
| k = k + 1 |
| else: |
| for i in range(-self.w // 2, self.w // 2): |
| self.X_axis[i + self.w / 2 + 1] = i * self.res_X.value |
|
|
| for i in range(0, self.h - 1): |
| self.Y_axis[i] = i * self.res_Y.value |
|
|
| self.old_resolution_x = self.res_X.value |
| self.old_resolution_y = self.res_X.value |
|
|
| self.img.set_data(reshaped_array[:, :, 0:3]) |
| self.img.set_extent( |
| [ |
| np.amin(self.X_axis), |
| np.amax(self.X_axis), |
| np.amax(self.Y_axis), |
| np.amin(self.Y_axis), |
| ] |
| ) |
|
|
| |
| img_array = np.asarray(self.img.get_array()) |
| img_array = img_array[::-1, :, ::-1] |
| return img_array |
|
|
|
|
| class Thread(QThread): |
| updateFrame = Signal(QImage) |
|
|
| def __init__(self, parent=None, args=None): |
| QThread.__init__(self, parent) |
| self.status = True |
| self.cap = True |
| self.args = args |
|
|
| |
| if args.video is None: |
| self.telemed = Telemed() |
|
|
| |
| is_async = ( |
| True if self.args.jobs == "auto" or int(self.args.jobs) > 1 else False |
| ) |
| self.model = Model( |
| model_path=self.args.model, |
| imgsz=self.args.img_size, |
| classes=self.args.classes, |
| device=self.args.device, |
| plot_mask=self.args.plot_mask, |
| conf_thres=self.args.conf_thres, |
| is_async=is_async, |
| n_jobs=self.args.jobs, |
| ) |
|
|
| def get_stats_fig(self, aorta_widths, aorta_confs, fig_w, fig_h, ts): |
| title_font_size = 28 |
| body_font_size = 24 |
| img_quality = 100 * np.mean(aorta_confs) |
| avg_width = np.mean(aorta_widths) |
| max_width = np.max(aorta_widths) |
| suggestions = [ |
| "N/A, within normal limit", |
| "Follow up in 5 years", |
| "Make an appointment as soon as possible", |
| ] |
| s = None |
| if avg_width < 3: |
| s = suggestions[0] |
| elif avg_width < 5: |
| s = suggestions[1] |
| else: |
| s = suggestions[2] |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| window_size = 53 |
| if len(aorta_widths) < window_size: |
| window_size = len(aorta_widths) - 1 |
| new_y = signal.savgol_filter(aorta_widths, window_size, 3) |
| |
|
|
| x = np.arange(1, len(aorta_widths) + 1, dtype=int) |
|
|
| fig = go.Figure() |
| fig.add_trace( |
| go.Scatter( |
| x=x, y=aorta_widths, mode="lines", line=dict(color="royalblue", width=1) |
| ) |
| ) |
| fig.add_trace( |
| go.Scatter( |
| x=x, |
| y=new_y, |
| mode="lines", |
| marker=dict( |
| size=3, |
| color="mediumpurple", |
| ), |
| ) |
| ) |
| fig.update_layout( |
| autosize=False, |
| width=fig_w, |
| height=fig_h, |
| margin=dict(l=50, r=50, b=50, t=400, pad=4), |
| paper_bgcolor="LightSteelBlue", |
| showlegend=False, |
| ) |
| fig.add_annotation( |
| text=f"max={max_width:.2f} cm", |
| x=np.argmax(aorta_widths), |
| y=np.max(aorta_widths), |
| xref="x", |
| yref="y", |
| showarrow=True, |
| font=dict(color="#ffffff"), |
| arrowhead=2, |
| arrowsize=1, |
| arrowwidth=2, |
| borderpad=4, |
| bgcolor="#ff7f0e", |
| opacity=0.8, |
| ) |
| fig.add_annotation( |
| text=f"smoothed max={np.max(new_y):.2f} cm", |
| x=np.argmax(new_y), |
| y=np.max(new_y), |
| xref="x", |
| yref="y", |
| showarrow=True, |
| font=dict(color="#ffffff"), |
| arrowhead=2, |
| arrowsize=1, |
| arrowwidth=2, |
| ax=-100, |
| ay=-50, |
| borderpad=4, |
| bgcolor="#ff7f0e", |
| opacity=0.8, |
| ) |
| fig.add_annotation( |
| text="<b>Report of Abdominal Aorta Examination</b>", |
| xref="paper", |
| yref="paper", |
| x=0.5, |
| y=2.3, |
| showarrow=False, |
| font=dict(size=title_font_size), |
| ) |
| fig.add_annotation( |
| text=f"Image acquisition quality: {img_quality:.0f}%", |
| xref="paper", |
| yref="paper", |
| x=0, |
| y=2.0, |
| showarrow=False, |
| font=dict(size=body_font_size), |
| ) |
| fig.add_annotation( |
| text=f"Aorta Maximal Width: {max_width:.2f} cm", |
| xref="paper", |
| yref="paper", |
| x=0, |
| y=1.8, |
| showarrow=False, |
| font=dict(size=body_font_size), |
| ) |
| fig.add_annotation( |
| text=f"Aorta Maximal Width (Smoothed): {np.max(new_y):.2f} cm", |
| xref="paper", |
| yref="paper", |
| x=0, |
| y=1.6, |
| showarrow=False, |
| font=dict(size=body_font_size), |
| ) |
| fig.add_annotation( |
| text=f"Average: {avg_width:.2f} cm", |
| xref="paper", |
| yref="paper", |
| x=0, |
| y=1.4, |
| showarrow=False, |
| font=dict(size=body_font_size), |
| ) |
| fig.add_annotation( |
| text=f"Suggestion: {s}", |
| xref="paper", |
| yref="paper", |
| x=0, |
| y=1.2, |
| showarrow=False, |
| font=dict(size=body_font_size), |
| ) |
| fig.add_annotation( |
| text=f"Generated at {ts}", |
| xref="paper", |
| yref="paper", |
| x=1, |
| y=1, |
| showarrow=False, |
| ) |
| return fig |
|
|
| def run(self): |
| one_cm_in_pixels = 48 |
| aorta_cm_thre1 = 3 |
| aorta_cm_thre2 = 5 |
| black = (0, 0, 0) |
| white = (255, 255, 255) |
| red = (0, 0, 255) |
| green = (0, 255, 0) |
|
|
| aorta_widths_stats = [0, 0, 0] |
| aorta_widths = [] |
| aorta_confs = [] |
|
|
| expected_fps = None |
| frame_count = None |
| frame_w = None |
| frame_h = None |
| if self.args.video: |
| self.cap = cv2.VideoCapture(self.args.video) |
| expected_fps = self.cap.get(cv2.CAP_PROP_FPS) |
| secs_per_frame = 1 / expected_fps |
| frame_w, frame_h = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int( |
| self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT) |
| ) |
| frame_count = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) |
| logger.info(f"Video source FPS: {expected_fps}") |
| logger.info(f"Milliseconds per frame: {secs_per_frame}") |
| logger.info(f"Video source resolution (WxH): {frame_w}x{frame_h}") |
| logger.info(f"Video source frame count: {frame_count}") |
| assert frame_count > 0, "No frame found" |
|
|
| n_read_frames = 0 |
| next_frame_to_infer = 0 |
| next_frame_to_show = 0 |
| n_repeat_failure = 0 |
| is_last_failed = False |
| start_time = perf_counter() |
| while self.status: |
| frame = None |
|
|
| |
| if n_repeat_failure > 30: |
| break |
|
|
| |
| color_frame, others, results, xyxy, conf = None, None, None, None, None |
| if self.model.is_async: |
| results = self.model.get_result(next_frame_to_show) |
| if results: |
| color_frame, others = results |
| xyxy, conf, _ = others |
| next_frame_to_show += 1 |
|
|
| if self.model.is_async and self.model.is_free_to_infer_async(): |
| if self.args.video: |
| ret, frame = self.cap.read() |
|
|
| if not ret: |
| n_repeat_failure += 1 if is_last_failed else 0 |
| is_last_failed = True |
| continue |
| else: |
| |
| |
| frame = self.telemed.imaging() |
|
|
| n_read_frames += 1 |
| self.model.predict_async(frame, next_frame_to_infer) |
| next_frame_to_infer += 1 |
| elif not self.model.is_async: |
| if self.args.video: |
| ret, frame = self.cap.read() |
| if not ret: |
| n_repeat_failure += 1 if is_last_failed else 0 |
| is_last_failed = True |
| continue |
| else: |
| |
| |
| frame = self.telemed.imaging() |
|
|
| n_read_frames += 1 |
| results = self.model.predict(frame) |
| color_frame, others = results |
| xyxy, conf, _ = others |
| if results is None: |
| continue |
|
|
| is_last_failed = False |
|
|
| |
| aorta_width_in_cm = 0 |
| is_found = xyxy is not None |
| is_in_box = False |
| is_too_left, is_too_right = False, False |
| w, h = color_frame.shape[1], color_frame.shape[0] |
| box_w = int(w * 0.1) |
| box_h = int(h * 0.5) |
| box_top_left = (w // 2 - box_w // 2, h // 4) |
| box_bottom_right = (w // 2 + box_w // 2, h // 4 + box_h) |
| if xyxy is not None: |
| x1, y1, x2, y2 = xyxy |
|
|
| |
| aorta_width_in_cm = (x2 - x1) / one_cm_in_pixels |
| aorta_widths.append(aorta_width_in_cm) |
| aorta_confs.append(conf) |
| if aorta_width_in_cm < aorta_cm_thre1: |
| aorta_widths_stats[0] += 1 |
| elif aorta_width_in_cm < aorta_cm_thre2: |
| aorta_widths_stats[1] += 1 |
| else: |
| aorta_widths_stats[2] += 1 |
|
|
| |
| if ( |
| x1 > box_top_left[0] |
| and x2 < box_bottom_right[0] |
| and y1 > box_top_left[1] |
| and y2 < box_bottom_right[1] |
| ): |
| is_in_box = True |
| is_too_right = x2 > box_bottom_right[0] |
| is_too_left = x1 < box_top_left[0] |
|
|
| |
| box_color = green if is_in_box else red |
| color_frame = cv2.rectangle( |
| color_frame, box_top_left, box_bottom_right, box_color, 2 |
| ) |
| assert not ( |
| is_too_left and is_too_right |
| ), "Cannot be both too left and too right" |
| if is_too_left: |
| start_p = (box_top_left[0], int(h * 0.9)) |
| end_p = (box_bottom_right[0], int(h * 0.9)) |
| cv2.arrowedLine(color_frame, start_p, end_p, red, 3) |
| if is_too_right: |
| start_p = (box_bottom_right[0], int(h * 0.9)) |
| end_p = (box_top_left[0], int(h * 0.9)) |
| cv2.arrowedLine(color_frame, start_p, end_p, red, 3) |
| if is_in_box: |
| cv2.putText( |
| color_frame, |
| "GOOD", |
| (box_top_left[0], int(h * 0.9)), |
| cv2.FONT_HERSHEY_SIMPLEX, |
| 1, |
| green, |
| 3, |
| ) |
|
|
| |
| text = ( |
| f"Aorta width: {aorta_width_in_cm:.2f} cm" |
| if is_found |
| else "Aorta width: N/A" |
| ) |
| cv2.putText( |
| color_frame, text, (50, 90), cv2.FONT_HERSHEY_SIMPLEX, 1, white, 3 |
| ) |
|
|
| |
| fps = None |
| if n_read_frames > 0: |
| fps = n_read_frames / (perf_counter() - start_time) |
|
|
| |
| if self.args.sync: |
| while fps > expected_fps: |
| time.sleep(0.001) |
| fps = n_read_frames / (perf_counter() - start_time) |
|
|
| cv2.putText( |
| color_frame, |
| f"FPS: {fps:.2f}", |
| (50, 30), |
| cv2.FONT_HERSHEY_SIMPLEX, |
| 1, |
| white, |
| 3, |
| ) |
| |
|
|
| |
| h, w, ch = color_frame.shape |
| img = QImage(color_frame.data, w, h, ch * w, QImage.Format_BGR888) |
| scaled_img = img.scaled(video_w, video_h, Qt.KeepAspectRatio) |
|
|
| |
| self.updateFrame.emit(scaled_img) |
|
|
| if self.args.video: |
| progress = 100 * n_read_frames / frame_count |
| fps_msg = f", FPS: {fps:.2f}" if fps is not None else "" |
| print( |
| f"Processed {n_read_frames}/{frame_count} ({progress:.2f}%) frames" |
| + fps_msg, |
| end="\r" if n_read_frames < frame_count else os.linesep, |
| ) |
| if n_read_frames >= frame_count: |
| logger.info("Finished processing video") |
| break |
| if self.args.video: |
| self.cap.release() |
|
|
| if not self.status: |
| logger.info("Stopped by user") |
| return |
|
|
| |
| |
| |
| im = np.zeros((frame_h, frame_w, 3), np.uint8) |
| cv2.putText( |
| im, |
| "Generating report for you...", |
| (frame_w // 3, frame_h // 2), |
| cv2.FONT_HERSHEY_SIMPLEX, |
| 1, |
| white, |
| 3, |
| ) |
| img = QImage(im.data, frame_w, frame_h, ch * w, QImage.Format_BGR888) |
| scaled_img = img.scaled(video_w, video_h, Qt.KeepAspectRatio) |
| self.updateFrame.emit(scaled_img) |
| time.sleep(3) |
|
|
| |
| now_t = datetime.now() |
| ts1 = now_t.strftime("%Y%m%d_%H%M%S") |
| ts2 = now_t.strftime("%Y/%m/%d %I:%M:%S") |
| Path("runs").mkdir(parents=True, exist_ok=True) |
| |
| fig_out_p = f"runs/aorta_report_{ts1}.jpeg" |
| fig = self.get_stats_fig(aorta_widths, aorta_confs, video_w, video_h, ts2) |
|
|
| |
| |
| fig.write_image(fig_out_p) |
|
|
| logger.info(f"Saved aorta report: {fig_out_p}") |
| img_bytes = fig.to_image(format="jpg", width=video_w, height=video_h) |
| line_chart = np.array(Image.open(io.BytesIO(img_bytes))) |
| line_chart = cv2.cvtColor(line_chart, cv2.COLOR_RGB2BGR) |
| h, w, ch = line_chart.shape |
| img = QImage(line_chart.data, video_w, video_h, ch * w, QImage.Format_BGR888) |
| scaled_img = img.scaled(w, h, Qt.KeepAspectRatio) |
| |
| self.updateFrame.emit(scaled_img) |
| time.sleep(5) |
|
|
| |
| while self.status and not self.args.exit_on_end: |
| time.sleep(0.1) |
|
|
|
|
| class Window(QMainWindow): |
| def __init__(self, args=None): |
| super().__init__() |
| |
| self.setWindowTitle("Demo") |
| self.setGeometry(0, 0, 800, 500) |
|
|
| |
| self.label = QLabel(self) |
| |
| self.label.setFixedSize(video_w, video_h) |
|
|
| |
| self.th = Thread(self, args) |
| self.th.finished.connect(self.close) |
| self.th.updateFrame.connect(self.setImage) |
|
|
| |
| buttons_layout = QHBoxLayout() |
| self.button1 = QPushButton("Start") |
| self.button2 = QPushButton("Stop/Close") |
| self.button1.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) |
| self.button2.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) |
| buttons_layout.addWidget(self.button2) |
| buttons_layout.addWidget(self.button1) |
|
|
| right_layout = QHBoxLayout() |
| |
| right_layout.addLayout(buttons_layout, 1) |
|
|
| |
| layout = QVBoxLayout() |
| layout.addWidget(self.label) |
| layout.addLayout(right_layout) |
|
|
| |
| widget = QWidget(self) |
| widget.setLayout(layout) |
| self.setCentralWidget(widget) |
|
|
| |
| self.button1.clicked.connect(self.start) |
| self.button2.clicked.connect(self.kill_thread) |
| self.button2.setEnabled(False) |
|
|
| if args.start_on_open: |
| |
| self.start() |
|
|
| @Slot() |
| def kill_thread(self): |
| logger.info("Finishing...") |
| self.th.status = False |
| time.sleep(1) |
| |
| self.button2.setEnabled(False) |
| self.button1.setEnabled(True) |
| cv2.destroyAllWindows() |
| self.th.exit() |
| |
| time.sleep(1) |
|
|
| @Slot() |
| def start(self): |
| logger.info("Starting...") |
| self.button2.setEnabled(True) |
| self.button1.setEnabled(False) |
| self.th.start() |
| logger.info("Thread started") |
|
|
| @Slot(QImage) |
| def setImage(self, image): |
| self.label.setPixmap(QPixmap.fromImage(image)) |
|
|
|
|
| if __name__ == "__main__": |
| |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| "--video", |
| type=str, |
| default=None, |
| help="path to video file, if None (default) would read from telemed", |
| ) |
| parser.add_argument( |
| "--model", |
| type=str, |
| default="best_openvino_model/best.xml", |
| help="path to model file", |
| ) |
| parser.add_argument("--img-size", type=int, default=640, help="image size") |
| parser.add_argument( |
| "--classes", nargs="+", type=int, default=[0], help="filter by class" |
| ) |
| parser.add_argument("--device", type=str, default="CPU", help="device to use") |
| parser.add_argument("--sync", action="store_true", help="sync video FPS") |
| parser.add_argument("--plot-mask", action="store_true", help="plot mask") |
| parser.add_argument("--conf-thres", type=float, default=0.25, help="conf thresh") |
| parser.add_argument("--jobs", type=str, default=1, help="num of jobs, async if > 1") |
| parser.add_argument("--start-on-open", action="store_true", help="start on open") |
| parser.add_argument("--exit-on-end", action="store_true", help="exit if video ends") |
| args = parser.parse_args() |
| assert ( |
| args.jobs == "auto" or int(args.jobs) > 0 |
| ), f"--jobs must be > 0 or auto, got {args.jobs}" |
| if args.video: |
| assert Path(args.video).exists(), f"Video file {args.video} not found" |
| assert Path(args.model).exists(), f"Model file {args.model} not found" |
| app = QApplication() |
| w = Window(args) |
| w.show() |
| sys.exit(app.exec()) |
|
|