1504 lines
47 KiB
Python
1504 lines
47 KiB
Python
from collections import defaultdict
|
|
import json
|
|
import os
|
|
import threading
|
|
from typing import Iterable
|
|
from bs4 import BeautifulSoup
|
|
import numpy as np
|
|
import requests
|
|
import webbrowser
|
|
|
|
# from blake3 import blake3
|
|
|
|
import zmq
|
|
from blake3 import blake3
|
|
|
|
# import time
|
|
#
|
|
# import sys
|
|
#
|
|
# from datauri import DataURI
|
|
|
|
from pathlib import Path
|
|
|
|
import pydicom
|
|
import pydicom.errors
|
|
import pydicom.encoders.gdcm
|
|
import pydicom.encoders.pylibjpeg
|
|
import dicognito.anonymizer
|
|
from six import create_bound_method
|
|
|
|
import typer
|
|
|
|
import time
|
|
import sys
|
|
import tempfile
|
|
|
|
# from typing import List
|
|
import wx
|
|
import wx.adv
|
|
|
|
|
|
from wxDicomViewer import BasicDicomViewer, ViewerFrame
|
|
|
|
# from watchdog.observers import Observer
|
|
|
|
# from watchdog.events import LoggingEventHandler
|
|
# from watchdog.events import PatternMatchingEventHandler
|
|
|
|
import hashlib
|
|
|
|
# import pywintypes
|
|
# import win32api
|
|
# import win32com
|
|
|
|
|
|
try:
|
|
from agw import hyperlink as hl
|
|
from agw import pygauge as PG
|
|
except ImportError: # if it's not there locally, try the wxPython lib.
|
|
import wx.lib.agw.hyperlink as hl
|
|
import wx.lib.agw.pygauge as PG
|
|
|
|
|
|
import wx.lib.agw.thumbnailctrl as TC
|
|
import wx.lib.agw.ultimatelistctrl as ULC
|
|
|
|
|
|
from wx.lib.agw.scrolledthumbnail import (
|
|
EVT_THUMBNAILS_SEL_CHANGED,
|
|
EVT_THUMBNAILS_POINTED,
|
|
EVT_THUMBNAILS_DCLICK,
|
|
)
|
|
|
|
import wx.lib.mixins.listctrl as listmix
|
|
|
|
import time
|
|
import datetime
|
|
import contextlib
|
|
|
|
# import watchdog.observers
|
|
|
|
__VERSION__ = "0.1"
|
|
__APP_NAME__ = "Uploader"
|
|
|
|
anonymizer = dicognito.anonymizer.Anonymizer()
|
|
|
|
# determine if application is a script file or frozen exe
|
|
if getattr(sys, "frozen", False):
|
|
application_path = os.path.dirname(sys.executable)
|
|
elif __file__:
|
|
application_path = os.path.dirname(__file__)
|
|
|
|
from loguru import logger
|
|
|
|
try:
|
|
logger.add(
|
|
sys.stderr, format="{time} {level} {message}", filter="my_module", level="DEBUG"
|
|
)
|
|
except TypeError:
|
|
pass
|
|
try:
|
|
logger.add(os.path.join(application_path, "logs", "anon_gui_{time}.log"))
|
|
except TypeError:
|
|
pass
|
|
|
|
logger.debug("Starting")
|
|
|
|
# import logging
|
|
# logging.basicConfig(filename=os.path.join(sys.path[0], f'anon_gui_{datetime.datetime.now().strftime("%Y-%m-%d--%H-%M-%S")}.log'), encoding='utf-8', level=logging.DEBUG)
|
|
|
|
|
|
def get_image_hash(img) -> (str, bool):
|
|
is_dicom = False
|
|
# Try and read the file as a dicom
|
|
try:
|
|
# and generate a hash from the pixel data
|
|
# TODO: improve?
|
|
dataset = pydicom.dcmread(img)
|
|
# flatten = dataset.pixel_array.astype(str).flatten()
|
|
# print("flatteded")
|
|
# pre_join = ",".join(flatten)
|
|
# print(pre_join)
|
|
# hash = hashlib.md5(pre_join.encode()).hexdigest()
|
|
# ----
|
|
md5 = hashlib.md5()
|
|
first = True
|
|
for i in dataset.pixel_array.astype(str).flatten():
|
|
if first:
|
|
first = False
|
|
md5.update(f"{i}".encode())
|
|
else:
|
|
md5.update(f",{i}".encode())
|
|
|
|
hash = md5.hexdigest()
|
|
is_dicom = True
|
|
# ----
|
|
|
|
except pydicom.errors.InvalidDicomError:
|
|
try: # This is horrible (but needed for current unit tests)
|
|
# (we use a temporary file that breaks here)
|
|
img.file.open()
|
|
hash = hashlib.md5(img.read()).hexdigest()
|
|
except AttributeError:
|
|
return "12345ABCD", False
|
|
|
|
return hash, is_dicom
|
|
|
|
|
|
app = typer.Typer(add_completion=False)
|
|
|
|
|
|
@app.command()
|
|
@logger.catch()
|
|
def annonymise(
|
|
path: Path,
|
|
output_dir: Path,
|
|
run_gui: bool = True,
|
|
file_list: bool = False,
|
|
dev: bool = False,
|
|
remove_files_on_annonymisation: bool = True,
|
|
):
|
|
logger.debug(locals())
|
|
if run_gui: # Let the GUI handle it all
|
|
launchGUI(
|
|
input_dir=path,
|
|
output_dir=output_dir,
|
|
silent_fail=True,
|
|
clear_files=False,
|
|
clear_files_on_close=True,
|
|
dev=dev,
|
|
remove_files_on_annonymisation=remove_files_on_annonymisation,
|
|
)
|
|
return
|
|
if path.is_dir():
|
|
files = list(Path(path).glob("*.dcm"))
|
|
elif path.is_file():
|
|
if file_list:
|
|
with open(path) as f:
|
|
files = [Path(p) for p in f.read().splitlines()]
|
|
# files = [path]
|
|
else:
|
|
files = [path]
|
|
else:
|
|
typer.echo("Invalid path")
|
|
return
|
|
|
|
# progress_dialog = wx.ProgressDialog("Annonymise files", "Removing data...", maximum=len(files), style=wx.PD_APP_MODAL|wx.PD_ELAPSED_TIME|wx.PD_ESTIMATED_TIME)
|
|
for n, file in enumerate(files):
|
|
# progress_dialog.Update(n)
|
|
new_filepath: Path
|
|
# Get next available filename
|
|
i: int = 0
|
|
while os.path.exists(new_filepath := output_dir.joinpath(f"IMG_{i:03}.dcm")):
|
|
i += 1
|
|
# new_filepath = Path(output_dir).joinpath(f"IMG_{i:03}.dcm")
|
|
|
|
anonymize_file(file, new_filepath)
|
|
|
|
# progress_dialog.Destroy()
|
|
|
|
return new_filepath
|
|
|
|
|
|
def anonymize_file(file: Path, new_filepath: Path, remove_original: bool = False):
|
|
logger.debug(f"Annonymise file, in={file}, out={new_filepath}")
|
|
# Ensure the output folder exists
|
|
new_filepath.parents[0].mkdir(parents=True, exist_ok=True)
|
|
# was this on windows
|
|
# new_filepath.mkdir(parents=True, exist_ok=True)
|
|
# with new_filepath.open() as f:
|
|
# f.write("", "wb")
|
|
# new_filepath.touch()
|
|
with pydicom.dcmread(file) as dataset:
|
|
anonymizer.anonymize(dataset)
|
|
dataset.save_as(new_filepath)
|
|
|
|
if remove_original:
|
|
os.remove(file)
|
|
return dataset, new_filepath
|
|
|
|
|
|
def order_files_by_dicom_attribute(
|
|
paths: Iterable[Path], dicom_attribute: str = "SliceLocation"
|
|
) -> Iterable[Path]:
|
|
files = []
|
|
|
|
for path in paths:
|
|
dataset = pydicom.dcmread(path)
|
|
files.append((path, dataset))
|
|
|
|
try:
|
|
sorted_datasets = sorted(
|
|
files, key=lambda f: f[1][dicom_attribute].value, reverse=True
|
|
)
|
|
except KeyError: # Happens if no slicelocation
|
|
return paths
|
|
return [d[0] for d in sorted_datasets]
|
|
|
|
|
|
def get_dicom_pixel_md5_hash(file: Path):
|
|
try:
|
|
with pydicom.dcmread(file) as dataset:
|
|
a = ",".join([str(i) for i in dataset.pixel_array.flatten()])
|
|
|
|
return hashlib.md5(a.encode()).hexdigest()
|
|
except pydicom.errors.InvalidDicomError:
|
|
logger.debug(f"Invalid dicom: {file}")
|
|
with open(file) as f:
|
|
return hashlib.md5(f.read()).hexdigest()
|
|
|
|
|
|
def get_file_hash(file: Path, hasher="blake3"):
|
|
match hasher:
|
|
case "blake3":
|
|
file_hash = blake3()
|
|
case "md5":
|
|
file_hash = hashlib.md5()
|
|
case _:
|
|
raise Exception(f"{_} is not a valid hasher")
|
|
|
|
with open(file, "rb") as f:
|
|
while chunk := f.read(8192):
|
|
file_hash.update(chunk)
|
|
|
|
return file_hash.hexdigest()
|
|
|
|
with open(file) as f:
|
|
return hashlib.md5(f.read()).hexdigest()
|
|
|
|
|
|
# def get_dicom_blake_hash(file: Path)@
|
|
|
|
|
|
class LoginDialog(wx.Dialog):
|
|
"""
|
|
Class to define login dialog
|
|
"""
|
|
|
|
# ----------------------------------------------------------------------
|
|
def __init__(self, parent):
|
|
"""Constructor"""
|
|
wx.Dialog.__init__(self, None, title="Login")
|
|
|
|
self.parent = parent
|
|
|
|
# user info
|
|
user_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
|
|
user_lbl = wx.StaticText(self, label="Username:")
|
|
user_sizer.Add(user_lbl, 0, wx.ALL | wx.CENTER, 5)
|
|
self.user = wx.TextCtrl(self)
|
|
user_sizer.Add(self.user, 0, wx.ALL, 5)
|
|
|
|
# pass info
|
|
p_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
|
|
p_lbl = wx.StaticText(self, label="Password:")
|
|
p_sizer.Add(p_lbl, 0, wx.ALL | wx.CENTER, 5)
|
|
self.password = wx.TextCtrl(self, style=wx.TE_PASSWORD | wx.TE_PROCESS_ENTER)
|
|
self.password.Bind(wx.EVT_TEXT_ENTER, self.on_login)
|
|
p_sizer.Add(self.password, 0, wx.ALL, 5)
|
|
|
|
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
main_sizer.Add(user_sizer, 0, wx.ALL, 5)
|
|
main_sizer.Add(p_sizer, 0, wx.ALL, 5)
|
|
|
|
btn = wx.Button(self, id=wx.ID_OK, label="Login")
|
|
btn.Bind(wx.EVT_BUTTON, self.on_login)
|
|
main_sizer.Add(btn, 0, wx.ALL | wx.CENTER, 5)
|
|
|
|
self.main_sizer = main_sizer
|
|
|
|
self.error_text = wx.StaticText(self, label="Error logging in.")
|
|
|
|
self.error_text.SetForegroundColour(wx.RED)
|
|
self.error_text.Hide()
|
|
|
|
self.main_sizer.Add(self.error_text)
|
|
|
|
self.Bind(wx.EVT_CLOSE, self.on_close)
|
|
|
|
self.SetSizer(main_sizer)
|
|
|
|
def on_close(self, event):
|
|
self.Destroy()
|
|
|
|
# ----------------------------------------------------------------------
|
|
def on_login(self, event):
|
|
"""
|
|
Check credentials and login
|
|
"""
|
|
LOGIN_URL = f"{self.parent.base_site_url}/accounts/login/"
|
|
|
|
user = self.user.GetValue()
|
|
password = self.password.GetValue()
|
|
|
|
rqst = requests.session()
|
|
rsp = rqst.get(LOGIN_URL)
|
|
|
|
token = (
|
|
BeautifulSoup(rsp.content, "html.parser").find("input").attrs["value"]
|
|
) # , attr={"name": "csrfmiddlewaretoken"}).attrs("value")
|
|
|
|
# token = rsp.cookies["csrftoken"]
|
|
# header = {"X-CSRFToken": token}
|
|
# cookies = {"csrftoken": token}
|
|
|
|
data = {
|
|
"username": user,
|
|
"password": password,
|
|
"csrfmiddlewaretoken": token,
|
|
"next": "/",
|
|
}
|
|
|
|
print(data)
|
|
rqst.headers["Referer"] = LOGIN_URL
|
|
|
|
rsp = rqst.post(
|
|
LOGIN_URL,
|
|
data=data,
|
|
# headers=header,
|
|
# cookies=cookies
|
|
)
|
|
|
|
soup = BeautifulSoup(rsp.content, "html.parser")
|
|
|
|
if soup.find("button") and soup.find("button").find(string="Login"):
|
|
print("login fail")
|
|
pass
|
|
else:
|
|
print("login success")
|
|
self.parent.login_success = True
|
|
# print(rqst.headers)
|
|
|
|
rqst.headers["X-CSRFToken"] = token
|
|
self.parent.rqst = rqst
|
|
|
|
print(rqst.headers)
|
|
print(rqst.cookies)
|
|
new = rqst.get(f"{self.parent.base_site_url}/accounts/api/atlas/get_cases_user")
|
|
|
|
print("CASESE", new)
|
|
print(new.content)
|
|
|
|
if self.parent.login_success:
|
|
self.parent.upload_button.Enable()
|
|
self.parent.login_button.Hide()
|
|
self.parent.panel.Layout()
|
|
self.Destroy()
|
|
|
|
# self.parent.login_button.SetBackgroundColour(wx.BLUE)
|
|
# self.parent.login_button.SetForegroundColour(wx.WHITE)
|
|
else:
|
|
self.login_fail()
|
|
|
|
def login_fail(self):
|
|
self.error_text.Show()
|
|
self.Layout()
|
|
pass
|
|
|
|
|
|
class SortableList(ULC.UltimateListCtrl, listmix.ColumnSorterMixin):
|
|
def __init__(self, parent, col_number, DATA={}):
|
|
ULC.UltimateListCtrl.__init__(
|
|
self,
|
|
parent,
|
|
wx.ID_ANY,
|
|
agwStyle=ULC.ULC_REPORT | ULC.ULC_VRULES | ULC.ULC_HRULES
|
|
# | ULC.ULC_SINGLE_SEL
|
|
| ULC.ULC_HAS_VARIABLE_ROW_HEIGHT,
|
|
)
|
|
self.itemDataMap = DATA
|
|
listmix.ColumnSorterMixin.__init__(self, col_number)
|
|
self.Bind(wx.EVT_LIST_COL_CLICK, self.OnColumn)
|
|
|
|
def OnColumn(self, e):
|
|
self.Refresh()
|
|
e.Skip()
|
|
|
|
def GetListCtrl(self):
|
|
return self
|
|
|
|
def UpdateData(self, DATA):
|
|
self.itemDataMap = DATA
|
|
|
|
|
|
class SeriesGrid(wx.Panel):
|
|
def __init__(self, parent):
|
|
wx.Panel.__init__(self, parent, size=(-1, 200), style=wx.SUNKEN_BORDER)
|
|
|
|
self.parent = parent
|
|
|
|
self.main_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
self.series_sizer = wx.GridSizer(wx.HORIZONTAL)
|
|
self.series_sizer.SetVGap(40)
|
|
|
|
self.text = wx.StaticText(self, label="SERIES TO UPLOAD")
|
|
self.main_sizer.Add(self.text)
|
|
|
|
self.main_sizer.Add(self.series_sizer)
|
|
|
|
self.SetSizerAndFit(self.main_sizer)
|
|
|
|
self.series = []
|
|
|
|
def display_series(self):
|
|
self.series_sizer.Clear(True)
|
|
|
|
for name, number in self.series:
|
|
logger.debug(f"add series: {name}")
|
|
panel = SeriesPanel(self, name, number)
|
|
|
|
self.series_sizer.Add(panel)
|
|
|
|
self.Layout()
|
|
|
|
|
|
class SeriesPanel(wx.Panel):
|
|
def __init__(self, parent, name, number):
|
|
wx.Panel.__init__(self, parent, size=(-1, 200), style=wx.SUNKEN_BORDER)
|
|
|
|
self.parent = parent
|
|
|
|
self.SetBackgroundColour(wx.BLUE)
|
|
self.SetForegroundColour(wx.WHITE)
|
|
|
|
sizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
name = wx.StaticText(self, label=name)
|
|
number = wx.StaticText(
|
|
self, label=f"[{number}]", style=wx.ALIGN_CENTER_HORIZONTAL
|
|
)
|
|
sizer.Add(name)
|
|
sizer.Add(number)
|
|
|
|
self.SetSizerAndFit(sizer)
|
|
|
|
|
|
class AnonGui(wx.App):
|
|
def __init__(
|
|
self,
|
|
redirect=False,
|
|
filename=None,
|
|
input_dir="",
|
|
output_dir="",
|
|
silent_fail=False,
|
|
clear_files=True,
|
|
clear_files_on_close=True,
|
|
dev=False,
|
|
remove_files_on_annonymisation=False,
|
|
):
|
|
self.dir = output_dir
|
|
self.input_dir = input_dir
|
|
|
|
logger.debug(f"Input dir: {input_dir}")
|
|
logger.debug(f"Output dir: {output_dir}")
|
|
|
|
self.base_site_url = "https://www.penracourses.org.uk"
|
|
|
|
if dev:
|
|
self.base_site_url = "http://localhost:8000"
|
|
|
|
self.login_success = False
|
|
|
|
self.files = {}
|
|
self.series_map = {}
|
|
self.series_map_details = {}
|
|
|
|
self.active = ()
|
|
|
|
self.silent_fail = silent_fail
|
|
self.clear_files_on_start = clear_files
|
|
self.clear_files_on_close = clear_files_on_close
|
|
self.clear_files_on_upload = True
|
|
self.remove_files_on_annonymisation = remove_files_on_annonymisation
|
|
|
|
self.rqst = None
|
|
|
|
wx.App.__init__(self, redirect, filename)
|
|
|
|
def my_listener(self, message, arg2=None):
|
|
"""
|
|
Listener function
|
|
"""
|
|
logger.debug(f"Received the following message: {message}")
|
|
if arg2:
|
|
logger.debug(f"Received another arguments: {arg2}")
|
|
|
|
def OnInit(self):
|
|
self.name = "SingleApp-%s" % wx.GetUserId()
|
|
self.instance = wx.SingleInstanceChecker(self.name)
|
|
|
|
if self.instance.IsAnotherRunning():
|
|
logger.debug("Another instance detected")
|
|
wx.adv.NotificationMessage("Another instance detected", "Trigger refresh").Show()
|
|
context = zmq.Context()
|
|
socket = context.socket(zmq.PUSH)
|
|
socket.connect("tcp://127.0.0.1:5556")
|
|
socket.send_string("loaded")
|
|
logger.debug("Trigger refresh")
|
|
|
|
time.sleep(5)
|
|
if not self.silent_fail:
|
|
wx.MessageBox("Another instance is running", "ERROR")
|
|
return False
|
|
|
|
def zmq_listener(self):
|
|
context = zmq.Context()
|
|
self.socket = context.socket(zmq.PULL)
|
|
self.socket.bind("tcp://127.0.0.1:5556")
|
|
logger.debug("idle")
|
|
while True:
|
|
msg = self.socket.recv()
|
|
# if msg == 'zeromq':
|
|
logger.debug(f"{msg=}")
|
|
if msg == b"loaded":
|
|
logger.debug("loaded1")
|
|
wx.CallAfter(self.AnnonymiseFilesInDir, self.input_dir, self.dir)
|
|
|
|
self.zmq_thread = threading.Thread(
|
|
target=zmq_listener, args=(self,), daemon=True
|
|
)
|
|
self.zmq_thread.start()
|
|
|
|
self.frame = wx.Frame(
|
|
None,
|
|
wx.ID_ANY,
|
|
title=__APP_NAME__,
|
|
style=wx.DEFAULT_FRAME_STYLE | wx.STAY_ON_TOP,
|
|
size=(300, 400),
|
|
)
|
|
|
|
self.frame.SetIcon(wx.Icon("icon/icon1.ico"))
|
|
|
|
self.panel = wx.Panel(self.frame, wx.ID_ANY)
|
|
|
|
self.file_detail_map = {}
|
|
self.file_duplicate_check = {}
|
|
|
|
self.list_ctrl = SortableList(
|
|
self.panel,
|
|
6,
|
|
)
|
|
self.series_grid = SeriesGrid(self.panel)
|
|
|
|
if self.clear_files_on_start:
|
|
self.ClearFiles(load_dir=False)
|
|
|
|
self.panel.sizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
#self.link_rapid_upload = hl.HyperLinkCtrl(
|
|
# self.panel,
|
|
# wx.ID_ANY,
|
|
# "Rapid",
|
|
# URL=f"{self.base_site_url}/rapids/create/",
|
|
#)
|
|
## self.panel.sizer.Add(self.link_rapid_upload, 0, wx.ALL, 10)
|
|
|
|
#self.link_long_upload = hl.HyperLinkCtrl(
|
|
# self.panel,
|
|
# wx.ID_ANY,
|
|
# "Long Series",
|
|
# URL=f"{self.base_site_url}/longs/create/series",
|
|
#)
|
|
#self.link_atlas_case = hl.HyperLinkCtrl(
|
|
# self.panel,
|
|
# wx.ID_ANY,
|
|
# "Atlas Case",
|
|
# URL=f"{self.base_site_url}/atlas/case/create/",
|
|
#)
|
|
# self.panel.sizer.Add(self.link_long_upload, 0, wx.ALL, 10)
|
|
|
|
# Create menu bar
|
|
self.menu_bar = wx.MenuBar()
|
|
|
|
|
|
# Create File menu
|
|
file_menu = wx.Menu()
|
|
|
|
|
|
# Create Load menu item
|
|
load_item = wx.MenuItem(file_menu, wx.ID_ANY, "&Import Directory")
|
|
#load_item.SetToolTip("Click to manually load dicom from a directory.")
|
|
file_menu.Append(load_item)
|
|
|
|
self.Bind(wx.EVT_MENU, self.import_from_dir, load_item)
|
|
#file_menu.Append(wx.ID_OPEN, "&Open")
|
|
#file_menu.Append(wx.ID_SAVE, "&Save")
|
|
#file_menu.AppendSeparator()
|
|
file_menu.Append(wx.ID_EXIT, "&Exit")
|
|
self.Bind(wx.EVT_MENU, lambda evt: wx.Exit(), id=wx.ID_EXIT)
|
|
|
|
self.menu_bar.Append(file_menu, "&File")
|
|
|
|
|
|
# Create Images menu
|
|
images_menu = wx.Menu()
|
|
|
|
# Create Clear Files menu item
|
|
clear_files_item = wx.MenuItem(images_menu, wx.ID_ANY, "&Clear files")
|
|
#clear_files_item.SetToolTip("Click to clear all files.")
|
|
images_menu.Append(clear_files_item)
|
|
|
|
self.menu_bar.Append(images_menu, "&Images")
|
|
|
|
# Create Reorder Files menu item
|
|
reorder_files_item = wx.MenuItem(images_menu, wx.ID_ANY, "&Reorder")
|
|
#reorder_files_item.SetToolTip("Click to reorder files as they are shown below. This will sequentially rename the files. Please note this is usually better done once the files have been uploaded.")
|
|
images_menu.Append(reorder_files_item)
|
|
|
|
self.Bind(wx.EVT_MENU, self.on_reorder_files, reorder_files_item)
|
|
|
|
# Bind Clear Files menu item event
|
|
self.Bind(wx.EVT_MENU, self.ClearFiles, clear_files_item)
|
|
|
|
|
|
# Create Reannonymise Files menu item
|
|
reannonymise_files_item = wx.MenuItem(images_menu, wx.ID_ANY, "&Reannonymise")
|
|
#reannonymise_files_item.SetToolTip("Click to reannonymise the currently loaded files.")
|
|
images_menu.Append(reannonymise_files_item)
|
|
|
|
self.Bind(wx.EVT_MENU, self.on_reannonymise_files, reannonymise_files_item)
|
|
|
|
|
|
# Create Refresh Files menu item
|
|
refresh_files_item = wx.MenuItem(images_menu, wx.ID_ANY, "&Refresh")
|
|
images_menu.Append(refresh_files_item)
|
|
|
|
self.Bind(wx.EVT_MENU, lambda evt: self.LoadDir(), refresh_files_item)
|
|
|
|
|
|
# Create Item creation menu
|
|
create_menu = wx.Menu()
|
|
|
|
create_menu_items = (
|
|
("&Rapid", f"/rapids/create/"),
|
|
("&Long Series", f"/longs/create/series"),
|
|
("&Atlas Case", f"/atlas/case/create/"),
|
|
)
|
|
|
|
for label, url in create_menu_items:
|
|
item = wx.MenuItem(create_menu, wx.ID_ANY, label)
|
|
create_menu.Append(item)
|
|
self.Bind(wx.EVT_MENU, lambda evt, url=url: webbrowser.open(f"{self.base_site_url}{url}"), item)
|
|
|
|
self.menu_bar.Append(create_menu, "&Create")
|
|
|
|
# Create Help menu
|
|
help_menu = wx.Menu()
|
|
help_menu.Append(wx.ID_HELP, "&Help")
|
|
help_menu.Append(wx.ID_ABOUT, "&About")
|
|
self.menu_bar.Append(help_menu, "&Help")
|
|
|
|
self.Bind(wx.EVT_MENU, self.on_about_box, id=wx.ID_ABOUT)
|
|
self.Bind(wx.EVT_MENU, self.on_help, id=wx.ID_HELP)
|
|
|
|
# Set menu bar
|
|
self.frame.SetMenuBar(self.menu_bar)
|
|
|
|
#hoz_links = wx.BoxSizer(wx.HORIZONTAL)
|
|
#hoz_links.Add(self.link_rapid_upload, 1, wx.EXPAND)
|
|
#hoz_links.Add(self.link_long_upload, 1, wx.EXPAND)
|
|
#hoz_links.Add(self.link_atlas_case, 1, wx.EXPAND)
|
|
#self.panel.sizer.Add(hoz_links, 1, wx.EXPAND)
|
|
|
|
# self.dirctrl = wx.GenericDirCtrl(self.panel, wx.ID_ANY,
|
|
# dir=dir,
|
|
# style=wx.DIRCTRL_SHOW_FILTERS |
|
|
# wx.DIRCTRL_3D_INTERNAL |
|
|
# wx.DIRCTRL_MULTIPLE,
|
|
# filter="Dicom files (*.dcm)|*.dcm")
|
|
bottom_button_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
self.drag_button = wx.Button(
|
|
self.panel, id=wx.ID_ANY, label="Drag"
|
|
)
|
|
self.drag_button.SetToolTip("Click here to drag files")
|
|
self.upload_button = wx.Button(
|
|
self.panel, id=wx.ID_ANY, label="Click here to upload files"
|
|
)
|
|
self.upload_button.SetToolTip("Upload files to the server. You must be logged in.")
|
|
self.upload_button.Disable()
|
|
bottom_button_sizer.Add(self.upload_button, 4, wx.EXPAND)
|
|
bottom_button_sizer.Add(self.drag_button, 1, wx.EXPAND)
|
|
|
|
# Use some sizers to see layout options
|
|
# self.panel.sizer.Add(self.dirctrl, 1, wx.EXPAND)
|
|
|
|
# self.Bind(wx.EVT_BUTTON, self.OnDragInit, self.drag_button)
|
|
self.Bind(wx.EVT_LEFT_DOWN, self.OnDragInit, self.drag_button)
|
|
self.Bind(wx.EVT_LEFT_DOWN, self.OnFileUpload, self.upload_button)
|
|
self.Bind(wx.EVT_LIST_COL_CLICK, self.OnColumnClick)
|
|
|
|
# self.list_ctrl = wx.ListCtrl(self.panel,
|
|
# style=wx.LC_REPORT
|
|
# |wx.BORDER_SUNKEN
|
|
# )
|
|
|
|
self.Bind(ULC.EVT_LIST_ITEM_RIGHT_CLICK, self.ShowPopupMenu, self.list_ctrl)
|
|
|
|
self.list_ctrl.InsertColumn(0, "Filename")
|
|
self.list_ctrl.InsertColumn(1, "Examination")
|
|
self.list_ctrl.InsertColumn(2, "Instance Number")
|
|
self.list_ctrl.InsertColumn(3, "Name")
|
|
self.list_ctrl.InsertColumn(4, "Series Description")
|
|
self.list_ctrl.InsertColumn(5, "Slice location")
|
|
self.list_ctrl.InsertColumn(6, "Hash")
|
|
self.list_ctrl.InsertColumn(7, "Series Instance UID")
|
|
|
|
# self.UpdateListCtrl()
|
|
|
|
self.login_button = wx.Button(self.panel, id=wx.ID_ANY, label="Login")
|
|
self.login_button.SetForegroundColour(wx.BLUE)
|
|
self.login_button.SetBackgroundColour(wx.WHITE)
|
|
self.login_button.SetToolTip("Click to login to the server")
|
|
|
|
|
|
# wx.Bind(self.clear_files_button, )
|
|
self.Bind(wx.EVT_BUTTON, self.Login, self.login_button)
|
|
#self.Bind(wx.EVT_BUTTON, self.ClearFiles, self.clear_files_button)
|
|
# Use some sizers to see layout options
|
|
# self.panel.sizer.Add(self.dirctrl, 1, wx.EXPAND)
|
|
|
|
hoz = wx.BoxSizer(wx.HORIZONTAL)
|
|
hoz.Add(self.login_button, 1, wx.EXPAND)
|
|
|
|
self.panel.sizer.Add(hoz, 1, wx.EXPAND)
|
|
|
|
self.panel.sizer.Add(self.series_grid, 2, wx.EXPAND)
|
|
self.panel.sizer.Add(self.list_ctrl, 8, wx.EXPAND)
|
|
#self.panel.sizer.Add(self.drag_button, 2, wx.EXPAND)
|
|
#self.panel.sizer.Add(self.upload_button, 2, wx.EXPAND)
|
|
self.panel.sizer.Add(bottom_button_sizer, 1, wx.EXPAND)
|
|
|
|
self.progress_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
self.progress_bar = wx.Gauge(
|
|
self.panel, style=wx.GA_HORIZONTAL | wx.GA_PROGRESS
|
|
)
|
|
# self.progress_bar = PG.PyGauge(self.panel, style=wx.GA_HORIZONTAL|wx.GA_PROGRESS)
|
|
# self.progress_bar.SetDrawValue(draw=True, drawPercent=True, font=None, colour=wx.RED, formatString=None)
|
|
self.progress_text = wx.StaticText(self.panel, label="Text")
|
|
self.progress_sizer.Add(self.progress_bar, 2, wx.EXPAND)
|
|
self.progress_sizer.Add(self.progress_text, 2, wx.EXPAND)
|
|
self.panel.sizer.Add(self.progress_sizer, 2, wx.EXPAND)
|
|
self.progress_sizer.ShowItems(show=False)
|
|
|
|
# Layout sizers
|
|
self.panel.SetSizer(self.panel.sizer)
|
|
self.panel.SetAutoLayout(1)
|
|
self.panel.sizer.Fit(self.panel)
|
|
self.frame.Show()
|
|
# self.spinner.Hide()
|
|
|
|
wx.CallAfter(self.post_init)
|
|
|
|
return super().OnInit()
|
|
|
|
def post_init(self):
|
|
self.AnnonymiseFilesInDir(self.input_dir, self.dir)
|
|
|
|
self.Bind(wx.EVT_KEY_DOWN, self.onKeyPress)
|
|
|
|
self.image_hash_check_in_progress = False
|
|
self.duplicate_check_timer = wx.Timer()
|
|
self.Bind(wx.EVT_TIMER, self.check_image_hashes, self.duplicate_check_timer)
|
|
self.duplicate_check_timer.Start(2000)
|
|
|
|
def populate_series_grid(self):
|
|
self.series_grid.series = []
|
|
|
|
for series in self.series_map:
|
|
logger.debug(f"Create series: {series}")
|
|
self.series_grid.series.append(
|
|
(
|
|
self.series_map_details[series]["series_description"],
|
|
len(self.series_map[series]),
|
|
)
|
|
)
|
|
|
|
self.series_grid.display_series()
|
|
|
|
def import_from_dir(self, event):
|
|
default_path = str(self.input_dir.resolve())
|
|
with wx.DirDialog(
|
|
self.frame, "Choose a directory to import dicom files from.", style=wx.DD_DIR_MUST_EXIST, defaultPath=default_path
|
|
) as dlg:
|
|
if dlg.ShowModal() == wx.ID_OK:
|
|
load_path = Path(dlg.GetPath())
|
|
self.AnnonymiseFilesInDir(load_path, self.dir, remove_original=False)
|
|
|
|
def AnnonymiseFilesInDir(self, input_dir, output_dir, remove_original: bool | None=None):
|
|
logger.debug("AnnonymiseFilesInDir")
|
|
logger.debug(f"input: {input_dir} / output {output_dir}")
|
|
if input_dir.is_dir():
|
|
logger.debug("is dir")
|
|
files = list(Path(input_dir).rglob("*.dcm"))
|
|
else:
|
|
return
|
|
|
|
if not files:
|
|
logger.debug("No files to annonymise")
|
|
wx.adv.NotificationMessage("Annonymise files", "No files to annonymise").Show()
|
|
return
|
|
|
|
logger.debug(files)
|
|
|
|
|
|
if remove_original is None:
|
|
remove_original = self.remove_files_on_annonymisation
|
|
|
|
# files = order_files_by_dicom_attribute(files)
|
|
|
|
# progress_dialog = wx.ProgressDialog(
|
|
# "Annonymise files",
|
|
# "Removing data...",
|
|
# maximum=len(files),
|
|
# style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME | wx.PD_ESTIMATED_TIME,
|
|
# )
|
|
with self.progress(len(files), "Annonymise files") as p:
|
|
# p.SetRange(len(files))
|
|
|
|
for n, file in enumerate(files):
|
|
# progress_dialog.Update(n)
|
|
p.update(n)
|
|
new_filepath: Path
|
|
# Get next available filename
|
|
i: int = 0
|
|
while os.path.exists(
|
|
new_filepath := output_dir.joinpath(f"IMG_{i:03}.dcm")
|
|
):
|
|
i += 1
|
|
# new_filepath = Path(output_dir).joinpath(f"IMG_{i:03}.dcm")
|
|
|
|
dataset, filepath = anonymize_file(file, new_filepath, remove_original=remove_original)
|
|
self.add_datails_to_file_detail_map(dataset, filepath)
|
|
# progress_dialog.Destroy()
|
|
|
|
self.LoadDir(self.dir)
|
|
|
|
@contextlib.contextmanager
|
|
def progress(self, total: int, text: str = "Working"):
|
|
# self.progress_sizer.Show()
|
|
self.progress_text.SetLabelText(text)
|
|
self.progress_bar.SetRange(total)
|
|
self.progress_sizer.ShowItems(show=True)
|
|
self.panel.sizer.Fit(self.panel)
|
|
self.frame.Layout()
|
|
try:
|
|
|
|
class P:
|
|
def __init__(self, gui, total, text):
|
|
self.gui = gui
|
|
self.text = text
|
|
self.total = total
|
|
|
|
def update(self, n: int):
|
|
self.gui.progress_bar.SetValue(n)
|
|
self.gui.progress_text.SetLabelText(f"{self.text} : {n}")
|
|
self.gui.frame.Refresh()
|
|
|
|
yield P(self, total, text)
|
|
finally:
|
|
# self.progress_sizer.Hide()
|
|
self.progress_sizer.ShowItems(show=False)
|
|
self.panel.Layout()
|
|
# self.panel.SetAutoLayout(1)
|
|
# self.panel.sizer.Fit(self.panel)
|
|
|
|
def onKeyPress(self, event):
|
|
keycode = event.GetKeyCode()
|
|
logger.debug(keycode)
|
|
if keycode == wx.WXK_F1:
|
|
logger.debug("you pressed the F1!")
|
|
self.on_help()
|
|
return
|
|
event.Skip()
|
|
|
|
def on_help(self, event=None):
|
|
webbrowser.open(f"{self.base_site_url}/atlas/help")
|
|
|
|
def on_about_box(self, event=None):
|
|
description = f"""
|
|
A dicom export/upload tool
|
|
|
|
Settings
|
|
--------
|
|
Watch/import dir: {self.input_dir}
|
|
Annonymised files dir: {self.dir}
|
|
|
|
clear files on start: {self.clear_files_on_start}
|
|
clear files on close: {self.clear_files_on_close}
|
|
remove files on annonymisations: {self.remove_files_on_annonymisation}
|
|
|
|
base site url: {self.base_site_url}
|
|
"""
|
|
|
|
licence = """GPL"""
|
|
|
|
info = wx.adv.AboutDialogInfo()
|
|
|
|
# info.SetIcon(wx.Icon('hunter.png', wx.BITMAP_TYPE_PNG))
|
|
info.SetName(__APP_NAME__)
|
|
info.SetVersion(__VERSION__)
|
|
info.SetDescription(description)
|
|
icon_path = "icon/icon1.png"
|
|
desired_width = 200
|
|
desired_height = 200
|
|
|
|
# Load the icon image
|
|
image = wx.Image(icon_path)
|
|
|
|
# Resize the image
|
|
resized_image = image.Rescale(desired_width, desired_height)
|
|
|
|
# Create a new icon with the resized image
|
|
resized_icon = wx.Icon(resized_image.ConvertToBitmap())
|
|
|
|
info.SetIcon(resized_icon)
|
|
|
|
info.SetLicence(licence)
|
|
|
|
wx.adv.AboutBox(info)
|
|
|
|
def OnExit(self, event=None):
|
|
print("on exit")
|
|
if self.clear_files_on_close:
|
|
self.ClearFiles(load_dir=False)
|
|
|
|
# self.zmq_thread.join()
|
|
|
|
return super().OnExit()
|
|
|
|
def LoadDir(self, dir=None, refresh=True, clear_file_detail_map=False):
|
|
"""Loads data from the output directory (annonymised files)"""
|
|
if clear_file_detail_map:
|
|
self.file_detail_map = {}
|
|
self.file_duplicate_check = {}
|
|
if dir is None:
|
|
dir = self.dir
|
|
|
|
logger.debug(f"LoadDir : {dir} / refresh: {refresh} / clear_file_detail_map: {clear_file_detail_map}")
|
|
|
|
if refresh:
|
|
self.files = {}
|
|
self.series_map = {}
|
|
self.series_map_details = {}
|
|
for path in Path(dir).glob("*.dcm"):
|
|
self.AddFile(path)
|
|
|
|
self.UpdateListCtrl()
|
|
|
|
def AddFile(self, path):
|
|
hash = get_file_hash(path)
|
|
logger.debug("Hash", hash)
|
|
|
|
if hash not in self.files.values():
|
|
self.files[path] = hash
|
|
# Remove duplicate files
|
|
else:
|
|
os.remove(path)
|
|
|
|
def RemoveFile(self, path):
|
|
del self.files[path]
|
|
|
|
def OnColumnClick(self, event):
|
|
self.list_ctrl.Refresh()
|
|
event.Skip()
|
|
|
|
def ShowPopupMenu(self, event):
|
|
if self.list_ctrl.GetSelectedItemCount():
|
|
pass
|
|
else:
|
|
ind = event.GetIndex()
|
|
if ind > -1:
|
|
self.list_ctrl.Select(ind)
|
|
else:
|
|
return
|
|
menu = wx.Menu()
|
|
menu.Append(1, "Delete selected items")
|
|
menu.Bind(wx.EVT_MENU, self.DeleteItems, id=1)
|
|
menu.Append(2, "View Dicom")
|
|
menu.Bind(wx.EVT_MENU, self.ViewDicom, id=2)
|
|
|
|
for i in range(self.list_ctrl.GetItemCount()):
|
|
if self.list_ctrl.IsSelected(i):
|
|
path = self.list_ctrl.GetItemPyData(i)["file"]
|
|
if (
|
|
path in self.file_duplicate_check
|
|
and self.file_duplicate_check[path]["id"]
|
|
):
|
|
if self.file_duplicate_check[path]["type"] == "series":
|
|
menu.Append(3, "View on site (series)")
|
|
menu.Bind(
|
|
wx.EVT_MENU,
|
|
lambda evt: webbrowser.open(
|
|
f"{self.base_site_url}{self.file_duplicate_check[path]['url']}"
|
|
),
|
|
id=3,
|
|
)
|
|
else:
|
|
menu.Append(3, "View on site (uncategorised)")
|
|
menu.Bind(
|
|
wx.EVT_MENU,
|
|
lambda evt: webbrowser.open(
|
|
f"{self.base_site_url}{self.file_duplicate_check[path]['url']}"
|
|
),
|
|
id=3,
|
|
)
|
|
break
|
|
|
|
self.frame.PopupMenu(menu)
|
|
|
|
def ViewDicom(self, event):
|
|
selectedItems = []
|
|
for i in range(self.list_ctrl.GetItemCount()):
|
|
if self.list_ctrl.IsSelected(i):
|
|
# path = self.list_ctrl.GetItemPyData(i["file"])
|
|
path = self.list_ctrl.GetItemPyData(i)["file"]
|
|
print(path)
|
|
|
|
viewer = ViewerFrame(None, "Viewer", path)
|
|
# viewer.MainLoop()
|
|
break
|
|
|
|
def on_reannonymise_files(self, event):
|
|
global anonymizer
|
|
anonymizer = dicognito.anonymizer.Anonymizer()
|
|
paths = []
|
|
|
|
# self.file_observer.pause()
|
|
|
|
tempdir = Path(tempfile.gettempdir())
|
|
to_move = []
|
|
with self.progress(self.list_ctrl.GetItemCount(), "Reannonymise files") as p:
|
|
for i in range(self.list_ctrl.GetItemCount()):
|
|
p.update(i)
|
|
path = self.list_ctrl.GetItemPyData(i)["file"]
|
|
logger.debug("--", path)
|
|
# paths.append(path)
|
|
new_path = annonymise(path, tempdir, run_gui=False)
|
|
os.remove(path)
|
|
to_move.append((new_path, Path(self.dir, new_path.name)))
|
|
|
|
for path, new_path in to_move:
|
|
logger.debug("**", path, new_path)
|
|
os.rename(path, new_path)
|
|
|
|
self.LoadDir(self.dir, clear_file_detail_map=True)
|
|
|
|
def on_reorder_files(self, event):
|
|
paths = []
|
|
|
|
# self.file_observer.pause()
|
|
|
|
tempdir = Path(tempfile.gettempdir())
|
|
to_move = []
|
|
for i in range(self.list_ctrl.GetItemCount()):
|
|
path = self.list_ctrl.GetItemPyData(i)["file"]
|
|
logger.debug("--", path)
|
|
# paths.append(path)
|
|
|
|
os.replace(path, new_path := tempdir.joinpath(f"IMG_{i:03}.dcm"))
|
|
# os.remove(path)
|
|
to_move.append((new_path, Path(self.dir, new_path.name)))
|
|
i += 1
|
|
|
|
for path, new_path in to_move:
|
|
logger.debug("**", path, new_path)
|
|
os.rename(path, new_path)
|
|
|
|
self.LoadDir(self.dir)
|
|
# self.file_observer.resume()
|
|
|
|
def DeleteItems(self, event):
|
|
selectedItems = []
|
|
for i in range(self.list_ctrl.GetItemCount()):
|
|
if self.list_ctrl.IsSelected(i):
|
|
selectedItems.append(self.list_ctrl.GetItemPyData(i)["file"])
|
|
|
|
logger.debug(selectedItems)
|
|
for f in selectedItems:
|
|
self.RemoveFile(f)
|
|
os.remove(f)
|
|
|
|
wx.CallAfter(self.UpdateListCtrl)
|
|
|
|
def Login(self, event=None):
|
|
dlg = LoginDialog(self)
|
|
|
|
dlg.ShowModal()
|
|
|
|
def ClearFiles(self, event=None, load_dir: bool = True, files:None| list[(str, str)] = None):
|
|
if files is None:
|
|
files = Path(self.dir).glob("*")
|
|
else:
|
|
files = [Path(self.dir, f) for f, file_hash in files]
|
|
|
|
for f in files:
|
|
os.remove(f)
|
|
|
|
if load_dir:
|
|
self.LoadDir(clear_file_detail_map=True)
|
|
|
|
def add_datails_to_file_detail_map(self, dataset, file):
|
|
try:
|
|
study_desc = dataset.StudyDescription
|
|
except:
|
|
study_desc = "..."
|
|
try:
|
|
series_desc = dataset.SeriesDescription
|
|
except:
|
|
series_desc = "..."
|
|
try:
|
|
instance_number = f"{dataset.InstanceNumber:03}"
|
|
except:
|
|
instance_number = "..."
|
|
try:
|
|
patient_name = str(dataset.PatientName)
|
|
except:
|
|
patient_name = "..."
|
|
try:
|
|
slice_location = str(dataset.SliceLocation)
|
|
except:
|
|
slice_location = "..."
|
|
|
|
series_instances_uid = str(dataset.SeriesInstanceUID)
|
|
d2 = {
|
|
"series_instances_uid": series_instances_uid,
|
|
"series_description": series_desc,
|
|
"study_description": study_desc,
|
|
"name": patient_name,
|
|
}
|
|
|
|
# md5 = hashlib.md5()
|
|
# first = True
|
|
# for i in dataset.pixel_array.astype(str).flatten():
|
|
# if first:
|
|
# first = False
|
|
# md5.update(f"{i}".encode())
|
|
# else:
|
|
# md5.update(f",{i}".encode())
|
|
# md5.update(dataset.PixelData)
|
|
# md5.update("".join(dataset.pixel_array.astype(str).flatten()).encode())
|
|
# md5.update(np.char.join("", dataset.pixel_array.astype(str).flatten()).encode())
|
|
# hash = md5.hexdigest()
|
|
# print(hash)
|
|
|
|
# md5 = hashlib.md5()
|
|
##hash = md5.hexdigest()
|
|
##md5 = hashlib.md5()
|
|
##first = True
|
|
##s = ",".join([f"{i}" for i in dataset.pixel_array.astype(str).flatten()]).encode()
|
|
##print(s)
|
|
# print(dataset.PixelData[:20])
|
|
# print(len(dataset.PixelData))
|
|
# md5.update(dataset.PixelData)
|
|
|
|
# hash = md5.hexdigest()
|
|
|
|
hasher = blake3()
|
|
hasher.update(dataset.PixelData)
|
|
hash = hasher.hexdigest()
|
|
# dataset.PixelData
|
|
|
|
d = (
|
|
file.name,
|
|
study_desc,
|
|
instance_number,
|
|
patient_name,
|
|
series_desc,
|
|
slice_location,
|
|
hash,
|
|
series_instances_uid,
|
|
)
|
|
|
|
# hasher = blake3()
|
|
# first = True
|
|
# for i in dataset.pixel_array.astype(str).flatten():
|
|
# if first:
|
|
# first = False
|
|
# hasher.update(f"{i}".encode())
|
|
# else:
|
|
# hasher.update(f",{i}".encode())
|
|
|
|
# hash = hasher.digest()
|
|
|
|
# hash = get_file_hash(file, hasher="md5")
|
|
|
|
# hash = None # If we don't use the hash...
|
|
self.file_detail_map[file] = d, hash, d2
|
|
|
|
return d, hash, d2
|
|
|
|
def UpdateListCtrl(self, event=None):
|
|
logger.debug("update list ctrl")
|
|
self.list_ctrl.DeleteAllItems()
|
|
n = 0
|
|
DATA = {}
|
|
|
|
# progress_dialog = wx.ProgressDialog(
|
|
# "Loading files",
|
|
# "Extracting data from files",
|
|
# maximum=len(self.files),
|
|
# style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME | wx.PD_ESTIMATED_TIME,
|
|
# )
|
|
with self.progress((len(self.files)), "Loading files") as p:
|
|
# p.SetRange(len(self.files))
|
|
|
|
self.series_map = defaultdict(list)
|
|
self.series_map_details = defaultdict(dict)
|
|
|
|
hashes = []
|
|
|
|
for n, f in enumerate(self.files):
|
|
# progress_dialog.Update(n)
|
|
p.update(n)
|
|
# dataset = pydicom.read_file(f)
|
|
|
|
if f in self.file_detail_map:
|
|
d, hash, d2 = self.file_detail_map[f]
|
|
print(f"{n}: in map")
|
|
else:
|
|
try:
|
|
dataset = pydicom.dcmread(f)
|
|
except FileNotFoundError:
|
|
return
|
|
|
|
print("recalc")
|
|
d, hash, d2 = self.add_datails_to_file_detail_map(dataset, f)
|
|
|
|
hashes.append(hash)
|
|
|
|
series_instances_uid = d2["series_instances_uid"]
|
|
try:
|
|
self.series_map[series_instances_uid].append(f)
|
|
self.series_map_details[series_instances_uid][
|
|
"series_description"
|
|
] = d2["series_description"]
|
|
self.series_map_details[series_instances_uid][
|
|
"study_description"
|
|
] = d2["study_description"]
|
|
self.series_map_details[series_instances_uid]["name"] = d2["name"]
|
|
except:
|
|
logger.debug(f"Unable to get Series Instance UID: {f.name}")
|
|
|
|
logger.debug(d)
|
|
DATA[n] = d
|
|
self.list_ctrl.Append(d)
|
|
self.list_ctrl.SetItemPyData(n, {"file": f, "hash": hash})
|
|
self.list_ctrl.SetItemData(n, n)
|
|
|
|
if f in self.file_duplicate_check:
|
|
if self.file_duplicate_check[f]["id"]:
|
|
self.list_ctrl.SetItemBackgroundColour(n, wx.RED)
|
|
|
|
n = n + 1
|
|
self.list_ctrl.UpdateData(DATA)
|
|
self.populate_series_grid()
|
|
|
|
self.frame.Refresh()
|
|
self.frame.Layout()
|
|
|
|
# progress_dialog.Destroy()
|
|
|
|
# if hashes:
|
|
# self.check_image_hashes(hashes)
|
|
|
|
def check_image_hashes(self, evt):
|
|
if self.login_success and not self.image_hash_check_in_progress:
|
|
self.image_hash_check_in_progress = True
|
|
|
|
to_check = self.file_detail_map.keys() - self.file_duplicate_check.keys()
|
|
|
|
if to_check:
|
|
hash_file_map = {}
|
|
for file in to_check:
|
|
hash_file_map[self.file_detail_map[file][1]] = file
|
|
|
|
hashes = list(hash_file_map.keys())
|
|
|
|
self.rqst.headers["X-CSRFToken"] = self.rqst.cookies["csrftoken"]
|
|
# print(self.rqst.headers)
|
|
# print(self.rqst.cookies)
|
|
resp = self.rqst.post(
|
|
f"{self.base_site_url}/api/atlas/check_image_hashes/",
|
|
data=json.dumps(hashes),
|
|
# files=files,
|
|
)
|
|
|
|
for hash, dup in resp.json().items():
|
|
self.file_duplicate_check[hash_file_map[hash]] = dup
|
|
|
|
if resp.json().items():
|
|
wx.CallAfter(self.UpdateListCtrl)
|
|
self.image_hash_check_in_progress = False
|
|
|
|
def OnColRightClick(self, event):
|
|
item = self.list_ctrl.GetColumn(event.GetColumn())
|
|
logger.debug(item)
|
|
|
|
def OnFileUpload(self, event):
|
|
# self.spinner.Show()
|
|
wx.CallAfter(self.upload)
|
|
|
|
def upload(self):
|
|
files_to_upload = []
|
|
duplicate_files_not_uploaded = []
|
|
logger.debug("Upload files")
|
|
for f in self.files:
|
|
if f in self.file_duplicate_check:
|
|
# duplicate the api return format so ClearFiles works
|
|
duplicate_files_not_uploaded.append((f.name, "..."))
|
|
else:
|
|
files_to_upload.append(("files", open(str(f), "rb")))
|
|
|
|
# chunck files
|
|
n = 10
|
|
chunked_files = [
|
|
files_to_upload[i : i + n] for i in range(0, len(files_to_upload), n)
|
|
]
|
|
|
|
# progress_dialog = wx.ProgressDialog(
|
|
# "Uploading files",
|
|
# "Uploading files. This is done in batches...",
|
|
# maximum=len(chunked_files),
|
|
# style=wx.PD_ELAPSED_TIME | wx.PD_ESTIMATED_TIME,
|
|
# )
|
|
with self.progress(len(chunked_files), "Uploading files (in batches)") as p:
|
|
# p.SetRange(len(chunked_files))
|
|
|
|
upload_file_list = []
|
|
duplicate_file_list = []
|
|
failed = []
|
|
|
|
for n, files in enumerate(chunked_files):
|
|
|
|
def upload_files(files):
|
|
self.rqst.headers["X-CSRFToken"] = self.rqst.cookies["csrftoken"]
|
|
# print(self.rqst.headers)
|
|
# print(self.rqst.cookies)
|
|
resp = self.rqst.post(
|
|
f"{self.base_site_url}/api/atlas/upload_dicom",
|
|
# data=data,
|
|
files=files,
|
|
)
|
|
|
|
logger.debug(f"Resp: {resp}")
|
|
logger.debug(f"{resp.content}")
|
|
return resp
|
|
|
|
p.update(n)
|
|
# progress_dialog.Update(n, f"Uploading batch {n}/{len(chunked_files)}")
|
|
|
|
logger.debug(f"n: {n}")
|
|
# try to upload the files
|
|
|
|
for i in range(3):
|
|
resp = upload_files(files)
|
|
if resp.status_code == 200:
|
|
upload_file_list.extend(resp.json()["uploaded"])
|
|
duplicate_file_list.extend(resp.json()["duplicates"])
|
|
failed.extend(resp.json()["failed"])
|
|
|
|
continue
|
|
|
|
logger.debug(f"n: {n} fail (attempt {i})")
|
|
|
|
# progress_dialog.Destroy()
|
|
|
|
print(upload_file_list)
|
|
print("dup", duplicate_file_list)
|
|
print("failed", failed)
|
|
|
|
|
|
|
|
wx.CallAfter(
|
|
self.upload_complete_message, upload_file_list, duplicate_file_list, failed, duplicate_files_not_uploaded
|
|
)
|
|
|
|
if self.clear_files_on_upload:
|
|
all_files = upload_file_list + duplicate_file_list + duplicate_files_not_uploaded
|
|
print(all_files)
|
|
self.ClearFiles(event=None, load_dir=True, files=all_files)
|
|
|
|
def upload_complete_message(self, upload_file_list, duplicate_file_list, failed, duplicate_files_not_uploaded):
|
|
wx.MessageBox(
|
|
f"""Files uploaded: {len(upload_file_list)}
|
|
Duplicate files (not uploaded): {len(duplicate_file_list)}
|
|
Failed to upload: {len(failed)}
|
|
Duplicate files (not uploaded): {len(duplicate_files_not_uploaded)}"""
|
|
)
|
|
self.file_duplicate_check = {}
|
|
|
|
def OnDragInit(self, event):
|
|
my_data = wx.FileDataObject()
|
|
# files = pathlib.Path(self.dir).glob("*.dcm")
|
|
for f in self.files:
|
|
my_data.AddFile(str(f))
|
|
|
|
dragSource = wx.DropSource(self.panel)
|
|
dragSource.SetData(my_data)
|
|
|
|
result = dragSource.DoDragDrop(True)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@app.command()
|
|
@app.callback(invoke_without_command=True)
|
|
def runGUI(
|
|
ctx: typer.Context,
|
|
input_dir: Path = "C:\\Temp\\",
|
|
output_dir: Path = "C:\\temp_upload\\",
|
|
silent_fail: bool = False,
|
|
clear_files: bool = True,
|
|
clear_files_on_close: bool = True,
|
|
remove_files_on_annonymisation: bool = True,
|
|
dev: bool = False,
|
|
):
|
|
if "linux" in sys.platform:
|
|
input_dir = Path("/home/ross/temp/in/")
|
|
output_dir = Path("/home/ross/temp/out/")
|
|
|
|
if ctx.invoked_subcommand is None:
|
|
try:
|
|
typer.echo("run gui")
|
|
except AttributeError: # No console
|
|
pass
|
|
launchGUI(
|
|
input_dir,
|
|
output_dir,
|
|
silent_fail,
|
|
clear_files,
|
|
clear_files_on_close,
|
|
dev,
|
|
remove_files_on_annonymisation,
|
|
)
|
|
|
|
|
|
def launchGUI(
|
|
input_dir,
|
|
output_dir,
|
|
silent_fail,
|
|
clear_files,
|
|
clear_files_on_close,
|
|
dev,
|
|
remove_files_on_annonymisation,
|
|
):
|
|
app = AnonGui(
|
|
input_dir=input_dir,
|
|
output_dir=output_dir,
|
|
silent_fail=silent_fail,
|
|
clear_files=clear_files,
|
|
clear_files_on_close=clear_files_on_close,
|
|
dev=dev,
|
|
remove_files_on_annonymisation=remove_files_on_annonymisation,
|
|
)
|
|
app.MainLoop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app()
|