Example #1
0
def _retrieve_instance_metadata(args):
    '''Retrieves metadata for an individual Instances and either
    writes it to standard output or to a file on disk.
    '''
    client = DICOMwebClient(args.url,
                            username=args.username,
                            password=args.password,
                            ca_bundle=args.ca_bundle,
                            cert=args.cert)
    metadata = client.retrieve_instance_metadata(args.study_instance_uid,
                                                 args.series_instance_uid,
                                                 args.sop_instance_uid)
    if args.save:
        _save_metadata(metadata, args.output_dir, args.sop_instance_uid,
                       args.prettify, args.dicomize)
    else:
        _print_metadata(metadata, args.prettify, args.dicomize)
Example #2
0
class DICOMWindow(Gtk.Window):
    def __init__(self, title="DICOM Viewer"):

        self.client = DICOMwebClient(
            url='http://dev-demo.sectra.se',
            qido_url_prefix='SectraQidoRs/mrnissuers/all',
            wado_url_prefix='SectraWadoRs/mrnissuers/all',
            stow_url_prefix='SectraStowRs/mrnissuers/all')

        # construct top window
        Gtk.Window.__init__(
            self,
            title=title,
            vexpand=True,
            hexpand=True,
            border_width=0,
        )

        # create top grid
        self.grid = Gtk.Grid(vexpand=True,
                             hexpand=True,
                             column_homogeneous=True,
                             row_homogeneous=True)
        self.add(self.grid)  # add it to parent window

        # STUDIES
        self.study_store = StudyStore(client=self.client)
        self.current_study_filter = None
        self.study_filter = self.study_store.filter_new()
        self.study_filter.set_visible_func(self.study_view_filter_func)
        self.study_view = Gtk.TreeView.new_with_model(self.study_filter)
        self.study_view.connect("row-activated",
                                self.on_study_view_row_activated
                                )  # upon double-click or return press
        self.study_selection = self.study_view.get_selection()
        self.study_selection.set_mode(Gtk.SelectionMode.MULTIPLE)
        self.study_selection.connect("changed",
                                     self.on_study_selection_changed)
        for i, column_title in enumerate([
                "Study UID",
                "Study Name",
                "Person Name",
                "Study Date",
                "Study Time",
        ]):
            if i == 0:
                continue  #skip "Study UID"
            renderer = Gtk.CellRendererText()
            column = Gtk.TreeViewColumn(column_title, renderer, text=i)
            column.set_sort_column_id(i)  #trying to make columns sortable
            self.study_view.append_column(column)
        self.study_view_scroller = Gtk.ScrolledWindow(vexpand=True,
                                                      hexpand=True)
        self.study_view_scroller.add(self.study_view)
        self.grid.attach(self.study_view_scroller,
                         left=0,
                         top=0,
                         width=1,
                         height=1)

        # SERIESES
        self.series_store = SeriesStore(client=self.client)
        self.current_series_filter = None
        self.series_filter = self.series_store.filter_new()
        self.series_filter.set_visible_func(self.series_view_filter_func)
        self.series_view = Gtk.TreeView.new_with_model(self.series_filter)
        self.series_view.connect("row-activated",
                                 self.on_series_view_row_activated
                                 )  # upon double-click or return press
        self.series_selection = self.series_view.get_selection()
        self.series_selection.set_mode(Gtk.SelectionMode.MULTIPLE)
        self.series_selection.connect("changed",
                                      self.on_series_selection_changed)
        for i, column_title in enumerate(["Series UID", "Series Modality"]):
            if i == 0:
                continue  # skip "Series UID"
            renderer = Gtk.CellRendererText()
            column = Gtk.TreeViewColumn(column_title, renderer, text=i)
            self.series_view.append_column(column)
        self.series_view_scroller = Gtk.ScrolledWindow(vexpand=True,
                                                       hexpand=True)
        self.series_view_scroller.add(self.series_view)
        self.grid.attach(self.series_view_scroller,
                         left=1,
                         top=0,
                         width=1,
                         height=1)

        # INSTANCES
        self.instance_store = InstanceStore(client=self.client)
        self.current_instance_filter = None
        self.instance_filter = self.instance_store.filter_new()
        self.instance_filter.set_visible_func(self.instance_view_filter_func)
        self.instance_view = Gtk.TreeView.new_with_model(self.instance_filter)
        self.instance_view.connect("row-activated",
                                   self.on_instance_view_row_activated
                                   )  # upon double-click or return press
        self.instance_selection = self.instance_view.get_selection()
        self.instance_selection.set_mode(Gtk.SelectionMode.MULTIPLE)
        self.instance_selection.connect("changed",
                                        self.on_instance_selection_changed)
        for i, column_title in enumerate(["SOP Instance UID", "SOP Class"]):
            if i == 0:
                continue  # skip "SOP Instance UID"
            renderer = Gtk.CellRendererText()
            column = Gtk.TreeViewColumn(column_title, renderer, text=i)
            self.instance_view.append_column(column)
        self.instance_view_scroller = Gtk.ScrolledWindow(vexpand=True,
                                                         hexpand=True)
        self.instance_view_scroller.add(self.instance_view)
        self.grid.attach(self.instance_view_scroller,
                         left=2,
                         top=0,
                         width=1,
                         height=1)

        self.image_view = Gtk.Image()
        self.image_view_scroller = Gtk.ScrolledWindow(vexpand=True,
                                                      hexpand=True)
        self.image_view_scroller.add(self.image_view)
        self.grid.attach(self.image_view_scroller,
                         left=0,
                         top=1,
                         width=3,
                         height=3)

        self.show_all()

    def on_study_view_row_activated(self, view, row_index, column):
        print("on_study_view_row_activated:", view, " row_index:", row_index)

        # clear in reverse order
        self.clear_current_image_and_pixel_data()
        self.clear_current_instance_store_and_view()
        self.clear_current_series_store_and_view()

        model, treeiter = view.get_selection().get_selected_rows()
        self.study_instance_uid = model[row_index][0]
        self.series_store.update_using_study(
            study_instance_uid=self.study_instance_uid)

    def on_study_selection_changed(self, selection):
        # clear in reverse order
        self.clear_current_image_and_pixel_data()
        self.clear_current_instance_store_and_view()
        self.clear_current_series_store_and_view()
        # print('on_study_selection_changed: seletion:{}, selection_count={}'.format(selection,
        #                                                                            selection.count_selected_rows()))
        # TODO visually present:
        # study_metadata = self.client.retrieve_study_metadata(self.study_instance_uid)

    def on_series_view_row_activated(self, view, row_index, column):
        print("on_series_view_row_activated:", view, " row_index:", row_index)

        # clear in reverse order
        self.clear_current_image_and_pixel_data()
        self.clear_current_instance_store_and_view()

        model, treeiter = view.get_selection().get_selected_rows()
        self.series_instance_uid = model[row_index][0]
        self.instance_store.update_using_study_and_series(
            study_instance_uid=self.study_instance_uid,
            series_instance_uid=self.series_instance_uid)

    def on_series_selection_changed(self, selection):
        '''This is function is called too often so we cannot do any heavy work here.'''
        # print('on_series_selection_changed: seletion:{}, selection_count={}'.format(selection,
        #                                                                            selection.count_selected_rows()))

        # clear in reverse order
        self.clear_current_image_and_pixel_data()
        self.clear_current_instance_store_and_view()

        # TODO visually present:
        # series_metadata = self.client.retrieve_series_metadata(self.series_instance_uid)

    def on_instance_view_row_activated(self, view, row_index, column):
        print("on_instance_view_row_activated:", view, " row_index:",
              row_index)

        # clear in reverse order
        self.clear_current_image_and_pixel_data()

        model, treeiter = view.get_selection().get_selected_rows()
        self.sop_instance_uid = model[row_index][0]
        if True:
            # TODO visually present more parts of:
            instance_metadata = self.client.retrieve_instance_metadata(
                study_instance_uid=self.study_instance_uid,
                series_instance_uid=self.series_instance_uid,
                sop_instance_uid=self.sop_instance_uid)[0]
            self.image_height = try_get_attr(instance_metadata,
                                             '00280010')  # rows
            self.image_width = try_get_attr(instance_metadata,
                                            '00280011')  # columns
            self.image_bits_per_pixel = try_get_attr(
                instance_metadata, '00280100')  # bits per pixel
            self.frame_count = try_get_attr(
                instance_metadata, '00280008', '1'
            )  # only present for multi-frame image instances so default to 1
            transfer_syntax_uid = try_get_attr(instance_metadata, '00020010')
            if transfer_syntax_uid is not None:
                transfer_type = DICOM_TRANSFER_SYNTAXES[transfer_syntax_uid]

            image_format = None  # possible values are None (uncompressed/raw), 'jpeg', or 'jp2'
            try:
                start = time.time()
                frames = self.client.retrieve_instance_frames(
                    study_instance_uid=self.study_instance_uid,
                    series_instance_uid=self.series_instance_uid,
                    sop_instance_uid=self.sop_instance_uid,
                    frame_numbers=[1],
                    image_format=image_format)
                stop = time.time()
                print("Retrieving frame took {}".format(stop - start))
            except requests.exceptions.HTTPError:
                frames = []  # no frames
                pass

            # need to store frames because Pixbuf.new_from_data doesn't keep own
            # reference to data so Python will destroy it without Gtk knowing about it
            self.rgb_frames = len(frames) * [None]

            for image_index, frame in enumerate(frames):
                pixel_count = (self.image_width * self.image_height)
                rgb_frame_temp = bytearray(
                    3 * pixel_count)  # pre-allocate RGB pixels

                if image_format is None:  # raw pixels
                    start = time.time()
                    if self.image_bits_per_pixel == 8:
                        for j in range(0, pixel_count):
                            grey8 = frame[j]
                            rgb_frame_temp[3 * j + 0] = grey8
                            rgb_frame_temp[3 * j + 1] = grey8
                            rgb_frame_temp[3 * j + 2] = grey8
                    if self.image_bits_per_pixel == 16:
                        for j in range(0, pixel_count):
                            # 16-bit grey pixel value
                            grey16 = (256 * frame[2 * j + 1] +
                                      frame[2 * j + 0])
                            grey8 = int(grey16 /
                                        128)  # 16-bit to 8-bit conversion
                            rgb_frame_temp[3 * j + 0] = grey8
                            rgb_frame_temp[3 * j + 1] = grey8
                            rgb_frame_temp[3 * j + 2] = grey8
                    else:
                        raise Exception("Cannot handle bits_per_pixel being" +
                                        str(self.image_bits_per_pixel))
                    stop = time.time()
                    print("Converting frame to grey-scale RGB-image took {}".
                          format(stop - start))

                    # Ref: https://lazka.github.io/pgi-docs/#GdkPixbuf-2.0/classes/Pixbuf.html#GdkPixbuf.Pixbuf.new_from_data
                    # this better than `Pixbuf.new_from_bytes` which requires
                    # the data parameter to be wrapped in a GLib.Bytes object

                    # WARNING: be aware of that `new_from_data` is buggy. See for instance:
                    # https://stackoverflow.com/questions/29501835/gtk3-gdk-pixbuf-new-from-data-gives-segmentation-fault-core-dumped-error-13
                    start = time.time()
                    self.rgb_frames[image_index] = bytes(
                        rgb_frame_temp
                    )  # bytearray needs to be wrapped in a `bytes` and store here in order to enot loose ref
                    pixbuf = Pixbuf.new_from_data(
                        self.rgb_frames[image_index],  # data
                        Colorspace.RGB,  # colorspace
                        False,  # has_alpha
                        8,  # bits_per_sample
                        self.image_width,  # width
                        self.image_height,  # height
                        self.image_width *
                        3,  # rowstride, 3 because RGB has 3 bytes per pixel
                        None,
                        None)  # rely on Python for deallocation
                    if image_index == 0:
                        self.image_view.set_from_pixbuf(pixbuf)
                    stop = time.time()
                    print("Display image took {}".format(stop - start))
                elif image_format == 'jp2':
                    print("TODO Decode jp2 image...")
                    if False:
                        input_stream = Gio.MemoryInputStream.new_from_data(
                            frame, None)
                        pixbuf = Pixbuf.new_from_stream(input_stream, None)
                        if image_index == 0:
                            self.image_view.set_from_pixbuf(pixbuf)

    def on_instance_selection_changed(self, selection):
        '''This is function is called too often so we cannot do any heavy work here.'''
        pass
        # print('on_instance_selection_changed: seletion:{}, selection_count={}'.format(selection,
        #                                                                             selection.count_selected_rows()))
        # TODO visually present:
        # instance_metadata = self.client.retrieve_instance_metadata(self.instance_instance_uid)

    def clear_current_series_store_and_view(self):
        self.series_view.get_selection().unselect_all()
        self.series_store.clear()

    def clear_current_instance_store_and_view(self):
        self.instance_view.get_selection().unselect_all()
        self.instance_store.clear()

    def clear_current_image_and_pixel_data(self):
        self.image_view.clear()  # remove image before
        self.rgb_frames = None  # drop pixel data

    def study_view_filter_func(self, model, iter, data):
        return True  # accept everything for now

    def series_view_filter_func(self, model, iter, data):
        return True  # accept everything for now

    def instance_view_filter_func(self, model, iter, data):
        return True  # accept everything for now
