Skip to content

Commit

Permalink
Merge pull request #834 from pymedphys/structure-dedupe
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonBiggs committed May 15, 2020
2 parents 7982fbc + 2327064 commit 8b9284a
Show file tree
Hide file tree
Showing 12 changed files with 904 additions and 4 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Expand Up @@ -33,6 +33,7 @@
"Elekta",
"ICOM",
"IEXEC",
"IMPORTABLES",
"IMRT",
"INET",
"IREAD",
Expand Down Expand Up @@ -100,6 +101,7 @@
"dcmread",
"decubitus",
"dedupe",
"deduplication",
"deidentified",
"deps",
"deque",
Expand All @@ -116,6 +118,7 @@
"dtype",
"dtypes",
"einsum",
"elec",
"electronfactors",
"elems",
"erfinv",
Expand Down
2 changes: 0 additions & 2 deletions examples/protyping/dicom/dedupe-contour/001-scratch.ipynb
Expand Up @@ -277,8 +277,6 @@
" stacked_coords = np.round(stacked_coords, 1)\n",
" stacked_coords = stacked_coords.tolist()\n",
" \n",
" print(stacked_coords)\n",
" \n",
" new_contour_data.append(stacked_coords)\n",
" \n",
" dicom_format_coords_by_z[z] = new_contour_data"
Expand Down
310 changes: 310 additions & 0 deletions examples/protyping/dicom/dedupe-contour/002-scratch.ipynb
@@ -0,0 +1,310 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import copy\n",
"\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"\n",
"import shapely\n",
"import shapely.ops\n",
"\n",
"import pydicom\n",
"\n",
"import pymedphys"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dcm_path = pymedphys.data_path(\"dedupe-contour.dcm\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"CONTOUR_GEOMETRIC_TYPE = 'CLOSED_PLANAR'"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dcm = pydicom.read_file(str(dcm_path), force=True)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def extract_contours_and_image_sequences(contour_sequence):\n",
" contours_by_z = {}\n",
" image_sequence_by_z = {}\n",
"\n",
" expected_contour_keys = {\n",
" pydicom.tag.Tag(*tag)\n",
" for tag in [(0x3006, 0x0050), (0x3006, 0x0046), (0x3006, 0x0042), (0x3006, 0x0016)]\n",
" }\n",
"\n",
" for contour in contour_sequence:\n",
" if contour.ContourGeometricType != CONTOUR_GEOMETRIC_TYPE:\n",
" raise ValueError(f\"Only {CONTOUR_GEOMETRIC_TYPE} type is supported\")\n",
"\n",
" if set(contour.keys()) != expected_contour_keys:\n",
" raise ValueError(\"Unexpected contour sequence format\")\n",
"\n",
" contour_data = contour.ContourData\n",
"\n",
" x = np.array(contour_data[0::3])\n",
" y = np.array(contour_data[1::3])\n",
" z = np.array(contour_data[2::3])\n",
"\n",
" unique_z = np.unique(z)\n",
"\n",
" if len(unique_z) != 1:\n",
" raise ValueError(\"All z values should be equal\")\n",
"\n",
" z = unique_z[0]\n",
" polygon = shapely.geometry.Polygon(zip(x, y))\n",
"\n",
" try:\n",
" contours_by_z[z].append(polygon)\n",
" except KeyError:\n",
" contours_by_z[z] = [polygon]\n",
"\n",
" try:\n",
" image_sequence_by_z[z].append(contour.ContourImageSequence)\n",
" except KeyError:\n",
" image_sequence_by_z[z] = [contour.ContourImageSequence]\n",
" \n",
" \n",
" return contours_by_z, image_sequence_by_z"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def collapse_image_sequence(image_sequence_by_z):\n",
" image_sequence_by_z_collapsed = {}\n",
"\n",
" expected_image_sequence_keys = {\n",
" pydicom.tag.Tag(*tag)\n",
" for tag in [(0x0008, 0x1150), (0x0008, 0x1155)]\n",
" }\n",
"\n",
" for z, image_sequences in image_sequence_by_z.items():\n",
" values = set()\n",
"\n",
" for image_sequence in image_sequences:\n",
" if len(image_sequence) != 1:\n",
" raise ValueError(\"Expected only one item per image sequence\")\n",
"\n",
" image_sequence_item = image_sequence[0]\n",
"\n",
" if set(image_sequence_item.keys()) != expected_image_sequence_keys:\n",
" raise ValueError(\"Unexpected contour image sequence format\")\n",
"\n",
" values.add((\n",
" image_sequence_item.ReferencedSOPClassUID,\n",
" image_sequence_item.ReferencedSOPInstanceUID,\n",
" ))\n",
"\n",
" if len(values) != 1:\n",
" raise ValueError(\"Each z value should only point to one image slice\")\n",
"\n",
" image_sequence_by_z_collapsed[z] = image_sequences[0]\n",
" \n",
" return image_sequence_by_z_collapsed"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def merge_contours(contours_by_z):\n",
" all_merged = {}\n",
"\n",
" for z, contours in contours_by_z.items():\n",
" all_merged[z] = shapely.ops.unary_union(contours)\n",
" \n",
" return all_merged"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def get_coords_from_polygon(polygon):\n",
" return polygon.exterior.coords.xy"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def get_coords_from_multipolygon(multipolygon):\n",
" return [\n",
" get_coords_from_polygon(item) for item in multipolygon\n",
" ]"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def get_coords_from_polygon_or_multipolygon(polygon_or_multipolygon):\n",
" try:\n",
" return [get_coords_from_polygon(polygon_or_multipolygon)]\n",
" except AttributeError:\n",
" return get_coords_from_multipolygon(polygon_or_multipolygon)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def format_coords_for_dicom(all_merged):\n",
" dicom_format_coords_by_z = {}\n",
"\n",
" for z, merged in all_merged.items():\n",
" coords = get_coords_from_polygon_or_multipolygon(merged)\n",
" new_contour_data = []\n",
" for coord in coords:\n",
" stacked_coords = np.hstack(list(zip(coord[0], coord[1], z * np.ones_like(coord[1]))))\n",
" stacked_coords = np.round(stacked_coords, 1)\n",
" stacked_coords = stacked_coords.tolist()\n",
"\n",
" new_contour_data.append(stacked_coords)\n",
"\n",
" dicom_format_coords_by_z[z] = new_contour_data\n",
" \n",
" return dicom_format_coords_by_z"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def create_new_contour_sequence(dicom_format_coords_by_z, image_sequence_by_z_collapsed):\n",
" new_contour_sequence = pydicom.sequence.Sequence()\n",
"\n",
" for z, contour_items in dicom_format_coords_by_z.items():\n",
" contour_image_sequence = image_sequence_by_z_collapsed[z]\n",
" for contour_data in contour_items:\n",
" new_contour_dataset = pydicom.dataset.Dataset()\n",
" new_contour_dataset.ContourGeometricType = CONTOUR_GEOMETRIC_TYPE\n",
" if len(contour_data) % 3 != 0:\n",
" raise ValueError(\"The contour points should be divisible by 3\")\n",
"\n",
" new_contour_dataset.NumberOfContourPoints = len(contour_data) // 3\n",
" new_contour_dataset.ContourImageSequence = contour_image_sequence\n",
" new_contour_dataset.ContourData = contour_data\n",
"\n",
" new_contour_sequence.append(new_contour_dataset)\n",
" \n",
" return new_contour_sequence\n",
" \n",
" "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def concatenate_contours_within_sequence(roi_contour_sequence, inplace=False):\n",
" contour_sequence = roi_contour_sequence.ContourSequence\n",
" contours_by_z, image_sequence_by_z = extract_contours_and_image_sequences(contour_sequence)\n",
" \n",
" image_sequence_by_z_collapsed = collapse_image_sequence(image_sequence_by_z)\n",
" all_merged = merge_contours(contours_by_z)\n",
" \n",
" dicom_format_coords_by_z = format_coords_for_dicom(all_merged)\n",
" \n",
" new_contour_sequence = create_new_contour_sequence(\n",
" dicom_format_coords_by_z, image_sequence_by_z_collapsed)\n",
" \n",
" if not inplace:\n",
" new_roi_contour_sequence = copy.copy(roi_contour_sequence)\n",
" new_roi_contour_sequence.ContourSequence = new_contour_sequence\n",
" \n",
" return new_roi_contour_sequence\n",
" else:\n",
" roi_contour_sequence.ContourSequence = new_contour_sequence "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"roi_contour_sequences = dcm.ROIContourSequence\n",
"\n",
"for item in roi_contour_sequences:\n",
" concatenate_contours_within_sequence(item, inplace=True)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dcm.save_as('output.dcm')"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "pymedphys-master",
"language": "python",
"name": "pymedphys-master"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}

0 comments on commit 8b9284a

Please sign in to comment.