Skip to content

Commit

Permalink
Colors for mappings (#2965)
Browse files Browse the repository at this point in the history
* make it possible to specify custom colors for mappings (#2173)

* fix shader code when mappings are not supported, add test (#2173)

* remove volume tracing assertion from getVolumeTracingLayerName api function

* fix flow, linter

* improve usability of colors for mappings, first color specifies the color of the first equivalence class now, add option to hide unmapped ids (#2173)

* fix mapping info view if custom colors are used, but missing -> make segment transparent

* apply PR feedback, default color for mapping with colors, add changelog
  • Loading branch information
daniel-wer authored Jul 30, 2018
1 parent 3b71b12 commit d0554da
Show file tree
Hide file tree
Showing 15 changed files with 221 additions and 56 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.md).

### Added

- Added two new properties to mapping json files. The `colors: [<hsvHueValue1>, <hsvHueValue2>, ...]` property can be used to specify up to 256 custom colors for the first 256 equivalence classes of the mapping. The `hideUnmappedIds: <true|false>` property indicates whether segments that were not mapped should be rendered transparently or not. [#2965](https://github.com/scalableminds/webknossos/pull/2965)
- Added a button for refreshing the dataset in the backend cache. [#2975](https://github.com/scalableminds/webknossos/pull/2975)
- All dates in webknossos will be shown in the browser's timezone. On hover, a tooltip will show the date in UTC. [#2916](https://github.com/scalableminds/webknossos/pull/2916) ![image](https://user-images.githubusercontent.com/2486553/42888385-74c82bc0-8aa8-11e8-9c3e-7cfc90ce93bc.png)
- When merging datasets within a tracing via the merge-modal, the user can choose whether the merge should be executed directly in the currently opened tracing. Alternatively, a new annotation can be created which is accessible via the dashboard (as it has been before).
Expand Down
24 changes: 19 additions & 5 deletions app/assets/javascripts/admin/api_flow_types.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import type { Vector3, Vector6, Point3 } from "oxalis/constants";
import type {
SettingsType,
BoundingBoxObjectType,
CategoryType,
ElementClassType,
EdgeType,
CommentType,
TreeGroupType,
Expand All @@ -17,20 +15,36 @@ import Enum from "Enumjs";

export type APIMessageType = { ["info" | "warning" | "error"]: string };

type ElementClassType = "uint8" | "uint16" | "uint32";

export type APIMappingType = {
+parent?: string,
+name: string,
+classes?: Array<Array<number>>,
+colors?: Array<number>,
+hideUnmappedIds?: boolean,
};

export type APIDataLayerType = {
type APIDataLayerBaseType = {|
+name: string,
+category: CategoryType,
+boundingBox: BoundingBoxObjectType,
+resolutions: Array<Vector3>,
+elementClass: ElementClassType,
+mappings?: Array<string>,
};
|};

type APIColorLayerType = {|
...APIDataLayerBaseType,
category: "color",
|};

type APISegmentationLayerType = {|
...APIDataLayerBaseType,
category: "segmentation",
largestSegmentId: number,
|};

export type APIDataLayerType = APIColorLayerType | APISegmentationLayerType;

export type APIDataSourceType = {
+id: {
Expand Down
11 changes: 7 additions & 4 deletions app/assets/javascripts/oxalis/api/api_latest.js
Original file line number Diff line number Diff line change
Expand Up @@ -542,10 +542,9 @@ class DataApi {

/**
* Returns the name of the volume tracing layer.
* _Volume tracing only!_
*/
getVolumeTracingLayerName(): string {
assertVolume(Store.getState().tracing);
// TODO: Rename method to getSegmentationLayerName() and increase api version
const segmentationLayer = this.model.getSegmentationLayer();
assertExists(segmentationLayer, "Segmentation layer not found!");
return segmentationLayer.name;
Expand All @@ -562,7 +561,11 @@ class DataApi {
*
* api.setMapping("segmentation", mapping);
*/
setMapping(layerName: string, mapping: MappingType) {
setMapping(
layerName: string,
mapping: MappingType,
options?: { colors?: Array<number>, hideUnmappedIds?: boolean } = {},
) {
if (!Model.isMappingSupported) {
throw new Error(messages["mapping.too_few_textures"]);
}
Expand All @@ -573,7 +576,7 @@ class DataApi {
if (layerName !== segmentationLayerName) {
throw new Error(messages["mapping.unsupported_layer"]);
}
Store.dispatch(setMappingAction(_.clone(mapping)));
Store.dispatch(setMappingAction(_.clone(mapping), options.colors, options.hideUnmappedIds));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ class PlaneMaterialFactory extends AbstractPlaneMaterialFactory {
type: "f",
value: 0,
},
hideUnmappedIds: {
type: "b",
value: false,
},
globalMousePosition: {
type: "v3",
value: new THREE.Vector3(0, 0, 0),
Expand Down Expand Up @@ -177,6 +181,7 @@ class PlaneMaterialFactory extends AbstractPlaneMaterialFactory {
const [
mappingTexture,
mappingLookupTexture,
mappingColorTexture,
] = segmentationLayer.mappings.getMappingTextures();
this.uniforms[sanitizeName(`${segmentationLayer.name}_mapping_texture`)] = {
type: "t",
Expand All @@ -186,6 +191,10 @@ class PlaneMaterialFactory extends AbstractPlaneMaterialFactory {
type: "t",
value: mappingLookupTexture,
};
this.uniforms[sanitizeName(`${segmentationLayer.name}_mapping_color_texture`)] = {
type: "t",
value: mappingColorTexture,
};
}

// Add weight/color uniforms
Expand Down Expand Up @@ -270,6 +279,16 @@ class PlaneMaterialFactory extends AbstractPlaneMaterialFactory {
),
);

this.storePropertyUnsubscribers.push(
listenToStoreProperty(
storeState => storeState.temporaryConfiguration.activeMapping.hideUnmappedIds,
hideUnmappedIds => {
this.uniforms.hideUnmappedIds.value = hideUnmappedIds;
},
true,
),
);

this.storePropertyUnsubscribers.push(
listenToStoreProperty(
storeState => storeState.temporaryConfiguration.viewMode,
Expand Down
15 changes: 13 additions & 2 deletions app/assets/javascripts/oxalis/model/actions/settings_actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ type SetViewModeActionType = { type: "SET_VIEW_MODE", viewMode: ModeType };
type SetFlightmodeRecordingActionType = { type: "SET_FLIGHTMODE_RECORDING", value: boolean };
type SetControlModeActionType = { type: "SET_CONTROL_MODE", controlMode: ControlModeType };
type SetMappingEnabledActionType = { type: "SET_MAPPING_ENABLED", isMappingEnabled: boolean };
type SetMappingActionType = { type: "SET_MAPPING", mapping: ?MappingType };
type SetMappingActionType = {
type: "SET_MAPPING",
mapping: ?MappingType,
mappingColors: ?Array<number>,
hideUnmappedIds: ?boolean,
};
export type SettingActionType =
| UpdateUserSettingActionType
| UpdateDatasetSettingActionType
Expand Down Expand Up @@ -145,7 +150,13 @@ export const setMappingEnabledAction = (
isMappingEnabled,
});

export const setMappingAction = (mapping: ?MappingType): SetMappingActionType => ({
export const setMappingAction = (
mapping: ?MappingType,
mappingColors: ?Array<number>,
hideUnmappedIds: ?boolean,
): SetMappingActionType => ({
type: "SET_MAPPING",
mapping,
mappingColors,
hideUnmappedIds,
});
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,21 @@ class DataCube {
: null;
}

shouldHideUnmappedIds(): boolean {
return this.isSegmentation
? Store.getState().temporaryConfiguration.activeMapping.hideUnmappedIds
: false;
}

mapId(idToMap: number): number {
let mappedId = null;
const mapping = this.getMapping();
if (mapping != null && this.isMappingEnabled()) {
mappedId = mapping[idToMap];
}
if (this.shouldHideUnmappedIds() && mappedId == null) {
mappedId = 0;
}
return mappedId != null ? mappedId : idToMap;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ function calculateNecessaryTextureCount<Layer>(
}

function calculateMappingTextureCount(): number {
// If there is a segmentation layer, we need one lookup and one data texture for mappings
const textureCountForCellMappings = 2;
// If there is a segmentation layer, we need one lookup, one data and one color texture for mappings
const textureCountForCellMappings = 3;
return textureCountForCellMappings;
}

Expand All @@ -128,7 +128,6 @@ function deriveSupportedFeatures<Layer>(
}

// Count textures needed for mappings separately, because they are not strictly necessary

const notEnoughTexturesForMapping =
necessaryTextureCount + calculateMappingTextureCount() > specs.maxTextureCount;
if (hasSegmentation && notEnoughTexturesForMapping) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ import UpdatableTexture from "libs/UpdatableTexture";
import { getRenderer } from "oxalis/controller/renderer";
import { listenToStoreProperty } from "oxalis/model/helpers/listener_helpers";
import messages from "messages";
import { getMappings } from "oxalis/model/accessors/dataset_accessor";
import { getMappings, getLayerByName } from "oxalis/model/accessors/dataset_accessor";
import type { MappingType } from "oxalis/store";
import type { APIMappingType } from "admin/api_flow_types";
import type DataLayer from "oxalis/model/data_layer";

export const MAPPING_TEXTURE_WIDTH = 4096;
export const MAPPING_COLOR_TEXTURE_WIDTH = 16;

type APIMappingsType = { [string]: APIMappingType };

Expand All @@ -41,20 +42,21 @@ export function setupGlobalMappingsObject(segmentationLayer: DataLayer) {

class Mappings {
baseUrl: string;
availableMappings: Array<string>;
layerName: string;
mappingTexture: UpdatableTexture;
mappingLookupTexture: UpdatableTexture;
mappingColorTexture: UpdatableTexture;

constructor(layerName: string) {
const { dataset } = Store.getState();
const datasetName = dataset.name;
const dataStoreUrl = dataset.dataStore.url;
this.layerName = layerName;
this.baseUrl = `${dataStoreUrl}/data/datasets/${datasetName}/layers/${layerName}/mappings/`;
this.availableMappings = getMappings(dataset, layerName);
}

getMappingNames(): Array<string> {
return this.availableMappings;
return getMappings(Store.getState().dataset, this.layerName);
}

async activateMapping(mappingName: ?string) {
Expand All @@ -63,8 +65,12 @@ class Mappings {
} else {
const fetchedMappings = {};
await this.fetchMappings(mappingName, fetchedMappings);
const mappingObject = this.buildMappingObject(mappingName, fetchedMappings);
Store.dispatch(setMappingAction(mappingObject));
const { hideUnmappedIds, colors: mappingColors } = fetchedMappings[mappingName];
// If custom colors are specified for a mapping, assign the mapped ids specifically, so that the first equivalence
// class will get the first color, and so on
const assignNewIds = mappingColors != null && mappingColors.length > 0;
const mappingObject = this.buildMappingObject(mappingName, fetchedMappings, assignNewIds);
Store.dispatch(setMappingAction(mappingObject, mappingColors, hideUnmappedIds));
}
}

Expand Down Expand Up @@ -92,19 +98,36 @@ class Mappings {
});
}

buildMappingObject(mappingName: string, fetchedMappings: APIMappingsType): MappingType {
getLargestSegmentId(): number {
const segmentationLayer = getLayerByName(Store.getState().dataset, this.layerName);
if (segmentationLayer.category !== "segmentation") {
throw new Error("Mappings class must be instantiated with a segmentation layer.");
}
return segmentationLayer.largestSegmentId;
}

buildMappingObject(
mappingName: string,
fetchedMappings: APIMappingsType,
assignNewIds: boolean,
): MappingType {
const mappingObject: MappingType = {};

const maxId = this.getLargestSegmentId() + 1;
// Initialize to the next multiple of 256 that is larger than maxId
let newMappedId = Math.ceil(maxId / 256) * 256;
for (const currentMappingName of this.getMappingChain(mappingName, fetchedMappings)) {
const mapping = fetchedMappings[currentMappingName];
ErrorHandling.assertExists(mapping.classes, "Mappings must have been fetched at this point");

if (mapping.classes) {
for (const mappingClass of mapping.classes) {
const minId = _.min(mappingClass);
const minId = assignNewIds ? newMappedId : _.min(mappingClass);
const mappedId = mappingObject[minId] || minId;
for (const id of mappingClass) {
mappingObject[id] = mappedId;
}
newMappedId++;
}
}
}
Expand All @@ -125,23 +148,52 @@ class Mappings {
// MAPPING TEXTURES

setupMappingTextures() {
const renderer = getRenderer();
this.mappingTexture = createUpdatableTexture(
MAPPING_TEXTURE_WIDTH,
4,
THREE.UnsignedByteType,
getRenderer(),
renderer,
);
this.mappingLookupTexture = createUpdatableTexture(
MAPPING_TEXTURE_WIDTH,
4,
THREE.UnsignedByteType,
getRenderer(),
renderer,
);
// Up to 256 (16*16) custom colors can be specified for mappings
this.mappingColorTexture = createUpdatableTexture(
MAPPING_COLOR_TEXTURE_WIDTH,
1,
THREE.FloatType,
renderer,
);

listenToStoreProperty(
state => state.temporaryConfiguration.activeMapping.mapping,
mapping => this.updateMappingTextures(mapping),
);

listenToStoreProperty(
state => state.temporaryConfiguration.activeMapping.mappingColors,
mappingColors => this.updateMappingColorTexture(mappingColors),
);
}

updateMappingColorTexture(mappingColors: ?Array<number>) {
mappingColors = mappingColors || [];
const maxNumberOfColors = MAPPING_COLOR_TEXTURE_WIDTH ** 2;
const float32Colors = new Float32Array(maxNumberOfColors);
// Initialize the array with -1
float32Colors.fill(-1);
float32Colors.set(mappingColors.slice(0, maxNumberOfColors));
this.mappingColorTexture.update(
float32Colors,
0,
0,
MAPPING_COLOR_TEXTURE_WIDTH,
MAPPING_COLOR_TEXTURE_WIDTH,
);
}

updateMappingTextures(mapping: ?MappingType): void {
Expand Down Expand Up @@ -195,7 +247,7 @@ class Mappings {
if (this.mappingTexture == null) {
this.setupMappingTextures();
}
return [this.mappingTexture, this.mappingLookupTexture];
return [this.mappingTexture, this.mappingLookupTexture, this.mappingColorTexture];
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ function SettingsReducer(state: OxalisState, action: ActionType): OxalisState {
activeMapping: {
mappingSize: { $set: action.mapping != null ? _.size(action.mapping) : 0 },
mapping: { $set: action.mapping },
mappingColors: { $set: action.mappingColors },
hideUnmappedIds: { $set: action.hideUnmappedIds },
},
},
});
Expand Down
Loading

0 comments on commit d0554da

Please sign in to comment.