Example #3
0
  def _PubsubCallback(self, message):
    # type: pubsub_v1.Message -> None
    """Processes a Pubsub message.

    This function will retrieve the instance (specified in Pubsub message) from
    the Cloud Healthcare API. Then it will invoke CAIP Prediction API to get the
    prediction results. Finally (and optionally), it will store the inference
    results back to the Cloud Healthcare API as a DICOM Structured Report. The
    instance URL to the Structured Report containing the prediction is then
    published to a pub/sub.

    Args:
      message: Incoming pubsub message.
    """
    image_instance_path = message.data.decode()
    _logger.debug('Received instance in pubsub feed: %s', image_instance_path)
    try:
      parsed_message = pubsub_format.ParseMessage(message,
                                                  dicom_path.Type.INSTANCE)
    except exception.CustomExceptionError:
      _logger.info('Invalid input path: %s', image_instance_path)
      message.ack()
      return
    input_path = parsed_message.input_path
    authed_session = session_utils.create_session_from_gcp_credentials()
    dicomweb_url = posixpath.join(_HEALTHCARE_API_URL_PREFIX,
                                  input_path.dicomweb_path_str)
    input_client = DICOMwebClient(dicomweb_url, authed_session)
    if not self._IsMammoInstance(input_client, input_path):
      _logger.info('Instance is not of type MG modality, ignoring message: %s',
                   image_instance_path)
      message.ack()
      return

    _logger.info('Processing instance: %s', image_instance_path)
    # Retrieve instance from DICOM API in JPEG format.
    image_jpeg_bytes = input_client.retrieve_instance_rendered(
        input_path.study_uid,
        input_path.series_uid,
        input_path.instance_uid,
        media_types=('image/jpeg',))
    # Retrieve instance metadata.
    instance_metadata = input_client.retrieve_instance_metadata(
        input_path.study_uid, input_path.series_uid, input_path.instance_uid)
    # Get the predicted score and class from the inference model in Cloud ML or
    # AutoML.
    try:
      predicted_class, predicted_score = self._predictor.Predict(
          image_jpeg_bytes)
    except PermissionDenied as e:
      _logger.error('Permission error running prediction service: %s', e)
      message.nack()
      return
    except InvalidArgument as e:
      _logger.error('Invalid arguments when running prediction service: %s', e)
      message.nack()
      return

    # Print the prediction.
    inference_results = {
        'base_path': image_instance_path,
        _PREDICTION_CLASSES: predicted_class,
        _PREDICTION_SCORES: predicted_score
    }
    _logger.info(inference_results)

    # If user requested destination DICOM store for inference, create a DICOM
    # structured report that stores the prediction.
    if self._dicom_store_path:
      # Create DICOMwebClient with output url.
      output_dicom_web_url = posixpath.join(
          _HEALTHCARE_API_URL_PREFIX, self._dicom_store_path.dicomweb_path_str)
      output_client = DICOMwebClient(output_dicom_web_url, authed_session)

      # Generate series uid and instance uid for the structured report.
      sr_instance_uid = pydicom.uid.generate_uid(prefix=None)
      sr_series_uid = pydicom.uid.generate_uid(prefix=None)
      sr_dataset = _BuildComprehensiveSR(instance_metadata,
                                         inference_results[_PREDICTION_CLASSES],
                                         sr_series_uid, sr_instance_uid)
      try:
        output_client.store_instances([sr_dataset])
      except RuntimeError as e:
        _logger.error('Error storing DICOM in API: %s', e)
        message.nack()
        return

      # If user requested that new structured reports be published to a channel,
      # publish the instance path of the Structured Report
      structured_report_path = dicom_path.FromPath(
          self._dicom_store_path,
          study_uid=input_path.study_uid,
          series_uid=sr_series_uid,
          instance_uid=sr_instance_uid)
      self._PublishInferenceResultsReady(str(structured_report_path))
      _logger.info('Published structured report with path: %s',
                   structured_report_path)
    # Ack the message (successful or invalid message).
    message.ack()
    self._success_count += 1