start repo
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
.env/*
|
||||||
|
.venv/*
|
||||||
|
|
||||||
|
.pyc
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pydicom
|
||||||
|
import dicognito.anonymizer
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
|
||||||
|
anonymizer = dicognito.anonymizer.Anonymizer()
|
||||||
|
|
||||||
|
app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
|
def anonymize_file(filepath: Path, new_filepath: Path):
|
||||||
|
with pydicom.dcmread(filepath) as dataset:
|
||||||
|
anonymizer.anonymize(dataset)
|
||||||
|
dataset.save_as(new_filepath)
|
||||||
|
return new_filepath
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def annonymise(filepath: Path, output_dir: Path):
|
||||||
|
|
||||||
|
new_filepath : Path
|
||||||
|
# Get next available filename
|
||||||
|
i : int = 0
|
||||||
|
while os.path.exists(new_filepath := Path(output_dir).joinpath(f"IMG_{i:03}.dcm")):
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
anonymize_file(filepath, new_filepath)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app()
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
block_cipher = None
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['anon_gui.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[],
|
||||||
|
hiddenimports=[],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
win_no_prefer_redirects=False,
|
||||||
|
win_private_assemblies=False,
|
||||||
|
cipher=block_cipher,
|
||||||
|
noarchive=False,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.zipfiles,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='anon_gui',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=False,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
+1130
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
block_cipher = None
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['anon_gui.py'],
|
||||||
|
pathex=[],
|
||||||
|
#binaries=[(r"C:\Users\krugerro\Desktop\rad_tools\.venv\Lib\site-packages\pywin32_system32\pythoncom311.dll", "."),
|
||||||
|
#(r"C:\Users\krugerro\Desktop\rad_tools\.venv\Lib\site-packages\pywin32_system32\pywintypes311.dll", "."),
|
||||||
|
#],
|
||||||
|
datas=[],
|
||||||
|
hiddenimports=[],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
win_no_prefer_redirects=False,
|
||||||
|
win_private_assemblies=False,
|
||||||
|
cipher=block_cipher,
|
||||||
|
noarchive=False,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.zipfiles,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='anon_gui',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import typer
|
||||||
|
import requests
|
||||||
|
|
||||||
|
app = typer.Typer()
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
# client.py
|
||||||
|
LOGIN_URL = "https://www.penracourses.org.uk/accounts/login"
|
||||||
|
# ADD_URL = 'http://localhost/add'
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def login(user: str, password: str):
|
||||||
|
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")
|
||||||
|
print(token)
|
||||||
|
print(rqst.headers)
|
||||||
|
print(rqst.cookies)
|
||||||
|
|
||||||
|
login_success = False
|
||||||
|
if soup.find("button") and soup.find("button").find(string="Login"):
|
||||||
|
print("login fail")
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
print("login success")
|
||||||
|
login_success = True
|
||||||
|
#print(rqst.headers)
|
||||||
|
#print(rsp.content)
|
||||||
|
|
||||||
|
#new = rqst.get(
|
||||||
|
# "https://www.penracourses.org.uk/api/atlas/get_cases_user"
|
||||||
|
#)
|
||||||
|
|
||||||
|
#print(new)
|
||||||
|
#print(new.content)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app()
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
import pydicom
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import sys
|
||||||
|
import glob
|
||||||
|
|
||||||
|
# load the DICOM files
|
||||||
|
files = []
|
||||||
|
print('glob: {}'.format(sys.argv[1]))
|
||||||
|
for fname in glob.glob(sys.argv[1], recursive=False):
|
||||||
|
print("loading: {}".format(fname))
|
||||||
|
files.append(pydicom.dcmread(fname))
|
||||||
|
|
||||||
|
print("file count: {}".format(len(files)))
|
||||||
|
|
||||||
|
# skip files with no SliceLocation (eg scout views)
|
||||||
|
slices = []
|
||||||
|
skipcount = 0
|
||||||
|
for f in files:
|
||||||
|
if hasattr(f, 'SliceLocation'):
|
||||||
|
slices.append(f)
|
||||||
|
else:
|
||||||
|
skipcount = skipcount + 1
|
||||||
|
|
||||||
|
print("skipped, no SliceLocation: {}".format(skipcount))
|
||||||
|
|
||||||
|
# ensure they are in the correct order
|
||||||
|
slices = sorted(slices, key=lambda s: s.SliceLocation)
|
||||||
|
|
||||||
|
# pixel aspects, assuming all slices are the same
|
||||||
|
ps = slices[0].PixelSpacing
|
||||||
|
ss = slices[0].SliceThickness
|
||||||
|
ax_aspect = ps[1]/ps[0]
|
||||||
|
sag_aspect = ps[1]/ss
|
||||||
|
cor_aspect = ss/ps[0]
|
||||||
|
|
||||||
|
# create 3D array
|
||||||
|
img_shape = list(slices[0].pixel_array.shape)
|
||||||
|
img_shape.append(len(slices))
|
||||||
|
img3d = np.zeros(img_shape)
|
||||||
|
|
||||||
|
# fill 3D array with the images from the files
|
||||||
|
for i, s in enumerate(slices):
|
||||||
|
img2d = s.pixel_array
|
||||||
|
img3d[:, :, i] = img2d
|
||||||
|
|
||||||
|
# plot 3 orthogonal slices
|
||||||
|
a1 = plt.subplot(2, 2, 1)
|
||||||
|
plt.imshow(img3d[:, :, img_shape[2]//2])
|
||||||
|
a1.set_aspect(ax_aspect)
|
||||||
|
|
||||||
|
a2 = plt.subplot(2, 2, 2)
|
||||||
|
plt.imshow(img3d[:, img_shape[1]//2, :])
|
||||||
|
a2.set_aspect(sag_aspect)
|
||||||
|
|
||||||
|
a3 = plt.subplot(2, 2, 3)
|
||||||
|
plt.imshow(img3d[img_shape[0]//2, :, :].T)
|
||||||
|
a3.set_aspect(cor_aspect)
|
||||||
|
|
||||||
|
a4 = plt.subplot(2, 2, 4)
|
||||||
|
plt.imshow(img3d[img_shape[0]//2, :, :].T)
|
||||||
|
a4.set_aspect(cor_aspect)
|
||||||
|
|
||||||
|
plt.show()
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from distutils.core import setup # Need this to handle modules
|
||||||
|
import py2exe
|
||||||
|
import math # We have to import all modules used in our program
|
||||||
|
|
||||||
|
import zmq
|
||||||
|
|
||||||
|
setup(console=['anon_gui.py'], py_modules=['anon', 'anon_gui', 'reslice', 'sortList', 'uploader', 'wxDicomViewer'], options={
|
||||||
|
'py2exe': {
|
||||||
|
'includes': ['zmq.backend.cython'],
|
||||||
|
'excludes': ['zmq.libzmq'],
|
||||||
|
'dll_excludes': ['libzmq.pyd'],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data_files=[
|
||||||
|
('lib', (zmq.libzmq.__file__,))
|
||||||
|
])
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
DATA = {
|
||||||
|
0 : ("3", "3", "1"),
|
||||||
|
1 : ("2", "1", "2"),
|
||||||
|
2 : ("1", "2", "3")
|
||||||
|
}
|
||||||
|
|
||||||
|
import wx
|
||||||
|
import wx.lib.mixins.listctrl as listmix
|
||||||
|
from wx.lib.agw import ultimatelistctrl as ULC
|
||||||
|
|
||||||
|
class MyList(ULC.UltimateListCtrl, listmix.ColumnSorterMixin):
|
||||||
|
def __init__(self, parent, columns):
|
||||||
|
ULC.UltimateListCtrl.__init__(self, parent, agwStyle=ULC.ULC_REPORT | ULC.ULC_HAS_VARIABLE_ROW_HEIGHT)
|
||||||
|
self.itemDataMap = DATA
|
||||||
|
listmix.ColumnSorterMixin.__init__(self, columns)
|
||||||
|
self.Bind(wx.EVT_LIST_COL_CLICK, self.OnColumn)
|
||||||
|
|
||||||
|
def OnColumn(self, e):
|
||||||
|
self.Refresh()
|
||||||
|
e.Skip()
|
||||||
|
|
||||||
|
def GetListCtrl(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
class MainWindow(wx.Frame):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
wx.Frame.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
self.list = MyList(self, 3)
|
||||||
|
self.list.InsertColumn(0, "A")
|
||||||
|
self.list.InsertColumn(1, "B")
|
||||||
|
self.list.InsertColumn(2, "C")
|
||||||
|
|
||||||
|
items = DATA.items()
|
||||||
|
for key, data in items:
|
||||||
|
index = self.list.Append(data)
|
||||||
|
self.list.SetItemData(index, key)
|
||||||
|
|
||||||
|
self.Show()
|
||||||
|
|
||||||
|
app = wx.App(False)
|
||||||
|
win = MainWindow(None)
|
||||||
|
app.MainLoop()
|
||||||
+193
@@ -0,0 +1,193 @@
|
|||||||
|
import webview
|
||||||
|
import time
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from datauri import DataURI
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pydicom
|
||||||
|
import dicognito.anonymizer
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
|
||||||
|
# from watchdog.events import LoggingEventHandler
|
||||||
|
from watchdog.events import PatternMatchingEventHandler
|
||||||
|
|
||||||
|
|
||||||
|
anonymizer = dicognito.anonymizer.Anonymizer()
|
||||||
|
|
||||||
|
app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
|
def anonymize_file(filepath):
|
||||||
|
try:
|
||||||
|
with pydicom.dcmread(filepath) as dataset:
|
||||||
|
anonymizer.anonymize(dataset)
|
||||||
|
p = Path(filepath)
|
||||||
|
new_filename = Path(p.parent, f"{p.stem}_anon{p.suffix}")
|
||||||
|
dataset.save_as(new_filename)
|
||||||
|
return new_filename
|
||||||
|
except:
|
||||||
|
# Cannot anonymise so just return the same path
|
||||||
|
return filepath
|
||||||
|
|
||||||
|
|
||||||
|
class PythonJavascriptApi:
|
||||||
|
def __init__(self):
|
||||||
|
self.cancel_heavy_stuff_flag = False
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
response = {"message": "Hello from Python {0}".format(sys.version)}
|
||||||
|
return response
|
||||||
|
|
||||||
|
def sayHelloTo(self, name):
|
||||||
|
response = {"message": "Hello {0}!".format(name)}
|
||||||
|
return response
|
||||||
|
|
||||||
|
def error(self):
|
||||||
|
raise Exception("This is a Python exception")
|
||||||
|
|
||||||
|
|
||||||
|
class Watcher:
|
||||||
|
def __init__(self, window, anonymise: bool) -> None:
|
||||||
|
self.window = window
|
||||||
|
self.anonymise = anonymise
|
||||||
|
|
||||||
|
def on_created(self, event):
|
||||||
|
print(f"hey, {event.src_path} has been created!")
|
||||||
|
path = event.src_path
|
||||||
|
if self.anonymise:
|
||||||
|
path = anonymize_file(path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
file_uri = DataURI.from_file(path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("File deleted...")
|
||||||
|
return
|
||||||
|
|
||||||
|
file_name = Path(path).name
|
||||||
|
self.injectFile(file_uri, file_name)
|
||||||
|
|
||||||
|
def on_deleted(self, event):
|
||||||
|
print(f"what the f**k! Someone deleted {event.src_path}!")
|
||||||
|
|
||||||
|
def on_modified(self, event):
|
||||||
|
print(f"hey buddy, {event.src_path} has been modified")
|
||||||
|
|
||||||
|
def on_moved(self, event):
|
||||||
|
print(f"ok ok ok, someone moved {event.src_path} to {event.dest_path}")
|
||||||
|
|
||||||
|
def injectFile(self, file_uri: DataURI, file_name: str):
|
||||||
|
current_url = webview.windows[0].get_current_url()
|
||||||
|
if current_url == "https://www.penracourses.org.uk/rapids/create/":
|
||||||
|
print(f"inject {file_name}")
|
||||||
|
self.window.evaluate_js(
|
||||||
|
f"""
|
||||||
|
function urltoFile(url, filename, mimeType){{
|
||||||
|
return (fetch(url)
|
||||||
|
.then(function(res){{ return res.arrayBuffer(); }})
|
||||||
|
.then(function(buf){{return new File([buf], filename,{{type:mimeType}});}})
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
$(document).ready(() => {{
|
||||||
|
//Usage example:
|
||||||
|
urltoFile('{file_uri}', '{file_name}', '{file_uri.mimetype}')
|
||||||
|
.then(function(file){{
|
||||||
|
|
||||||
|
console.log(file);
|
||||||
|
|
||||||
|
//inputs = $("#rapid-form input[type=file][id^=id_images-]");
|
||||||
|
inputs = extendInputs(1);
|
||||||
|
console.log(inputs);
|
||||||
|
el = inputs.get(0);
|
||||||
|
console.log(el);
|
||||||
|
|
||||||
|
let dT = new DataTransfer();
|
||||||
|
dT.clearData();
|
||||||
|
dT.items.add(file);
|
||||||
|
|
||||||
|
el.files = dT.files;
|
||||||
|
|
||||||
|
// Manually fire the change event (qt doesn't seem to...)
|
||||||
|
$(el).change();
|
||||||
|
|
||||||
|
|
||||||
|
}});
|
||||||
|
|
||||||
|
}});
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def anatomy(filepath: str):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def rapid(watch_path: Path = "/home/ross/watch", anonymise: bool = True):
|
||||||
|
|
||||||
|
api = PythonJavascriptApi()
|
||||||
|
window = webview.create_window(
|
||||||
|
"Upload Rapid", "https://www.penracourses.org.uk/rapids/create/", js_api=api
|
||||||
|
)
|
||||||
|
|
||||||
|
# def on_loaded():
|
||||||
|
# print('DOM is ready')
|
||||||
|
# current_url = webview.windows[0].get_current_url()
|
||||||
|
|
||||||
|
# if current_url == "https://www.penracourses.org.uk/rapids/create/":
|
||||||
|
# #window.evaluate_js("alert('tent')")
|
||||||
|
|
||||||
|
# for file_uri, file_name in uri_name_list:
|
||||||
|
# #time.sleep(2)
|
||||||
|
|
||||||
|
# ## unsubscribe event listener
|
||||||
|
# #webview.windows[0].loaded -= on_loaded
|
||||||
|
# #webview.windows[0].load_url('https://google.com')
|
||||||
|
|
||||||
|
# window.loaded += on_loaded
|
||||||
|
# window = upload_rapid(["/home/ross/Downloads/DICOM CT/DICOM CXR/STD1/SER1/CXR.dcm"])
|
||||||
|
|
||||||
|
patterns = ["*"]
|
||||||
|
ignore_patterns = ["*anon*"]
|
||||||
|
ignore_directories = False
|
||||||
|
case_sensitive = True
|
||||||
|
my_event_handler = PatternMatchingEventHandler(
|
||||||
|
patterns, ignore_patterns, ignore_directories, case_sensitive
|
||||||
|
)
|
||||||
|
|
||||||
|
watcher = Watcher(window, anonymise)
|
||||||
|
my_event_handler.on_created = watcher.on_created
|
||||||
|
my_event_handler.on_deleted = watcher.on_deleted
|
||||||
|
my_event_handler.on_modified = watcher.on_modified
|
||||||
|
my_event_handler.on_moved = watcher.on_moved
|
||||||
|
my_observer = Observer()
|
||||||
|
|
||||||
|
Path(watch_path).mkdir(parents=True, exist_ok=True)
|
||||||
|
my_observer.schedule(my_event_handler, watch_path, recursive=True)
|
||||||
|
|
||||||
|
my_observer.start()
|
||||||
|
# try:
|
||||||
|
# while True:
|
||||||
|
# time.sleep(1)
|
||||||
|
# except KeyboardInterrupt:
|
||||||
|
# my_observer.stop()
|
||||||
|
# my_observer.join()
|
||||||
|
webview.start(None, window, debug=True, gui="qt")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# f = "/media/ross/f6200b79-c343-4882-a245-28fc06d43006/Downloads/temp/breasts.jpg"
|
||||||
|
f = "/home/ross/Downloads/DICOM CT/DICOM CXR/STD1/SER1/CXR.dcm"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app()
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
# ===================================================================
|
||||||
|
# imViewer-Simple.py
|
||||||
|
#
|
||||||
|
# An example program that opens uncompressed DICOM images and
|
||||||
|
# converts them via numPy and PIL to be viewed in wxWidgets GUI
|
||||||
|
# apps. The conversion is currently:
|
||||||
|
#
|
||||||
|
# pydicom->NumPy->PIL->wxPython.Image->wxPython.Bitmap
|
||||||
|
#
|
||||||
|
# Gruesome but it mostly works. Surely there is at least one
|
||||||
|
# of these steps that could be eliminated (probably PIL) but
|
||||||
|
# haven't tried that yet and I may want some of the PIL manipulation
|
||||||
|
# functions.
|
||||||
|
#
|
||||||
|
# This won't handle RLE, embedded JPEG-Lossy, JPEG-lossless,
|
||||||
|
# JPEG2000, old ACR/NEMA files, or anything wierd. Also doesn't
|
||||||
|
# handle some RGB images that I tried.
|
||||||
|
#
|
||||||
|
# Have added Adit Panchal's LUT code. It helps a lot, but needs
|
||||||
|
# to be further generalized. Added test for window and/or level
|
||||||
|
# as 'list' type - crude, but it worked for a bunch of old MR and
|
||||||
|
# CT slices I have.
|
||||||
|
#
|
||||||
|
# Testing: moderate
|
||||||
|
# Tested on Windows 7 and MacOS 10.13 using numpy 1.15.2,
|
||||||
|
# Pillow 5.2.0, Python 2.7.13 / 3.6.5, and wxPython 4.0.3
|
||||||
|
#
|
||||||
|
# Originally written by Dave Witten: Nov. 11, 2009
|
||||||
|
# Updated by Aditya Panchal: Oct. 9, 2018
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
import pathlib
|
||||||
|
from PIL import Image
|
||||||
|
import dicom2jpg
|
||||||
|
import pydicom
|
||||||
|
import wx
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
have_PIL = True
|
||||||
|
try:
|
||||||
|
import PIL.Image
|
||||||
|
except ImportError:
|
||||||
|
have_PIL = False
|
||||||
|
|
||||||
|
have_numpy = True
|
||||||
|
try:
|
||||||
|
import numpy as np
|
||||||
|
except ImportError:
|
||||||
|
have_numpy = False
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# Initialize image capabilities.
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
|
||||||
|
wx.InitAllImageHandlers()
|
||||||
|
|
||||||
|
|
||||||
|
def MsgDlg(window, string, caption='OFAImage', style=wx.YES_NO | wx.CANCEL):
|
||||||
|
"""Common MessageDialog."""
|
||||||
|
dlg = wx.MessageDialog(window, string, caption, style)
|
||||||
|
result = dlg.ShowModal()
|
||||||
|
dlg.Destroy()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ViewerFrame(wx.Frame):
|
||||||
|
"""Class for main window."""
|
||||||
|
|
||||||
|
def __init__(self, parent, title, initial_file=None):
|
||||||
|
"""Create the pydicom image example's main frame window."""
|
||||||
|
|
||||||
|
style = wx.DEFAULT_FRAME_STYLE | wx.SUNKEN_BORDER | wx.CLIP_CHILDREN
|
||||||
|
|
||||||
|
wx.Frame.__init__(
|
||||||
|
self,
|
||||||
|
parent,
|
||||||
|
id=-1,
|
||||||
|
title="",
|
||||||
|
pos=wx.DefaultPosition,
|
||||||
|
size=wx.Size(1024, 768),
|
||||||
|
style=style)
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# Set up the menubar.
|
||||||
|
# --------------------------------------------------------
|
||||||
|
self.mainmenu = wx.MenuBar()
|
||||||
|
|
||||||
|
# Make the 'File' menu.
|
||||||
|
menu = wx.Menu()
|
||||||
|
item = menu.Append(wx.ID_ANY, '&Open...\tCtrl+O', 'Open file for editing')
|
||||||
|
self.Bind(wx.EVT_MENU, self.OnFileOpen, item)
|
||||||
|
item = menu.Append(wx.ID_ANY, 'E&xit', 'Exit Program')
|
||||||
|
self.Bind(wx.EVT_MENU, self.OnFileExit, item)
|
||||||
|
self.mainmenu.Append(menu, '&File')
|
||||||
|
|
||||||
|
# Attach the menu bar to the window.
|
||||||
|
self.SetMenuBar(self.mainmenu)
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# Set up the main splitter window.
|
||||||
|
# --------------------------------------------------------
|
||||||
|
self.mainSplitter = wx.SplitterWindow(self)
|
||||||
|
self.mainSplitter.SetMinimumPaneSize(1)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# Create the folderTreeView on the left.
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
dsstyle = wx.TR_LINES_AT_ROOT | wx.TR_HAS_BUTTONS
|
||||||
|
self.dsTreeView = wx.TreeCtrl(self.mainSplitter, style=dsstyle)
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# Create the ImageView on the right pane.
|
||||||
|
# --------------------------------------------------------
|
||||||
|
imstyle = wx.VSCROLL | wx.HSCROLL | wx.CLIP_CHILDREN
|
||||||
|
self.imView = wx.Panel(self.mainSplitter, style=imstyle)
|
||||||
|
|
||||||
|
self.imView.Bind(wx.EVT_PAINT, self.OnPaint)
|
||||||
|
self.imView.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
|
||||||
|
|
||||||
|
self.imView.Bind(wx.EVT_SIZE, self.OnSize)
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# Install the splitter panes.
|
||||||
|
# --------------------------------------------------------
|
||||||
|
self.mainSplitter.SplitVertically(self.dsTreeView, self.imView)
|
||||||
|
self.mainSplitter.SetSashPosition(300, True)
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# Initialize some values
|
||||||
|
# --------------------------------------------------------
|
||||||
|
self.dcmdsRoot = False
|
||||||
|
self.foldersRoot = False
|
||||||
|
self.loadCentered = True
|
||||||
|
self.bitmap = None
|
||||||
|
self.Show(True)
|
||||||
|
|
||||||
|
print(initial_file)
|
||||||
|
|
||||||
|
if initial_file is not None:
|
||||||
|
path = pathlib.Path(initial_file)
|
||||||
|
self.show_file2(path.name, str(path))
|
||||||
|
|
||||||
|
def OnFileExit(self, event):
|
||||||
|
"""Exits the program."""
|
||||||
|
self.Destroy()
|
||||||
|
event.Skip()
|
||||||
|
|
||||||
|
def OnSize(self, event):
|
||||||
|
"""Window 'size' event."""
|
||||||
|
self.Refresh()
|
||||||
|
|
||||||
|
def OnEraseBackground(self, event):
|
||||||
|
"""Window 'erase background' event."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def populateTree(self, ds):
|
||||||
|
""" Populate the tree in the left window with the [desired]
|
||||||
|
dataset values"""
|
||||||
|
if not self.dcmdsRoot:
|
||||||
|
self.dcmdsRoot = self.dsTreeView.AddRoot(text="DICOM Objects")
|
||||||
|
else:
|
||||||
|
self.dsTreeView.DeleteChildren(self.dcmdsRoot)
|
||||||
|
self.recurse_tree(ds, self.dcmdsRoot)
|
||||||
|
self.dsTreeView.ExpandAll()
|
||||||
|
|
||||||
|
def recurse_tree(self, ds, parent, hide=False):
|
||||||
|
""" order the dicom tags """
|
||||||
|
for data_element in ds:
|
||||||
|
if isinstance(data_element.value, str):
|
||||||
|
text = str(data_element)
|
||||||
|
ip = self.dsTreeView.AppendItem(parent, text=text)
|
||||||
|
else:
|
||||||
|
ip = self.dsTreeView.AppendItem(parent, text=str(data_element))
|
||||||
|
|
||||||
|
if data_element.VR == "SQ":
|
||||||
|
for i, ds in enumerate(data_element.value):
|
||||||
|
item_describe = data_element.name.replace(" Sequence", "")
|
||||||
|
item_text = "%s %d" % (item_describe, i + 1)
|
||||||
|
rjust = item_text.rjust(128)
|
||||||
|
parentNodeID = self.dsTreeView.AppendItem(ip, text=rjust)
|
||||||
|
self.recurse_tree(ds, parentNodeID)
|
||||||
|
|
||||||
|
# --- Most of what is important happens below this line ---------------------
|
||||||
|
|
||||||
|
def OnFileOpen(self, event):
|
||||||
|
"""Opens a selected file."""
|
||||||
|
msg = 'Choose a file to add.'
|
||||||
|
dlg = wx.FileDialog(self, msg, '', '', '*.*', wx.FD_OPEN)
|
||||||
|
if dlg.ShowModal() == wx.ID_OK:
|
||||||
|
fullPath = dlg.GetPath()
|
||||||
|
imageFile = dlg.GetFilename()
|
||||||
|
# checkDICMHeader()
|
||||||
|
self.show_file2(imageFile, fullPath)
|
||||||
|
|
||||||
|
def OnPaint(self, event):
|
||||||
|
"""Window 'paint' event."""
|
||||||
|
dc = wx.PaintDC(self.imView)
|
||||||
|
dc = wx.BufferedDC(dc)
|
||||||
|
|
||||||
|
# paint a background just so it isn't *so* boring.
|
||||||
|
dc.SetBackground(wx.Brush("WHITE"))
|
||||||
|
dc.Clear()
|
||||||
|
dc.SetBrush(wx.Brush("GREY", wx.CROSSDIAG_HATCH))
|
||||||
|
windowsize = self.imView.GetSize()
|
||||||
|
dc.DrawRectangle(0, 0, windowsize[0], windowsize[1])
|
||||||
|
bmpX0 = 0
|
||||||
|
bmpY0 = 0
|
||||||
|
if self.bitmap is not None:
|
||||||
|
if self.loadCentered:
|
||||||
|
bmpX0 = (windowsize[0] - self.bitmap.Width) / 2
|
||||||
|
bmpY0 = (windowsize[1] - self.bitmap.Height) / 2
|
||||||
|
dc.DrawBitmap(self.bitmap, bmpX0, bmpY0, False)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# ViewerFrame.ConvertWXToPIL()
|
||||||
|
# Expropriated from Andrea Gavana's
|
||||||
|
# ShapedButton.py in the wxPython dist
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
def ConvertWXToPIL(self, bmp):
|
||||||
|
""" Convert wx.Image Into PIL Image. """
|
||||||
|
width = bmp.GetWidth()
|
||||||
|
height = bmp.GetHeight()
|
||||||
|
im = wx.EmptyImage(width, height)
|
||||||
|
im.fromarray("RGBA", (width, height), bmp.GetData())
|
||||||
|
return im
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# ViewerFrame.ConvertPILToWX()
|
||||||
|
# Expropriated from Andrea Gavana's
|
||||||
|
# ShapedButton.py in the wxPython dist
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
def ConvertPILToWX(self, pil, alpha=True):
|
||||||
|
""" Convert PIL Image Into wx.Image. """
|
||||||
|
if alpha:
|
||||||
|
image = wx.Image(pil.size[0], pil.size[1], clear=True)
|
||||||
|
image.SetData(pil.convert("RGB").tobytes())
|
||||||
|
image.SetAlpha(pil.convert("RGBA").tobytes()[3::4])
|
||||||
|
else:
|
||||||
|
image = wx.Image(pil.size[0], pil.size[1], clear=True)
|
||||||
|
new_image = pil.convert('RGB')
|
||||||
|
data = new_image.tobytes()
|
||||||
|
image.SetData(data)
|
||||||
|
return image
|
||||||
|
|
||||||
|
def get_LUT_value(self, data, window, level):
|
||||||
|
"""Apply the RGB Look-Up Table for the given
|
||||||
|
data and window/level value."""
|
||||||
|
if not have_numpy:
|
||||||
|
raise ImportError("Numpy is not available. "
|
||||||
|
"See http://numpy.scipy.org/ "
|
||||||
|
"to download and install")
|
||||||
|
|
||||||
|
win = window
|
||||||
|
lvl = level
|
||||||
|
|
||||||
|
e = [
|
||||||
|
0, 255,
|
||||||
|
lambda data: ((data - (lvl - 0.5)) / (win - 1) + 0.5) * (255 - 0)
|
||||||
|
]
|
||||||
|
return np.piecewise(data, [
|
||||||
|
data <= (lvl - 0.5 - (win - 1) / 2),
|
||||||
|
data > (lvl - 0.5 + (win - 1) / 2)
|
||||||
|
], e)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
# ViewerFrame.loadPIL_LUT(dataset)
|
||||||
|
# Display an image using the Python Imaging Library (PIL)
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
def loadPIL_LUT(self, dataset):
|
||||||
|
if not have_PIL:
|
||||||
|
raise ImportError("Python Imaging Library is not available."
|
||||||
|
" See http://www.pythonware.com/products/pil/"
|
||||||
|
" to download and install")
|
||||||
|
if 'PixelData' not in dataset:
|
||||||
|
raise TypeError("Cannot show image -- "
|
||||||
|
"DICOM dataset does not have pixel data")
|
||||||
|
|
||||||
|
# can only apply LUT if these values exist
|
||||||
|
if ('WindowWidth' not in dataset) or ('WindowCenter' not in dataset):
|
||||||
|
bits = dataset.BitsAllocated
|
||||||
|
samples = dataset.SamplesPerPixel
|
||||||
|
if bits == 8 and samples == 1:
|
||||||
|
mode = "L"
|
||||||
|
elif bits == 8 and samples == 3:
|
||||||
|
mode = "RGB"
|
||||||
|
# not sure about this -- PIL source says is
|
||||||
|
# 'experimental' and no documentation.
|
||||||
|
elif bits == 16:
|
||||||
|
# Also, should bytes swap depending
|
||||||
|
# on endian of file and system??
|
||||||
|
mode = "I;16"
|
||||||
|
else:
|
||||||
|
msg = "Don't know PIL mode for %d BitsAllocated" % (bits)
|
||||||
|
msg += " and %d SamplesPerPixel" % (samples)
|
||||||
|
raise TypeError(msg)
|
||||||
|
size = (dataset.Columns, dataset.Rows)
|
||||||
|
|
||||||
|
# Recommended to specify all details by
|
||||||
|
# http://www.pythonware.com/library/pil/handbook/image.htm
|
||||||
|
im = PIL.Image.frombuffer(mode, size, dataset.PixelData, "raw",
|
||||||
|
mode, 0, 1)
|
||||||
|
else:
|
||||||
|
ew = dataset['WindowWidth']
|
||||||
|
ec = dataset['WindowCenter']
|
||||||
|
ww = int(ew.value[0] if ew.VM > 1 else ew.value)
|
||||||
|
wc = int(ec.value[0] if ec.VM > 1 else ec.value)
|
||||||
|
image = self.get_LUT_value(dataset.pixel_array, ww, wc)
|
||||||
|
|
||||||
|
# Convert mode to L since LUT has only 256 values:
|
||||||
|
# http://www.pythonware.com/library/pil/handbook/image.htm
|
||||||
|
im = PIL.Image.fromarray(image).convert('L')
|
||||||
|
return im
|
||||||
|
|
||||||
|
def show_file(self, imageFile, fullPath):
|
||||||
|
""" Load the DICOM file, make sure it contains at least one
|
||||||
|
image, and set it up for display by OnPaint(). ** be
|
||||||
|
careful not to pass a unicode string to read_file or it will
|
||||||
|
give you 'fp object does not have a defer_size attribute,
|
||||||
|
or some such."""
|
||||||
|
ds = pydicom.dcmread(str(fullPath))
|
||||||
|
|
||||||
|
# change strings to unicode
|
||||||
|
ds.decode()
|
||||||
|
self.populateTree(ds)
|
||||||
|
if 'PixelData' in ds:
|
||||||
|
self.dImage = self.loadPIL_LUT(ds)
|
||||||
|
if self.dImage is not None:
|
||||||
|
tmpImage = self.ConvertPILToWX(self.dImage, False)
|
||||||
|
self.bitmap = wx.Bitmap(tmpImage)
|
||||||
|
self.Refresh()
|
||||||
|
|
||||||
|
|
||||||
|
def show_file2(self, imageFile, fullPath):
|
||||||
|
""" Load the DICOM file, make sure it contains at least one
|
||||||
|
image, and set it up for display by OnPaint(). ** be
|
||||||
|
careful not to pass a unicode string to read_file or it will
|
||||||
|
give you 'fp object does not have a defer_size attribute,
|
||||||
|
or some such."""
|
||||||
|
ds = pydicom.dcmread(str(fullPath))
|
||||||
|
|
||||||
|
# change strings to unicode
|
||||||
|
ds.decode()
|
||||||
|
self.populateTree(ds)
|
||||||
|
|
||||||
|
with open(fullPath, "rb") as f:
|
||||||
|
img_data = dicom2jpg.io2img(BytesIO(f.read()))
|
||||||
|
img = Image.fromarray(img_data)
|
||||||
|
#img.show()
|
||||||
|
|
||||||
|
tmpImage = self.ConvertPILToWX(img, False)
|
||||||
|
self.bitmap = wx.Bitmap(tmpImage)
|
||||||
|
self.Refresh()
|
||||||
|
|
||||||
|
|
||||||
|
# ------ This is just the initialization of the App ----
|
||||||
|
|
||||||
|
# =======================================================
|
||||||
|
# The main App Class.
|
||||||
|
# =======================================================
|
||||||
|
|
||||||
|
|
||||||
|
class BasicDicomViewer(wx.App):
|
||||||
|
"""Image Application."""
|
||||||
|
def __init__(self, initial_file=None, redirect=False, filename=None, useBestVisual=False, clearSigInt=True):
|
||||||
|
self.initial_file = initial_file
|
||||||
|
super().__init__(redirect=redirect, filename=filename, useBestVisual=useBestVisual, clearSigInt=clearSigInt)
|
||||||
|
|
||||||
|
|
||||||
|
def OnInit(self):
|
||||||
|
"""Create the Image Application."""
|
||||||
|
print(1)
|
||||||
|
ViewerFrame(None, 'Simple Dicom Viewer', initial_file=self.initial_file)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------
|
||||||
|
# If this file is running as main or a
|
||||||
|
# standalone test, begin execution here.
|
||||||
|
# ------------------------------------------------------
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app = BasicDicomViewer()
|
||||||
|
app.MainLoop()
|
||||||
Reference in New Issue
Block a user