Skip to content

Commit 43c4ff9

Browse files
beasteerswchargin
authored andcommitted
projector: add images to nearest neighbors list (#2543)
Summary: In addition to showing the labels of nearest neighbors, the projector also shows sprites associated with those points. Sometimes the label data alone is only minimally useful (e.g., when all the labels are the same), and the sprites provide much greater insight. The additional detail due to the larger view is also particularly helpful on datasets with higher-resolution sprites, like audio spectrograms. The behavior is enabled by default, and can be disabled via a new “show images” checkbox. Addresses part of #2492. Screenshots: - <https://user-images.githubusercontent.com/4317806/63119171-570ae280-bf54-11e9-8e83-80e19701a60f.png> - <https://user-images.githubusercontent.com/6741720/62971619-4a657d80-bde0-11e9-84d2-f7b9f56b8bf6.png> Test Plan: Launch the projector on a dataset without images, and observe that the behavior is the same as before: no images, no new checkbox, no error messages. Then, switch to a dataset with images, and select a point. Verify that the “show images” toggle works, that the images are properly rendered, and that the images update when you select a new data point.
1 parent f6a64aa commit 43c4ff9

File tree

2 files changed

+130
-3
lines changed

2 files changed

+130
-3
lines changed

tensorboard/plugins/projector/vz_projector/vz-projector-inspector-panel.html

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,16 @@
124124
border-left: 1px solid rgba(0, 0, 0, 0.15);
125125
}
126126

127+
.nn-list .sprite-image,
128+
.metadata-list .sprite-image {
129+
width: 100%;
130+
}
131+
132+
.nn-list.nn-img-show .sprite-image,
133+
.metadata-list.nn-img-show .sprite-image {
134+
display: block;
135+
}
136+
127137
.nn-list .neighbor-link:hover,
128138
.metadata-list .metadata-link:hover {
129139
cursor: pointer;
@@ -146,6 +156,11 @@
146156
float: right;
147157
}
148158

159+
.neighbor-image-controls {
160+
display: flex;
161+
padding: 0.8em 0.1em;
162+
}
163+
149164
.options a {
150165
color: #727272;
151166
font-size: 13px;
@@ -158,7 +173,7 @@
158173
}
159174

160175
.neighbors {
161-
margin-bottom: 30px;
176+
margin-bottom: 15px;
162177
}
163178

164179
.neighbors-options {
@@ -206,6 +221,12 @@
206221
display: flex;
207222
flex-direction: column;
208223
}
224+
225+
.results,
226+
.nn,
227+
.nn-list {
228+
flex: 1 0 100px;
229+
}
209230
</style>
210231
<div class="container">
211232
<div class="buttons">
@@ -267,6 +288,24 @@
267288
<a class="euclidean" href="javascript:void(0);">EUCLIDEAN</a>
268289
</div>
269290
</div>
291+
<div class="neighbor-image-controls">
292+
<template is="dom-if" if="[[spriteImagesAvailable]]">
293+
<paper-checkbox checked="{{showNeighborImages}}">
294+
show images
295+
<paper-icon-button
296+
icon="help"
297+
class="help-icon"
298+
></paper-icon-button>
299+
<paper-tooltip
300+
position="bottom"
301+
animation-delay="0"
302+
fit-to-visible-bounds
303+
>
304+
Show the images of the nearest neighbors.
305+
</paper-tooltip>
306+
</paper-checkbox>
307+
</template>
308+
</div>
270309
</div>
271310
<p>Nearest points in the original space:</p>
272311
<div class="nn-list"></div>

