Skip to content

Commit 4e51269

Browse files
Merge branch 'master' into fix-add-remote
2 parents 13eb34b + 0a4ff64 commit 4e51269

File tree

3 files changed

+127
-46
lines changed

3 files changed

+127
-46
lines changed

CHANGELOG.unreleased.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
2020
- Terms of Service for Webknossos are now accepted at registration, not afterward. [#8193](https://github.com/scalableminds/webknossos/pull/8193)
2121
- Removed bounding box size restriction for inferral jobs for super users. [#8200](https://github.com/scalableminds/webknossos/pull/8200)
2222
- Improved logging for errors when loading datasets and problems arise during a conversion step. [#8202](https://github.com/scalableminds/webknossos/pull/8202)
23+
- Allowed to train an AI model using differently sized bounding boxes. We recommend all bounding boxes to have equal dimensions or to have dimensions which are multiples of the smallest bounding box. [#8222](https://github.com/scalableminds/webknossos/pull/8222)
2324

2425
### Fixed
2526
- Fix performance bottleneck when deleting a lot of trees at once. [#8176](https://github.com/scalableminds/webknossos/pull/8176)
27+
- Fix that listing datasets with the `api/datasets` route without compression failed due to missing permissions regarding public datasets. [#8249](https://github.com/scalableminds/webknossos/pull/8249)
2628
- Fix a bug where changing the color of a segment via the menu in the segments tab would update the segment color of the previous segment, on which the context menu was opened. [#8225](https://github.com/scalableminds/webknossos/pull/8225)
2729
- Fix a bug where in the add remote dataset view the dataset name setting was not in sync with the datasource setting of the advanced tab making the form not submittable. [#8245](https://github.com/scalableminds/webknossos/pull/8245)
2830
- Fix a bug when importing an NML with groups when only groups but no trees exist in an annotation. [#8176](https://github.com/scalableminds/webknossos/pull/8176)

app/controllers/DatasetController.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ class DatasetController @Inject()(userService: UserService,
223223
groupedByOrga = datasets.groupBy(_._organization).toList
224224
js <- Fox.serialCombined(groupedByOrga) { byOrgaTuple: (String, List[Dataset]) =>
225225
for {
226-
organization <- organizationDAO.findOne(byOrgaTuple._1)
226+
organization <- organizationDAO.findOne(byOrgaTuple._1)(GlobalAccessContext)
227227
groupedByDataStore = byOrgaTuple._2.groupBy(_._dataStore).toList
228228
result <- Fox.serialCombined(groupedByDataStore) { byDataStoreTuple: (String, List[Dataset]) =>
229229
for {

frontend/javascripts/oxalis/view/jobs/train_ai_model.tsx

Lines changed: 124 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ import _ from "lodash";
3434
import BoundingBox from "oxalis/model/bucket_data_handling/bounding_box";
3535
import { formatVoxels } from "libs/format_utils";
3636
import * as Utils from "libs/utils";
37-
import { V3 } from "libs/mjs";
3837
import type { APIAnnotation, APIDataset, ServerVolumeTracing } from "types/api_flow_types";
39-
import type { Vector3 } from "oxalis/constants";
38+
import type { Vector3, Vector6 } from "oxalis/constants";
4039
import { serverVolumeToClientVolumeTracing } from "oxalis/model/reducers/volumetracing_reducer";
4140
import { convertUserBoundingBoxesFromServerToFrontend } from "oxalis/model/reducers/reducer_helpers";
41+
import { computeArrayFromBoundingBox } from "libs/utils";
4242

4343
const { TextArea } = Input;
4444
const FormItem = Form.Item;
@@ -66,8 +66,8 @@ enum AiModelCategory {
6666
const ExperimentalWarning = () => (
6767
<Row style={{ display: "grid", marginBottom: 16 }}>
6868
<Alert
69-
message="Please note that this feature is experimental. All bounding boxes must be the same size, with equal width and height. Ensure the size is not too small (we recommend at least 10 Vx per dimension) and choose boxes that represent the data well."
70-
type="warning"
69+
message="Please note that this feature is experimental. All bounding boxes should have equal dimensions or have dimensions which are multiples of the smallest bounding box. Ensure the size is not too small (we recommend at least 10 Vx per dimension) and choose boxes that represent the data well."
70+
type="info"
7171
showIcon
7272
/>
7373
</Row>
@@ -217,19 +217,29 @@ export function TrainAiModelTab<GenericAnnotation extends APIAnnotation | Hybrid
217217
modelCategory: AiModelCategory.EM_NEURONS,
218218
};
219219

220-
const userBoundingBoxes = annotationInfos.flatMap(({ userBoundingBoxes }) => userBoundingBoxes);
220+
const userBoundingBoxes = annotationInfos.flatMap(({ userBoundingBoxes, annotation }) =>
221+
userBoundingBoxes.map((box) => ({
222+
...box,
223+
annotationId: "id" in annotation ? annotation.id : annotation.annotationId,
224+
})),
225+
);
221226

222227
const bboxesVoxelCount = _.sum(
223228
(userBoundingBoxes || []).map((bbox) => new BoundingBox(bbox.boundingBox).getVolume()),
224229
);
225230

226-
const { areSomeAnnotationsInvalid, invalidAnnotationsReason } =
227-
areInvalidAnnotationsIncluded(annotationInfos);
228-
const { areSomeBBoxesInvalid, invalidBBoxesReason } =
229-
areInvalidBoundingBoxesIncluded(userBoundingBoxes);
230-
const invalidReasons = [invalidAnnotationsReason, invalidBBoxesReason]
231-
.filter((reason) => reason)
232-
.join("\n");
231+
const { hasAnnotationErrors, errors: annotationErrors } =
232+
checkAnnotationsForErrorsAndWarnings(annotationInfos);
233+
const {
234+
hasBBoxErrors,
235+
hasBBoxWarnings,
236+
errors: bboxErrors,
237+
warnings: bboxWarnings,
238+
} = checkBoundingBoxesForErrorsAndWarnings(userBoundingBoxes);
239+
const hasErrors = hasAnnotationErrors || hasBBoxErrors;
240+
const hasWarnings = hasBBoxWarnings;
241+
const errors = [...annotationErrors, ...bboxErrors];
242+
const warnings = bboxWarnings;
233243

234244
return (
235245
<Form
@@ -333,16 +343,46 @@ export function TrainAiModelTab<GenericAnnotation extends APIAnnotation | Hybrid
333343
</div>
334344
</FormItem>
335345
) : null}
346+
347+
{hasErrors
348+
? errors.map((error) => (
349+
<Alert
350+
key={error}
351+
description={error}
352+
style={{
353+
marginBottom: 12,
354+
whiteSpace: "pre-line",
355+
}}
356+
type="error"
357+
showIcon
358+
/>
359+
))
360+
: null}
361+
{hasWarnings
362+
? warnings.map((warning) => (
363+
<Alert
364+
key={warning}
365+
description={warning}
366+
style={{
367+
marginBottom: 12,
368+
whiteSpace: "pre-line",
369+
}}
370+
type="warning"
371+
showIcon
372+
/>
373+
))
374+
: null}
375+
336376
<FormItem>
337-
<Tooltip title={invalidReasons}>
377+
<Tooltip title={hasErrors ? "Solve the errors displayed above before continuing." : ""}>
338378
<Button
339379
size="large"
340380
type="primary"
341381
htmlType="submit"
342382
style={{
343383
width: "100%",
344384
}}
345-
disabled={areSomeBBoxesInvalid || areSomeAnnotationsInvalid}
385+
disabled={hasErrors}
346386
>
347387
Start Training
348388
</Button>
@@ -385,16 +425,16 @@ export function CollapsibleWorkflowYamlEditor({
385425
);
386426
}
387427

388-
function areInvalidAnnotationsIncluded<T extends HybridTracing | APIAnnotation>(
428+
function checkAnnotationsForErrorsAndWarnings<T extends HybridTracing | APIAnnotation>(
389429
annotationsWithDatasets: Array<AnnotationInfoForAIJob<T>>,
390430
): {
391-
areSomeAnnotationsInvalid: boolean;
392-
invalidAnnotationsReason: string | null;
431+
hasAnnotationErrors: boolean;
432+
errors: string[];
393433
} {
394434
if (annotationsWithDatasets.length === 0) {
395435
return {
396-
areSomeAnnotationsInvalid: true,
397-
invalidAnnotationsReason: "At least one annotation must be defined.",
436+
hasAnnotationErrors: true,
437+
errors: ["At least one annotation must be defined."],
398438
};
399439
}
400440
const annotationsWithoutBoundingBoxes = annotationsWithDatasets.filter(
@@ -407,42 +447,81 @@ function areInvalidAnnotationsIncluded<T extends HybridTracing | APIAnnotation>(
407447
"id" in annotation ? annotation.id : annotation.annotationId,
408448
);
409449
return {
410-
areSomeAnnotationsInvalid: true,
411-
invalidAnnotationsReason: `All annotations must have at least one bounding box. Annotations without bounding boxes are: ${annotationIds.join(", ")}`,
450+
hasAnnotationErrors: true,
451+
errors: [
452+
`All annotations must have at least one bounding box. Annotations without bounding boxes are:\n${annotationIds.join(", ")}`,
453+
],
412454
};
413455
}
414-
return { areSomeAnnotationsInvalid: false, invalidAnnotationsReason: null };
456+
return { hasAnnotationErrors: false, errors: [] };
415457
}
416458

417-
function areInvalidBoundingBoxesIncluded(userBoundingBoxes: UserBoundingBox[]): {
418-
areSomeBBoxesInvalid: boolean;
419-
invalidBBoxesReason: string | null;
459+
function checkBoundingBoxesForErrorsAndWarnings(
460+
userBoundingBoxes: (UserBoundingBox & { annotationId: string })[],
461+
): {
462+
hasBBoxErrors: boolean;
463+
hasBBoxWarnings: boolean;
464+
errors: string[];
465+
warnings: string[];
420466
} {
467+
let hasBBoxErrors = false;
468+
let hasBBoxWarnings = false;
469+
const errors = [];
470+
const warnings = [];
421471
if (userBoundingBoxes.length === 0) {
422-
return {
423-
areSomeBBoxesInvalid: true,
424-
invalidBBoxesReason: "At least one bounding box must be defined.",
425-
};
472+
hasBBoxErrors = true;
473+
errors.push("At least one bounding box must be defined.");
426474
}
427-
const getSize = (bbox: UserBoundingBox) => V3.sub(bbox.boundingBox.max, bbox.boundingBox.min);
475+
// Find smallest bounding box dimensions
476+
const minDimensions = userBoundingBoxes.reduce(
477+
(min, { boundingBox: box }) => ({
478+
x: Math.min(min.x, box.max[0] - box.min[0]),
479+
y: Math.min(min.y, box.max[1] - box.min[1]),
480+
z: Math.min(min.z, box.max[2] - box.min[2]),
481+
}),
482+
{ x: Number.POSITIVE_INFINITY, y: Number.POSITIVE_INFINITY, z: Number.POSITIVE_INFINITY },
483+
);
428484

429-
const size = getSize(userBoundingBoxes[0]);
430-
// width must equal height
431-
if (size[0] !== size[1]) {
432-
return {
433-
areSomeBBoxesInvalid: true,
434-
invalidBBoxesReason: "The bounding box width must equal its height.",
435-
};
485+
// Validate minimum size and multiple requirements
486+
type BoundingBoxWithAnnotationId = { boundingBox: Vector6; name: string; annotationId: string };
487+
const tooSmallBoxes: BoundingBoxWithAnnotationId[] = [];
488+
const nonMultipleBoxes: BoundingBoxWithAnnotationId[] = [];
489+
userBoundingBoxes.forEach(({ boundingBox: box, name, annotationId }) => {
490+
const arrayBox = computeArrayFromBoundingBox(box);
491+
const [_x, _y, _z, width, height, depth] = arrayBox;
492+
if (width < 10 || height < 10 || depth < 10) {
493+
tooSmallBoxes.push({ boundingBox: arrayBox, name, annotationId });
494+
}
495+
496+
if (
497+
width % minDimensions.x !== 0 ||
498+
height % minDimensions.y !== 0 ||
499+
depth % minDimensions.z !== 0
500+
) {
501+
nonMultipleBoxes.push({ boundingBox: arrayBox, name, annotationId });
502+
}
503+
});
504+
505+
const boxWithIdToString = ({ boundingBox, name, annotationId }: BoundingBoxWithAnnotationId) =>
506+
`'${name}' of annotation ${annotationId}: ${boundingBox.join(", ")}`;
507+
508+
if (tooSmallBoxes.length > 0) {
509+
hasBBoxWarnings = true;
510+
const tooSmallBoxesStrings = tooSmallBoxes.map(boxWithIdToString);
511+
warnings.push(
512+
`The following bounding boxes are not at least 10 Vx in each dimension which is suboptimal for the training:\n${tooSmallBoxesStrings.join("\n")}`,
513+
);
436514
}
437-
// all bounding boxes must have the same size
438-
const areSizesIdentical = userBoundingBoxes.every((bbox) => V3.isEqual(getSize(bbox), size));
439-
if (areSizesIdentical) {
440-
return { areSomeBBoxesInvalid: false, invalidBBoxesReason: null };
515+
516+
if (nonMultipleBoxes.length > 0) {
517+
hasBBoxWarnings = true;
518+
const nonMultipleBoxesStrings = nonMultipleBoxes.map(boxWithIdToString);
519+
warnings.push(
520+
`The minimum bounding box dimensions are ${minDimensions.x} x ${minDimensions.y} x ${minDimensions.z}. The following bounding boxes have dimensions which are not a multiple of the minimum dimensions which is suboptimal for the training:\n${nonMultipleBoxesStrings.join("\n")}`,
521+
);
441522
}
442-
return {
443-
areSomeBBoxesInvalid: true,
444-
invalidBBoxesReason: "All bounding boxes must have the same size.",
445-
};
523+
524+
return { hasBBoxErrors, hasBBoxWarnings, errors, warnings };
446525
}
447526

448527
function AnnotationsCsvInput({

0 commit comments

Comments
 (0)