From 4a80a23b664469e81de824db60c21c880093b2ab Mon Sep 17 00:00:00 2001 From: Brian Helba Date: Wed, 9 May 2018 16:44:37 -0400 Subject: [PATCH 1/3] Add markups to image histograms --- server/models/image.py | 133 ++++++++++++++++-- .../Facets/imagesFacetMarkupLevel.pug | 0 2 files changed, 122 insertions(+), 11 deletions(-) create mode 100644 web_external/ImagesGallery/Facets/imagesFacetMarkupLevel.pug diff --git a/server/models/image.py b/server/models/image.py index d9e7c5a8..bb5add4b 100644 --- a/server/models/image.py +++ b/server/models/image.py @@ -361,6 +361,9 @@ def load(self, id, level=AccessType.ADMIN, user=None, objectId=True, force=False raise def getHistograms(self, filterQuery, user): + # Avoid circular import + from .study import Study + # Define facets categorialFacets = [ 'meta.datasetId', @@ -406,15 +409,7 @@ def getHistograms(self, filterQuery, user): 'folderId': {'$in': [ dataset['folderId'] for dataset in Dataset().list(user=user)]} } - if filterQuery: - query = { - '$and': [ - folderQuery, - filterQuery - ] - } - else: - query = folderQuery + facetStages = { '__passedFilters__': [ {'$count': 'count'}], @@ -442,9 +437,125 @@ def getHistograms(self, filterQuery, user): 'default': None }} ] + facetStages['markups'] = [ + { + '$unwind': '$markups' + }, + { + '$group': { + '_id': '$markups', + 'count': {'$sum': 1} + } + }, + { + '$project': { + '_id': False, + 'label': '$_id', + 'count': True + } + } + ] + histogram = next(self.collection.aggregate([ - {'$match': query}, - {'$facet': facetStages} + { + '$match': ( + { + '$and': [ + folderQuery, + filterQuery + ] + } + if filterQuery else + folderQuery + ) + }, + { + '$lookup': { + 'from': 'annotation', + 'localField': '_id', + 'foreignField': 'imageId', + 'as': 'annotations' + } + }, + { + '$project': { + '_id': True, + 'name': True, + 'meta': True, + 'markups': { + # (6) Concatinate all annotations' markup IDs together, eliminating + # duplicates + '$reduce': { + 'input': { + # (2) Extract only the markup from each annotation + '$map': { + 'input': { + # (1) Remove incomplete, flagged, or inaccessable + # annotations + '$filter': { + 'input': '$annotations', + 'cond': { + '$and': [ + '$$this.stopTime', + { + '$eq': [ + '$$this.status', + 'ok' + ] + }, + { + '$in': [ + '$$this.studyId', + [ + study['_id'] + for study in Study().list(user=user) + ] + ] + } + ] + } + } + }, + 'as': 'annotation', + 'in': '$$annotation.markups' + } + }, + 'initialValue': [], + 'in': { + # (6a) Eliminate duplicates when extending array + '$setUnion': [ + '$$value', + { + # (5) Make an array of just markup IDs + '$map': { + 'input': { + # (4) Exclude false / empty markup + # TODO: if an annotation has no markups, mark it as a special value + '$filter': { + 'input': { + # (3) Convert the markup object to an array + # of markupItem + # '$$this' uses the (6) context, which is + # the output of (2) + '$objectToArray': '$$this' + }, + 'as': 'markupItem', + 'cond': '$$markupItem.v.present' + } + }, + 'as': 'markupItem', + 'in': '$$markupItem.k' + } + } + ] + } + } + } + } + }, + { + '$facet': facetStages + } ])) # Fix up the pipeline result diff --git a/web_external/ImagesGallery/Facets/imagesFacetMarkupLevel.pug b/web_external/ImagesGallery/Facets/imagesFacetMarkupLevel.pug new file mode 100644 index 00000000..e69de29b From 6d2dcc8d4c9f948b87a9e9d43d51f4cc8a9cdf88 Mon Sep 17 00:00:00 2001 From: Brian Helba Date: Wed, 9 May 2018 18:56:18 -0400 Subject: [PATCH 2/3] Add basic facet GUI for markups --- web_external/ImagesGallery/Facets/ImagesFacetView.js | 7 +++++++ web_external/ImagesGallery/Facets/ImagesFacetsPane.js | 2 ++ web_external/ImagesGallery/Facets/imagesFacetsPane.pug | 2 ++ 3 files changed, 11 insertions(+) diff --git a/web_external/ImagesGallery/Facets/ImagesFacetView.js b/web_external/ImagesGallery/Facets/ImagesFacetView.js index 5e563a0a..178271ee 100644 --- a/web_external/ImagesGallery/Facets/ImagesFacetView.js +++ b/web_external/ImagesGallery/Facets/ImagesFacetView.js @@ -722,6 +722,13 @@ const FACET_SCHEMA = { coerceToType: 'string', title: 'Tags', collapsed: true + }, + 'markups': { + FacetView: ImagesFacetCategoricalTagsView, + FacetFilter: TagsCategoricalFacetFilter, + coerceToType: 'string', + title: 'Feature Markups', + collapsed: true } }; diff --git a/web_external/ImagesGallery/Facets/ImagesFacetsPane.js b/web_external/ImagesGallery/Facets/ImagesFacetsPane.js index 1fbdb92e..14e73f19 100644 --- a/web_external/ImagesGallery/Facets/ImagesFacetsPane.js +++ b/web_external/ImagesGallery/Facets/ImagesFacetsPane.js @@ -47,6 +47,8 @@ const ImagesFacetsPane = View.extend({ headerEl = this.$('.isic-images-facets-clinical'); } else if (facetId.startsWith('meta.acquisition')) { headerEl = this.$('.isic-images-facets-acquisition'); + } else if (facetId.startsWith('markups')) { + headerEl = this.$('.isic-images-facets-markup'); } else { headerEl = this.$('.isic-images-facets-database'); } diff --git a/web_external/ImagesGallery/Facets/imagesFacetsPane.pug b/web_external/ImagesGallery/Facets/imagesFacetsPane.pug index 2816bf63..c4390aae 100644 --- a/web_external/ImagesGallery/Facets/imagesFacetsPane.pug +++ b/web_external/ImagesGallery/Facets/imagesFacetsPane.pug @@ -39,3 +39,5 @@ style. h3 Technological Attributes .isic-images-facets-database.isic-images-facets-section h3 Database Attributes +.isic-images-facets-markup.isic-images-facets-section + h3 Clinical Impressions From 90e8f34be4bb8b83148a10368c3062088b4e4944 Mon Sep 17 00:00:00 2001 From: Brian Helba Date: Thu, 10 May 2018 03:03:20 -0400 Subject: [PATCH 3/3] WIP: Improve markup facet --- .../ImagesGallery/Facets/ImagesFacetView.js | 166 +++++++++++++++++- .../Facets/imagesFacetMarkupLevel.pug | 6 + .../Facets/imagesFacetMarkups.pug | 4 + .../Facets/imagesFacetsPane.styl | 5 + 4 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 web_external/ImagesGallery/Facets/imagesFacetMarkups.pug diff --git a/web_external/ImagesGallery/Facets/ImagesFacetView.js b/web_external/ImagesGallery/Facets/ImagesFacetView.js index 178271ee..1bd5e0a4 100644 --- a/web_external/ImagesGallery/Facets/ImagesFacetView.js +++ b/web_external/ImagesGallery/Facets/ImagesFacetView.js @@ -8,15 +8,19 @@ import DatasetCollection from '../../collections/DatasetCollection'; import View from '../../view'; import HistogramScale from './HistogramScale'; +import masterFeatures from '../../masterFeatures.json'; /* eslint-disable import/order */ import ImagesFacetHistogramTemplate from './imagesFacetHistogram.pug'; import ImagesFacetCategoricalTemplate from './imagesFacetCategorical.pug'; +import ImagesFacetMarkupsTemplate from './imagesFacetMarkups.pug'; import checkImageUrl from '!url-loader!svg-fill-loader!./check.svg?fill=#999999'; // eslint-disable-line import/no-webpack-loader-syntax import dashImageUrl from '!url-loader!svg-fill-loader!./dash.svg?fill=#999999'; // eslint-disable-line import/no-webpack-loader-syntax import exImageUrl from '!url-loader!svg-fill-loader!./ex.svg?fill=#999999'; // eslint-disable-line import/no-webpack-loader-syntax /* eslint-enable import/order */ +import ImagesFacetMarkupLevelTemplate from './imagesFacetMarkupLevel.pug'; + const ImagesFacetView = View.extend({ className: 'isic-images-facet', @@ -151,6 +155,8 @@ const ImagesFacetHistogramView = ImagesFacetView.extend({ })); this._renderHistogram(); this._applyInitialCollapseState(); + + return this; }, _renderHistogram: function () { @@ -459,6 +465,8 @@ const ImagesFacetCategoricalView = ImagesFacetView.extend({ this._applyInitialCollapseState(); this._rerenderCounts(); this._rerenderSelections(); + + return this; }, _rerenderCounts: function () { @@ -552,6 +560,8 @@ const ImagesFacetCategoricalDatasetView = ImagesFacetCategoricalView.extend({ 'show': 100 } }); + + return this; }, _getBinTitle: function (completeBin) { @@ -570,6 +580,158 @@ const ImagesFacetCategoricalTagsView = ImagesFacetCategoricalView.extend({ } }); +const ImagesFacetMarkupGroupView = View.extend({ + /** + * @param {ImagesFacetModel} settings.completeFacet + * @param {ImagesFacetModel} settings.filteredFacet + * @param {String[]} settings.featureIds + * @param {Number} settings.depth + * @param {String} settings.featureLevelLabel + */ + initialize: function (settings) { + this.completeFacet = settings.completeFacet; + this.filteredFacet = settings.filteredFacet; + + this.featureIds = settings.featureIds; + this.depth = settings.depth; + this.featureLevelLabel = settings.featureLevelLabel; + + this.childViews = _.chain(this.featureIds) + .groupBy((featureId) => { + const featureLevelLabels = featureId.split(' : '); + const childFeatureLevelLabel = featureLevelLabels[this.depth + 1]; + return childFeatureLevelLabel; + }) + .map((childFeatureIds, childFeatureLevelLabel) => { + const childDepth = this.depth + 1; + + if (childFeatureIds.length === 1 + && childFeatureIds[0].split(' : ').length - 1 === childDepth) { + // If this is the single and last child, then it's a leaf + return new ImagesFacetMarkupLeafView({ + completeFacet: this.completeFacet, + filteredFacet: this.filteredFacet, + featureId: childFeatureIds[0], + depth: childDepth, + featureLevelLabel: childFeatureLevelLabel, + parentView: this + }); + } else { + return new ImagesFacetMarkupGroupView({ + completeFacet: this.completeFacet, + filteredFacet: this.filteredFacet, + featureIds: childFeatureIds, + depth: childDepth, + featureLevelLabel: childFeatureLevelLabel, + parentView: this + }); + } + }) + .value(); + }, + + render: function () { + this.$el.html(ImagesFacetMarkupLevelTemplate({ + featureLevelLabel: this.featureLevelLabel, + count: 0 + })); + + _.each(this.childViews, (childView) => { + childView + .render() + // To avoid selecting deep children, use '.children' + .$el.appendTo(this.$el.children('.isic-images-facet-childMarkups')); + }); + + this.completeCount = this.childViews.reduce( + (sum, childView) => sum + childView.completeCount, + 0 + ); + this.$el + .children('.isic-images-facet-bin') + .children('.isic-images-facet-bin-count') + .text(this.completeCount); + + return this; + } +}); + +const ImagesFacetMarkupLeafView = View.extend({ + /** + * @param {ImagesFacetModel} settings.completeFacet + * @param {ImagesFacetModel} settings.filteredFacet + * @param {String} settings.featureId + * @param {Number} settings.depth + * @param {String} settings.featureLevelLabel + */ + initialize: function (settings) { + this.completeFacet = settings.completeFacet; + this.filteredFacet = settings.filteredFacet; + + this.featureId = settings.featureId; + this.depth = settings.depth; + this.featureLevelLabel = settings.featureLevelLabel; + }, + + render: function () { + const getFeatureBinCount = (facet) => { + const featureBin = _.findWhere(facet.get('bins'), {label: this.featureId}); + const count = featureBin ? featureBin.count : 0; + return count; + }; + this.completeCount = getFeatureBinCount(this.completeFacet); + this.filteredCount = getFeatureBinCount(this.filteredFacet); + + this.$el.html(ImagesFacetMarkupLevelTemplate({ + featureLevelLabel: this.featureLevelLabel, + count: this.completeCount + })); + + return this; + } +}); + +const ImagesFacetMarkupsView = ImagesFacetView.extend({ + initialize: function (settings) { + ImagesFacetView.prototype.initialize.call(this, settings); + + const featureIds = _.pluck(masterFeatures, 'id'); + this.childViews = _.chain(featureIds) + .groupBy((featureId) => { + const featureLevelLabels = featureId.split(' : '); + const childFeatureLevelLabel = featureLevelLabels[0]; + return childFeatureLevelLabel; + }) + .map((childFeatureIds, childFeatureLevelLabel) => { + return new ImagesFacetMarkupGroupView({ + completeFacet: this.completeFacet, + filteredFacet: this.filteredFacet, + featureIds: childFeatureIds, + depth: 0, + featureLevelLabel: childFeatureLevelLabel, + parentView: this + }); + }) + .value(); + }, + + + render: function () { + this.$el.html(ImagesFacetMarkupsTemplate({ + title: this.title, + facetContentId: this.facetContentId, + })); + + _.each(this.childViews, (childView) => { + childView + .render() + .$el.appendTo(this.$('.isic-images-facet-markups-content')); + }); + + return this; + } +}); + const FACET_SCHEMA = { 'meta.datasetId': { FacetView: ImagesFacetCategoricalDatasetView, @@ -724,8 +886,8 @@ const FACET_SCHEMA = { collapsed: true }, 'markups': { - FacetView: ImagesFacetCategoricalTagsView, - FacetFilter: TagsCategoricalFacetFilter, + FacetView: ImagesFacetMarkupsView, + FacetFilter: CategoricalFacetFilter, coerceToType: 'string', title: 'Feature Markups', collapsed: true diff --git a/web_external/ImagesGallery/Facets/imagesFacetMarkupLevel.pug b/web_external/ImagesGallery/Facets/imagesFacetMarkupLevel.pug index e69de29b..05af68af 100644 --- a/web_external/ImagesGallery/Facets/imagesFacetMarkupLevel.pug +++ b/web_external/ImagesGallery/Facets/imagesFacetMarkupLevel.pug @@ -0,0 +1,6 @@ +a.isic-images-facet-bin + //i.icon-check + span.isic-images-facet-bin-name= featureLevelLabel + span.isic-images-facet-bin-count= count + +.isic-images-facet-childMarkups diff --git a/web_external/ImagesGallery/Facets/imagesFacetMarkups.pug b/web_external/ImagesGallery/Facets/imagesFacetMarkups.pug new file mode 100644 index 00000000..ede9d3cd --- /dev/null +++ b/web_external/ImagesGallery/Facets/imagesFacetMarkups.pug @@ -0,0 +1,4 @@ +extends imagesFacet + +block content + .isic-images-facet-markups-content diff --git a/web_external/ImagesGallery/Facets/imagesFacetsPane.styl b/web_external/ImagesGallery/Facets/imagesFacetsPane.styl index 359377d5..c6d5204c 100644 --- a/web_external/ImagesGallery/Facets/imagesFacetsPane.styl +++ b/web_external/ImagesGallery/Facets/imagesFacetsPane.styl @@ -50,6 +50,7 @@ stroke none .isic-images-facet-categorical-content + .isic-images-facet-markups-content margin-left 25px user-select none @@ -66,3 +67,7 @@ .isic-images-facet-bin-count margin-left 7px color #737373 + + .isic-images-facet-markups-content + .isic-images-facet-childMarkups + margin-left 20px