tensorboard/plugins/projector/vz_projector/vz-projector-inspector-panel.ts

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ limitations under the License.
1515
namespace vz_projector {
1616
/** Limit the number of search results we show to the user. */
1717
const LIMIT_RESULTS = 100;
18+
const DEFAULT_NEIGHBORS = 100;
1819

1920
// tslint:disable-next-line
2021
export let InspectorPanelPolymer = PolymerElement({
@@ -23,11 +24,29 @@ namespace vz_projector {
2324
selectedMetadataField: String,
2425
metadataFields: Array,
2526
metadataColumn: String,
26-
numNN: {type: Number, value: 100},
27+
numNN: {type: Number, value: DEFAULT_NEIGHBORS},
2728
updateNumNN: Object,
29+
spriteMeta: Object, // type: `SpriteMetadata`
30+
showNeighborImages: {
31+
type: Boolean,
32+
value: true,
33+
observer: '_refreshNeighborsList',
34+
},
35+
spriteImagesAvailable: {
36+
type: Boolean,
37+
value: true,
38+
observer: '_refreshNeighborsList',
39+
},
2840
},
2941
});
3042

43+
type SpriteMetadata = {
44+
imagePath?: string;
45+
singleImageDim?: number[];
46+
aspectRatio?: number;
47+
nCols?: number;
48+
};
49+
3150
export class InspectorPanel extends InspectorPanelPolymer {
3251
distFunc: DistanceFunction;
3352
numNN: number;
@@ -37,10 +56,13 @@ namespace vz_projector {
3756
private selectedMetadataField: string;
3857
private metadataFields: string[];
3958
private metadataColumn: string;
59+
private spriteMeta: SpriteMetadata;
4060
private displayContexts: string[];
4161
private projector: Projector;
4262
private selectedPointIndices: number[];
4363
private neighborsOfFirstPoint: knn.NearestEntry[];
64+
private showNeighborImages: boolean;
65+
private spriteImagesAvailable: boolean;
4466
private searchBox: ProjectorInput;
4567

4668
private resetFilterButton: HTMLButtonElement;
@@ -106,6 +128,26 @@ namespace vz_projector {
106128
return stats.name;
107129
});
108130

131+
if (
132+
spriteAndMetadata.spriteMetadata &&
133+
spriteAndMetadata.spriteMetadata.imagePath
134+
) {
135+
const [
136+
spriteWidth,
137+
spriteHeight,
138+
] = spriteAndMetadata.spriteMetadata.singleImageDim;
139+
140+
this.spriteMeta = {
141+
imagePath: spriteAndMetadata.spriteImage.src,
142+
aspectRatio: spriteWidth / spriteHeight,
143+
nCols: Math.floor(spriteAndMetadata.spriteImage.width / spriteWidth),
144+
singleImageDim: [spriteWidth, spriteHeight],
145+
};
146+
} else {
147+
this.spriteMeta = {};
148+
}
149+
this.spriteImagesAvailable = !!this.spriteMeta.imagePath;
150+
109151
if (
110152
this.selectedMetadataField == null ||
111153
this.metadataFields.filter(
@@ -127,6 +169,10 @@ namespace vz_projector {
127169
this.enableResetFilterButton(false);
128170
}
129171

172+
_refreshNeighborsList() {
173+
this.updateNeighborsList();
174+
}
175+
130176
metadataEditorContext(enabled: boolean, metadataColumn: string) {
131177
if (!this.projector || !this.projector.dataSet) {
132178
return;
@@ -269,7 +315,40 @@ namespace vz_projector {
269315
return point.metadata[this.selectedMetadataField].toString();
270316
}
271317

272-
private updateNeighborsList(neighbors: knn.NearestEntry[]) {
318+
private spriteImageRenderer() {
319+
const spriteImagePath = this.spriteMeta.imagePath;
320+
const {aspectRatio, nCols} = this.spriteMeta;
321+
const paddingBottom = 100 / aspectRatio + '%';
322+
const backgroundSize = `${nCols * 100}% ${nCols * 100}%`;
323+
const backgroundImage = `url(${CSS.escape(spriteImagePath)})`;
324+
325+
return (neighbor: knn.NearestEntry): HTMLElement => {
326+
const spriteElementImage = document.createElement('div');
327+
spriteElementImage.className = 'sprite-image';
328+
spriteElementImage.style.backgroundImage = backgroundImage;
329+
spriteElementImage.style.paddingBottom = paddingBottom;
330+
spriteElementImage.style.backgroundSize = backgroundSize;
331+
const [row, col] = [
332+
Math.floor(neighbor.index / nCols),
333+
neighbor.index % nCols,
334+
];
335+
const [top, left] = [
336+
(row / (nCols - 1)) * 100,
337+
(col / (nCols - 1)) * 100,
338+
];
339+
spriteElementImage.style.backgroundPosition = `${left}% ${top}%`;
340+
341+
return spriteElementImage;
342+
};
343+
}
344+
345+
private updateNeighborsList(neighbors?: knn.NearestEntry[]) {
346+
neighbors = neighbors || this._currentNeighbors;
347+
this._currentNeighbors = neighbors;
348+
if (neighbors == null) {
349+
return;
350+
}
351+
273352
const nnlist = this.$$('.nn-list') as HTMLDivElement;
274353
nnlist.innerHTML = '';
275354

@@ -282,6 +361,10 @@ namespace vz_projector {
282361
this.searchBox.message = '';
283362
const minDist = neighbors.length > 0 ? neighbors[0].dist : 0;
284363

364+
if (this.spriteImagesAvailable && this.showNeighborImages) {
365+
var imageRenderer = this.spriteImageRenderer();
366+
}
367+
285368
for (let i = 0; i < neighbors.length; i++) {
286369
const neighbor = neighbors[i];
287370

@@ -332,6 +415,11 @@ namespace vz_projector {
332415
barElement.appendChild(tickElement);
333416
}
334417

418+
if (this.spriteImagesAvailable && this.showNeighborImages) {
419+
const neighborElementImage = imageRenderer(neighbor);
420+
neighborElement.appendChild(neighborElementImage);
421+
}
422+
335423
neighborElementLink.appendChild(labelValueElement);
336424
neighborElementLink.appendChild(barElement);
337425
neighborElement.appendChild(neighborElementLink);

0 commit comments

Comments
 (0)