Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create, select, and delete image view presets #1237

Merged
merged 15 commits into from
Jul 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions girder/girder_large_image/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from .loadmodelcache import invalidateLoadModelCache
from .models.image_item import ImageItem
from .rest import addSystemEndpoints
from .rest.item_meta import InternalMetadataItemResource
from .rest.large_image_resource import LargeImageResource
from .rest.tiles import TilesItemResource

Expand Down Expand Up @@ -646,6 +647,7 @@ def load(self, info):

girder_tilesource.loadGirderTileSources()
TilesItemResource(info['apiRoot'])
InternalMetadataItemResource(info['apiRoot'])
info['apiRoot'].large_image = LargeImageResource()

Item().exposeFields(level=AccessType.READ, fields='largeImage')
Expand Down
100 changes: 100 additions & 0 deletions girder/girder_large_image/rest/item_meta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#############################################################################
# Copyright Kitware Inc.
#
# Licensed under the Apache License, Version 2.0 ( the "License" );
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#############################################################################

from girder.api import access
from girder.api.describe import Description, describeRoute
from girder.api.rest import loadmodel
from girder.api.v1.item import Item
from girder.constants import AccessType, TokenScope


class InternalMetadataItemResource(Item):
def __init__(self, apiRoot):
super().__init__()
apiRoot.item.route(
'GET', (':itemId', 'internal_metadata', ':key'), self.getMetadataKey
)
apiRoot.item.route(
'PUT', (':itemId', 'internal_metadata', ':key'), self.updateMetadataKey
)
apiRoot.item.route(
'DELETE', (':itemId', 'internal_metadata', ':key'), self.deleteMetadataKey
)

@describeRoute(
Description('Get the value for a single internal metadata key on this item.')
.param('itemId', 'The ID of the item.', paramType='path')
.param(
'key',
'The metadata key to retrieve.',
paramType='path',
default='meta',
)
.errorResponse('ID was invalid.')
.errorResponse('Read access was denied for the item.', 403)
)
@access.user(scope=TokenScope.DATA_READ)
@loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ)
def getMetadataKey(self, item, key, params):
if key not in item:
return None
return item[key]

@describeRoute(
Description(
'Overwrite the value for a single internal metadata key on this item.'
)
.param('itemId', 'The ID of the item.', paramType='path')
.param(
'key',
'The metadata key which should have a new value. \
The default key, "meta" is equivalent to the external metadata. \
Editing the "meta" key is equivalent to using PUT /item/{id}/metadata.',
paramType='path',
default='meta',
)
.param(
'value',
'The new value that should be written for the chosen metadata key',
paramType='body',
)
.errorResponse('ID was invalid.')
.errorResponse('Write access was denied for the item.', 403)
)
@access.user(scope=TokenScope.DATA_WRITE)
@loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.WRITE)
def updateMetadataKey(self, item, key, params):
item[key] = self.getBodyJson()
self._model.save(item)

@describeRoute(
Description('Delete a single internal metadata key on this item.')
.param('itemId', 'The ID of the item.', paramType='path')
.param(
'key',
'The metadata key to delete.',
paramType='path',
default='meta',
)
.errorResponse('ID was invalid.')
.errorResponse('Write access was denied for the item.', 403)
)
@access.user(scope=TokenScope.DATA_WRITE)
@loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ)
def deleteMetadataKey(self, item, key, params):
if key in item:
del item[key]
self._model.save(item)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CHANNEL_COLORS, OTHER_COLORS } from '../utils/colors'
import HistogramEditor from './HistogramEditor.vue';

