Skip to content

Commit

Permalink
Add basic image view and controls (#359)
Browse files Browse the repository at this point in the history
* feat: Add basic image view and controls

* fix: subject selection bug after having no data objects selected

* fix: clipping range issue; don't use hardcoded values

* refactor: reorganize imageSync methods

* fix: Set default value for `imageViewAxis`

* fix: prevent error when enabling constraints layer over images

* feat: Add basic shape intersection cutter

* feat: Add particle filtering when intersecting shape

* fix: Change default window width and level

* feat: Allow cropping to intersection area

* fix: make particle filter work with image cropping enabled

* fix: change window width range to [0, (max-min)]

* fix: upgrade vtk.js to include bug fix Kitware/vtk-js#3037
  • Loading branch information
annehaley authored Mar 29, 2024
1 parent 5e8d2f6 commit 4313760
Show file tree
Hide file tree
Showing 10 changed files with 692 additions and 138 deletions.
2 changes: 1 addition & 1 deletion web/shapeworks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"kd-tree-javascript": "1.0.0",
"lodash": "^4.17.21",
"shader-loader": "^1.3.1",
"vtk.js": ">=29.4.6",
"vtk.js": ">=29.11.1",
"vue": "^2.7.14",
"vue-echarts": "^6.2.4",
"vue-router": "^3.2.0",
Expand Down
10 changes: 7 additions & 3 deletions web/shapeworks/src/components/DataList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,15 @@ export default {
loadingState.value = false;
}
function updateSelectedObjects(){
function updateSelectedObjects() {
selectedDataObjects.value = allDataObjectsInDataset.value.filter(
(dataObject) => {
return selectedAnatomies.value.includes(dataObject.anatomy_type) &&
selectedSubjects.value.includes(dataObject.subject)
return (
(
!selectedAnatomies.value.length ||
selectedAnatomies.value.includes(dataObject.anatomy_type)
) && selectedSubjects.value.includes(dataObject.subject)
)
}
)
}
Expand Down
261 changes: 194 additions & 67 deletions web/shapeworks/src/components/RenderControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,18 @@ import {
showDifferenceFromMeanMode,
analysisFilesShown,
showGoodBadParticlesMode,
goodBadMaxAngle
goodBadMaxAngle,
imageViewMode,
imageViewIntersectMode,
imageViewIntersectCropMode,
imageViewAxis,
imageViewSlices,
imageViewSliceRanges,
imageViewCroppedSliceRanges,
imageViewWindow,
imageViewWindowRange,
imageViewLevel,
imageViewLevelRange,
} from '@/store';
Expand Down Expand Up @@ -48,6 +59,7 @@ export default {
yMinus: '-Y',
zPlus: '+Z',
zMinus: '-Z',
axes: ['X', 'Y', 'Z'],
},
{
text: 'Medical',
Expand All @@ -58,16 +70,16 @@ export default {
yMinus: 'A',
zPlus: 'S',
zMinus: 'I',
axes: ['L', 'P', 'S'],
}
]
const axisSystem = ref(axisSystemOptions.find(
(system) => system.value === 'xyz'
))
const axisSystem = ref()
function changeAxisSystem(newSystemValue: string){
const newSystem = axisSystemOptions.find(
(system) => system.value == newSystemValue
)
axisSystem.value = newSystem
if(newSystem){
orientationIndicator.value.setXPlusFaceProperty({
text: newSystem.xPlus
Expand Down Expand Up @@ -161,12 +173,72 @@ export default {
return props.currentTab === 'analyze' && analysisFilesShown.value?.length;
})
const imageIntersectAllowed = computed(() => {
// TODO: Add other applicable layers here
return layersShown.value.includes('Groomed')
})
const imageViewSlice = computed(() => {
if (!imageViewAxis.value) {
return undefined
} else if (['X', 'L'].includes(imageViewAxis.value)) {
return imageViewSlices.value.x
} else if (['Y', 'P'].includes(imageViewAxis.value)) {
return imageViewSlices.value.y
} else if (['Z', 'S'].includes(imageViewAxis.value)) {
return imageViewSlices.value.z
}
})
const imageViewSliceRange = computed(() => {
if (!imageViewAxis.value) {
return undefined
} else if (['X', 'L'].includes(imageViewAxis.value)) {
return imageViewIntersectCropMode.value ? imageViewCroppedSliceRanges.value.x : imageViewSliceRanges.value.x
} else if (['Y', 'P'].includes(imageViewAxis.value)) {
return imageViewIntersectCropMode.value ? imageViewCroppedSliceRanges.value.y : imageViewSliceRanges.value.y
} else if (['Z', 'S'].includes(imageViewAxis.value)) {
return imageViewIntersectCropMode.value ? imageViewCroppedSliceRanges.value.z : imageViewSliceRanges.value.z
}
})
function changeImageViewSlice(value) {
if (!imageViewAxis.value) {
return undefined
} else if (['X', 'L'].includes(imageViewAxis.value)) {
imageViewSlices.value.x = value
} else if (['Y', 'P'].includes(imageViewAxis.value)) {
imageViewSlices.value.y = value
} else if (['Z', 'S'].includes(imageViewAxis.value)) {
imageViewSlices.value.z = value
}
}
watch(imageIntersectAllowed, (value) => {
if (!value) {
imageViewIntersectMode.value = false
imageViewIntersectCropMode.value = false
}
})
return {
particleSize,
layersShown,
layers,
axisSystem,
axisSystemOptions,
imageViewMode,
imageViewIntersectMode,
imageViewIntersectCropMode,
imageIntersectAllowed,
imageViewAxis,
imageViewSlice,
imageViewSliceRange,
imageViewLevel,
imageViewLevelRange,
imageViewWindow,
imageViewWindowRange,
changeImageViewSlice,
changeAxisSystem,
resetView,
selectedDataObjects,
Expand All @@ -180,79 +252,134 @@ export default {
</script>

<template>
<div class="render-control-bar">
<v-select
v-model="layersShown"
v-if="!showAnalysisOptions"
:items="layers"
:item-disabled="(layer) => !layer.available()"
item-value="name"
label="Layers shown"
style="width: 500px"
multiple
small-chips
item-text="name"
>
<template #selection="{ item }">
<v-chip
close
@click:close="layersShown = layersShown.filter((l) => l !== item.name)"
:color="item.color"
:text-color="item.color === 'white' ? 'black' : 'white'"
>
{{ item.name }}
</v-chip>
</template>
</v-select>
<v-text-field
v-model.number="particleSize"
v-if="!showAnalysisOptions && layersShown.includes('Particles')"
label="Particle Size"
type="number"
style="width: 80px"
step="0.5"
min="0.5"
max="10"
hide-details
/>
<v-select
v-bind="axisSystem"
:items="axisSystemOptions"
@change="changeAxisSystem"
label="Axis System"
style="width: 150px"
/>
<v-switch
v-if="showAnalysisOptions"
v-model="showDifferenceFromMeanMode"
label="Show difference from mean"
/>
<v-btn
class="my-5"
v-if="!showAnalysisOptions && selectedDataObjects.length === 1"
@click="captureThumbnail"
>
Set {{ thumbnailTarget.type }} thumbnail
</v-btn>
<v-btn
class="my-5"
@click="resetView"
>
Reset view
</v-btn>
<div>
<div class="render-control-row">
<v-select
v-model="layersShown"
v-if="!showAnalysisOptions"
:items="layers"
:item-disabled="(layer) => !layer.available()"
item-value="name"
label="Layers shown"
style="width: 500px"
multiple
small-chips
item-text="name"
>
<template #selection="{ item }">
<v-chip
close
@click:close="layersShown = layersShown.filter((l) => l !== item.name)"
:color="item.color"
:text-color="item.color === 'white' ? 'black' : 'white'"
>
{{ item.name }}
</v-chip>
</template>
</v-select>
<v-text-field
v-model.number="particleSize"
v-if="!showAnalysisOptions && layersShown.includes('Particles')"
label="Particle Size"
type="number"
style="width: 80px"
step="0.5"
min="0.5"
max="10"
hide-details
/>
<v-select
:value="axisSystem"
:items="axisSystemOptions"
@change="changeAxisSystem"
label="Axis System"
style="width: 150px"
/>
<v-switch
v-if="showAnalysisOptions"
v-model="showDifferenceFromMeanMode"
label="Show difference from mean"
/>
<v-btn
class="my-5"
v-if="!showAnalysisOptions && selectedDataObjects.length === 1"
@click="captureThumbnail"
>
Set {{ thumbnailTarget.type }} thumbnail
</v-btn>
<v-btn
class="my-5"
@click="resetView"
>
Reset view
</v-btn>
</div>
<div class="render-control-row" v-if="imageViewMode && axisSystem">
<div v-if="imageIntersectAllowed">
<v-checkbox
v-model="imageViewIntersectMode"
label="Intersect"
:dense="true"
:hide-details="true"
class="mt-0 pt-0"
/>
<v-checkbox
v-model="imageViewIntersectCropMode"
label="Crop"
:dense="true"
:hide-details="true"
/>
</div>
<v-select
v-model="imageViewAxis"
:items="axisSystem.axes"
label="Axis"
style="width: 25%"
:hide-details="true"
/>
<v-slider
v-if="imageViewSliceRange"
:value="imageViewSlice"
:min="imageViewSliceRange[0]"
:max="imageViewSliceRange[1]"
:thumb-label="true"
label="Slice"
style="width: 25%"
@input="changeImageViewSlice"
:hide-details="true"
/>
<v-slider
v-model="imageViewWindow"
:min="imageViewWindowRange[0]"
:max="imageViewWindowRange[1]"
:thumb-label="true"
label="Window"
style="width: 25%"
:hide-details="true"
/>
<v-slider
v-model="imageViewLevel"
:min="imageViewLevelRange[0]"
:max="imageViewLevelRange[1]"
:thumb-label="true"
label="Level"
style="width: 25%"
:hide-details="true"
/>
</div>
</div>
</template>

<style scoped>
.render-control-bar {
.render-control-row {
display: flex;
width: 100%;
height: 70px;
justify-content: space-between;
align-items: baseline;
align-items: center;
column-gap: 20px;
}
.render-control-bar > * {
.render-control-row > * {
flex-grow: 0;
}
</style>
Loading

0 comments on commit 4313760

Please sign in to comment.