export default {
props: ['itemId', 'currentFrame', 'layers', 'layerMap', 'active'],
props: ['itemId', 'currentFrame', 'currentStyle', 'layers', 'layerMap', 'active'],
emits: ['updateStyle'],
components: {
'color-picker': Chrome,
Expand Down Expand Up @@ -100,6 +100,40 @@ export default {
})
this.fetchCurrentFrameHistogram()
},
initializeStateFromStyle() {
this.enabledLayers = []
const styleArray = this.currentStyle.bands
this.layers.forEach((layerName) => {
const layerInfo = this.compositeLayerInfo[layerName]
const currentLayerStyle = styleArray.find((s) => s.framedelta === layerInfo.framedelta && s.band === layerInfo.band)
if (currentLayerStyle) {
this.enabledLayers.push(layerName)
if (
currentLayerStyle.min && currentLayerStyle.max
&& currentLayerStyle.min.includes("min:")
&& currentLayerStyle.max.includes("max:")
) {
currentLayerStyle.autoRange = parseFloat(
currentLayerStyle.min.replace("min:", '')
) * 100
currentLayerStyle.min = undefined
currentLayerStyle.max = undefined
}
}
this.compositeLayerInfo[layerName] = Object.assign(
{}, layerInfo, currentLayerStyle
)
})
this.layers.forEach((layer) => {
this.compositeLayerInfo[layer].enabled = this.enabledLayers.includes(layer);
})
const autoRanges = Object.entries(this.compositeLayerInfo)
.map(([index, info]) => info.autoRange)
.filter((a) => a !== undefined)
if (autoRanges.every((v) => v === autoRanges[0])) {
this.autoRangeForAll = autoRanges[0]
}
},
fetchCurrentFrameHistogram() {
restRequest({
type: 'GET',
Expand All @@ -110,7 +144,7 @@ export default {
});
},
toggleEnableAll() {
if (this.enabledLayers !== this.layers) {
if (!this.layers.every((l) => this.enabledLayers.includes(l))) {
this.enabledLayers = this.layers
} else {
this.enabledLayers = []
Expand Down Expand Up @@ -217,23 +251,27 @@ export default {
},
mounted() {
this.initializeLayerInfo()
if (this.layerMap) {
// channels all enabled by default
this.enabledLayers = this.layers
if (this.currentStyle) {
this.initializeStateFromStyle()
} else {
// only some bands enabled by default
['red', 'green', 'blue', 'gray', 'grey'].forEach((bandColor) => {
if (this.layers.includes(bandColor)) {
this.enabledLayers.push(bandColor)
if (this.layerMap) {
// channels all enabled by default
this.enabledLayers = this.layers
} else {
// only some bands enabled by default
['red', 'green', 'blue', 'gray', 'grey'].forEach((bandColor) => {
if (this.layers.includes(bandColor)) {
this.enabledLayers.push(bandColor)
}
})
// if no known band colors exist, enable the first three
if (this.enabledLayers.length === 0) {
this.enabledLayers = this.layers.slice(0, 3)
}
})
// if no known band colors exist, enable the first three
if (this.enabledLayers.length === 0) {
this.enabledLayers = this.layers.slice(0, 3)
}
this.updateActiveLayers()
this.updateStyle()
}
this.updateActiveLayers()
this.updateStyle()
if (this.active) {
document.addEventListener('keydown', this.keyHandler)
}
Expand All @@ -245,6 +283,9 @@ export default {
} else {
document.removeEventListener('keydown', this.keyHandler)
}
},
currentStyle() {
this.initializeStateFromStyle()
}
}
}
Expand Down Expand Up @@ -285,7 +326,7 @@ export default {
<input
type="checkbox"
class="input-80"
:checked="enabledLayers === layers"
:checked="layers.every((l) => enabledLayers.includes(l))"
@input="toggleEnableAll"
>
</th>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export default {
}
},
watch: {
currentValue(v) {
this.value = v
},
value(v) {
this.$emit('updateValue', v);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<script>
import Vue from 'vue';
import CompositeLayers from './CompositeLayers.vue';
import DualInput from './DualInput.vue'
import DualInput from './DualInput.vue';
import PresetsMenu from './PresetsMenu.vue';

export default Vue.extend({
props: ['itemId', 'imageMetadata', 'frameUpdate'],
components: { CompositeLayers, DualInput },
components: { CompositeLayers, DualInput, PresetsMenu },
data() {
return {
loaded: false,
Expand Down Expand Up @@ -39,6 +41,19 @@ export default Vue.extend({
}
},
methods: {
setCurrentMode(mode) {
this.currentModeId = mode.id
},
setCurrentFrame(frame) {
this.currentFrame = frame
this.indexInfo = Object.fromEntries(
Object.entries(this.indexInfo)
.map(([index, info]) => {
info.current = Math.floor(frame / info.stride) % (info.range + 1)
return [index, info]
})
)
},
updateStyle(idx, style) {
this.$set(this.style, idx, style);
this.update()
Expand All @@ -48,6 +63,7 @@ export default Vue.extend({
this.update();
},
updateFrameSlider(frame) {
this.currentFrame = frame
this.frameUpdate(frame, undefined);
},
update() {
Expand Down Expand Up @@ -182,6 +198,15 @@ export default Vue.extend({
{{ mode.name }}
</option>
</select>
<presets-menu
:itemId="itemId"
:currentMode="sliderModes.find((m) => m.id === currentModeId)"
:currentFrame="currentFrame"
:currentStyle="style[currentModeId]"
@setCurrentMode="setCurrentMode"
@setCurrentFrame="setCurrentFrame"
@updateStyle="updateStyle"
/>
</div>
<dual-input
v-if="currentModeId === 0"
Expand Down Expand Up @@ -210,6 +235,7 @@ export default Vue.extend({
v-if="imageMetadata.channels && modesShown[2]"
:itemId="itemId"
:currentFrame="currentFrame"
:currentStyle="style[2]"
:layers="imageMetadata.channels"
:layerMap="imageMetadata.channelmap"
:active="currentModeId === 2"
Expand All @@ -221,6 +247,7 @@ export default Vue.extend({
v-if="imageMetadata.bands && modesShown[3]"
:itemId="itemId"
:currentFrame="currentFrame"
:currentStyle="style[3]"
:layers="imageMetadata.bands"
:layerMap="undefined"
:active="currentModeId === 3"
Expand Down
Loading