Skip to content

Commit 7b90571

Browse files
authored
Show task bounding boxes in compound annotations (#8735)
### URL of deployed dev instance (used for testing): - https://compoundincludetaskbboxes.webknossos.xyz ### Steps to test: - Create task with bbox - Trace it + finish - View compound. Bbox should be in user bounding boxes ### TODOs: - [x] No more unique-i-fying. Leave in all instances’ boxes - [x] Add both task id and annotation id to box name ### Issues: - fixes #7835 ------ - [x] Added changelog entry (create a `$PR_NUMBER.md` file in `unreleased_changes` or use `./tools/create-changelog-entry.py`) - [x] Considered [common edge cases](../blob/master/.github/common_edge_cases.md)
1 parent c88c3b1 commit 7b90571

File tree

14 files changed

+99
-36
lines changed

14 files changed

+99
-36
lines changed

app/models/annotation/AnnotationMerger.scala

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import models.annotation.AnnotationType.AnnotationType
1010
import models.dataset.DatasetDAO
1111
import models.user.User
1212
import com.scalableminds.util.objectid.ObjectId
13+
import com.scalableminds.webknossos.tracingstore.tracings.NamedBoundingBox
1314

1415
import scala.concurrent.ExecutionContext
1516

@@ -30,7 +31,8 @@ class AnnotationMerger @Inject()(datasetDAO: DatasetDAO, tracingStoreService: Tr
3031
annotationB._dataset,
3132
annotationB._team,
3233
AnnotationType.Explorational,
33-
List(annotationA, annotationB)
34+
List(annotationA, annotationB),
35+
Seq.empty
3436
)
3537

3638
def mergeN(
@@ -40,7 +42,8 @@ class AnnotationMerger @Inject()(datasetDAO: DatasetDAO, tracingStoreService: Tr
4042
datasetId: ObjectId,
4143
teamId: ObjectId,
4244
typ: AnnotationType,
43-
annotations: List[Annotation]
45+
annotations: List[Annotation],
46+
additionalBoundingBoxes: Seq[NamedBoundingBox]
4447
)(implicit ctx: DBAccessContext): Fox[Annotation] =
4548
if (annotations.isEmpty)
4649
Fox.empty
@@ -51,7 +54,8 @@ class AnnotationMerger @Inject()(datasetDAO: DatasetDAO, tracingStoreService: Tr
5154
datasetId,
5255
newId,
5356
userId,
54-
toTemporaryStore) ?~> "Failed to merge annotations in tracingstore."
57+
toTemporaryStore,
58+
additionalBoundingBoxes) ?~> "Failed to merge annotations in tracingstore."
5559
} yield {
5660
Annotation(
5761
newId,
@@ -70,15 +74,17 @@ class AnnotationMerger @Inject()(datasetDAO: DatasetDAO, tracingStoreService: Tr
7074
datasetId: ObjectId,
7175
newAnnotationId: ObjectId,
7276
requestingUserId: ObjectId,
73-
toTemporaryStore: Boolean)(implicit ctx: DBAccessContext): Fox[List[AnnotationLayer]] =
77+
toTemporaryStore: Boolean,
78+
additionalBoundingBoxes: Seq[NamedBoundingBox])(implicit ctx: DBAccessContext): Fox[List[AnnotationLayer]] =
7479
for {
7580
dataset <- datasetDAO.findOne(datasetId)
7681
tracingStoreClient: WKRemoteTracingStoreClient <- tracingStoreService.clientFor(dataset)
7782
mergedAnnotationProto <- tracingStoreClient.mergeAnnotationsByIds(annotations.map(_._id),
7883
annotations.map(_._user),
7984
newAnnotationId,
8085
toTemporaryStore,
81-
requestingUserId)
86+
requestingUserId,
87+
additionalBoundingBoxes)
8288
layers = mergedAnnotationProto.annotationLayers.map(AnnotationLayer.fromProto)
8389
} yield layers.toList
8490

app/models/annotation/WKRemoteTracingStoreClient.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import com.scalableminds.webknossos.datastore.models.annotation.{
2222
import com.scalableminds.webknossos.datastore.models.datasource.DataSourceLike
2323
import com.scalableminds.webknossos.datastore.rpc.RPC
2424
import com.scalableminds.webknossos.tracingstore.controllers.MergedFromIdsRequest
25-
import com.scalableminds.webknossos.tracingstore.tracings.TracingSelector
25+
import com.scalableminds.webknossos.tracingstore.tracings.{NamedBoundingBox, TracingSelector}
2626
import com.scalableminds.webknossos.tracingstore.tracings.volume.MagRestrictions
2727
import com.scalableminds.webknossos.tracingstore.tracings.volume.VolumeDataZipFormat.VolumeDataZipFormat
2828
import com.typesafe.scalalogging.LazyLogging
@@ -176,15 +176,16 @@ class WKRemoteTracingStoreClient(
176176
ownerIds: List[ObjectId],
177177
newAnnotationId: ObjectId,
178178
toTemporaryStore: Boolean,
179-
requestingUserId: ObjectId): Fox[AnnotationProto] = {
179+
requestingUserId: ObjectId,
180+
additionalBoundingBoxes: Seq[NamedBoundingBox]): Fox[AnnotationProto] = {
180181
logger.debug(s"Called to merge ${annotationIds.length} annotations by ids." + baseInfo)
181182
rpc(s"${tracingStore.url}/tracings/annotation/mergedFromIds").withLongTimeout
182183
.addQueryString("token" -> RpcTokenHolder.webknossosToken)
183184
.addQueryString("toTemporaryStore" -> toTemporaryStore.toString)
184185
.addQueryString("newAnnotationId" -> newAnnotationId.toString)
185186
.addQueryString("requestingUserId" -> requestingUserId.toString)
186-
.postJsonWithProtoResponse[MergedFromIdsRequest, AnnotationProto](MergedFromIdsRequest(annotationIds, ownerIds))(
187-
AnnotationProto)
187+
.postJsonWithProtoResponse[MergedFromIdsRequest, AnnotationProto](
188+
MergedFromIdsRequest(annotationIds, ownerIds, additionalBoundingBoxes))(AnnotationProto)
188189
}
189190

190191
def mergeSkeletonTracingsByContents(newTracingId: String, tracings: SkeletonTracings): Fox[Unit] = {

app/models/annotation/handler/ProjectInformationHandler.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@ package models.annotation.handler
22

33
import com.scalableminds.util.accesscontext.DBAccessContext
44
import com.scalableminds.util.tools.{Fox, FoxImplicits}
5+
56
import javax.inject.Inject
67
import models.annotation._
78
import models.project.ProjectDAO
89
import models.user.{User, UserService}
9-
1010
import com.scalableminds.util.objectid.ObjectId
11+
import models.task.TaskDAO
1112

1213
import scala.concurrent.ExecutionContext
1314

1415
class ProjectInformationHandler @Inject()(annotationDAO: AnnotationDAO,
1516
projectDAO: ProjectDAO,
17+
taskDAO: TaskDAO,
1618
userService: UserService,
1719
annotationMerger: AnnotationMerger)(implicit val ec: ExecutionContext)
1820
extends AnnotationInformationHandler
@@ -28,13 +30,15 @@ class ProjectInformationHandler @Inject()(annotationDAO: AnnotationDAO,
2830
_ <- assertAllOnSameDataset(annotations)
2931
_ <- assertNonEmpty(annotations) ?~> "project.noAnnotations"
3032
datasetId <- annotations.headOption.map(_._dataset).toFox
33+
taskBoundingBoxes <- taskDAO.findTaskBoundingBoxesByAnnotationIds(annotations.map(_._id))
3134
mergedAnnotation <- annotationMerger.mergeN(projectId,
3235
toTemporaryStore = true,
3336
user._id,
3437
datasetId,
3538
project._team,
3639
AnnotationType.CompoundProject,
37-
annotations) ?~> "annotation.merge.failed.compound"
40+
annotations,
41+
taskBoundingBoxes) ?~> "annotation.merge.failed.compound"
3842
} yield mergedAnnotation
3943

4044
override def restrictionsFor(projectId: ObjectId)(implicit ctx: DBAccessContext): Fox[AnnotationRestrictions] =

app/models/annotation/handler/TaskInformationHandler.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@ class TaskInformationHandler @Inject()(taskDAO: TaskDAO,
3131
user <- userOpt.toFox ?~> "user.notAuthorised"
3232
project <- projectDAO.findOne(task._project)
3333
datasetId <- finishedAnnotations.headOption.map(_._dataset).toFox
34+
taskBoundingBoxes <- taskDAO.findTaskBoundingBoxesByAnnotationIds(annotations.map(_._id))
3435
mergedAnnotation <- annotationMerger.mergeN(task._id,
3536
toTemporaryStore = true,
3637
user._id,
3738
datasetId,
3839
project._team,
3940
AnnotationType.CompoundTask,
40-
finishedAnnotations) ?~> "annotation.merge.failed.compound"
41+
finishedAnnotations,
42+
taskBoundingBoxes) ?~> "annotation.merge.failed.compound"
4143
} yield mergedAnnotation
4244

4345
def restrictionsFor(taskId: ObjectId)(implicit ctx: DBAccessContext): Fox[AnnotationRestrictions] =

app/models/annotation/handler/TaskTypeInformationHandler.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@ class TaskTypeInformationHandler @Inject()(taskTypeDAO: TaskTypeDAO,
3232
_ <- assertNonEmpty(finishedAnnotations) ?~> "taskType.noAnnotations"
3333
user <- userOpt.toFox ?~> "user.notAuthorised"
3434
datasetId <- finishedAnnotations.headOption.map(_._dataset).toFox
35+
taskBoundingBoxes <- taskDAO.findTaskBoundingBoxesByAnnotationIds(annotations.map(_._id))
3536
mergedAnnotation <- annotationMerger.mergeN(taskTypeId,
3637
toTemporaryStore = true,
3738
user._id,
3839
datasetId,
3940
taskType._team,
4041
AnnotationType.CompoundTaskType,
41-
finishedAnnotations) ?~> "annotation.merge.failed.compound"
42+
finishedAnnotations,
43+
taskBoundingBoxes) ?~> "annotation.merge.failed.compound"
4244
} yield mergedAnnotation
4345

4446
override def restrictionsFor(taskTypeId: ObjectId)(implicit ctx: DBAccessContext): Fox[AnnotationRestrictions] =

app/models/task/Task.scala

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.scalableminds.util.objectid.ObjectId
66
import com.scalableminds.util.time.Instant
77
import com.scalableminds.util.tools.Fox
88
import com.scalableminds.webknossos.schema.Tables._
9+
import com.scalableminds.webknossos.tracingstore.tracings.NamedBoundingBox
910

1011
import javax.inject.Inject
1112
import models.annotation._
@@ -59,7 +60,7 @@ class TaskDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext)
5960
r.totalinstances,
6061
r.pendinginstances,
6162
r.tracingtime,
62-
r.boundingbox.map(b => parseArrayLiteral(b).map(_.toInt)).flatMap(BoundingBox.fromSQL),
63+
parseBboxOpt(r.boundingbox),
6364
editPosition,
6465
editRotation,
6566
r.creationinfo,
@@ -68,6 +69,9 @@ class TaskDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext)
6869
)
6970
}
7071

72+
private def parseBboxOpt(bboxLiteral: Option[String]): Option[BoundingBox] =
73+
bboxLiteral.map(b => parseArrayLiteral(b).map(_.toInt)).flatMap(BoundingBox.fromSQL)
74+
7175
override protected def readAccessQ(requestingUserId: ObjectId) =
7276
q"""((SELECT _team FROM webknossos.projects p WHERE _project = p._id) IN (SELECT _team FROM webknossos.user_team_roles WHERE _user = $requestingUserId)
7377
or ((SELECT _organization FROM webknossos.teams WHERE webknossos.teams._id = (SELECT _team FROM webknossos.projects p WHERE _project = p._id))
@@ -282,6 +286,22 @@ class TaskDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext)
282286
q"SELECT domain FROM webknossos.experienceDomains WHERE _organization = $organizationId".as[String])
283287
} yield rowsRaw.toList
284288

289+
def findTaskBoundingBoxesByAnnotationIds(annotationIds: Seq[ObjectId]): Fox[Seq[NamedBoundingBox]] =
290+
for {
291+
rowsRaw <- run(q"""SELECT t.boundingBox, t._id, a._id
292+
FROM webknossos.tasks_ t
293+
JOIN webknossos.annotations_ a on a._task = t._id
294+
WHERE a._id IN ${SqlToken.tupleFromList(annotationIds)}
295+
AND t.boundingBox IS NOT NULL
296+
ORDER BY t._id
297+
""".as[(String, ObjectId, ObjectId)])
298+
namedBboxes = rowsRaw.flatMap {
299+
case (bboxLiteral, taskId, annotationId) =>
300+
parseBboxOpt(Some(bboxLiteral)).map(bbox =>
301+
NamedBoundingBox(0, Some(s"Task bounding box of instance $annotationId of task $taskId"), None, None, bbox))
302+
}
303+
} yield namedBboxes
304+
285305
def insertOne(t: Task): Fox[Unit] =
286306
for {
287307
_ <- run(q"""INSERT INTO webknossos.tasks(_id, _project, _script, _taskType,

conf/messages

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,14 +216,9 @@ annotation.notActive=The annotation is not active and cannot be finished
216216
annotation.reset.failed=Could not reset annotation to base
217217
annotation.merge.success=Merging successfully done
218218
annotation.merge.failed=Merging failed
219-
annotation.merge.failed.compound=Could not merge annotations for compound view
220-
annotation.transferee.noDatasetAccess=Cannot transfer annotation to a user who has no access to the dataset
221-
annotation.timelogging.read.failed=Time
222219
annotation.write.failed=Could not convert annotation to json
223220
annotation.merge.failed.compound=Couldn’t merge annotations for compound view
224221
annotation.transferee.noDatasetAccess=Cannot transfer annotation to a user who has no access to the dataset
225-
annotation.timelogging.read.failed=Time
226-
annotation.write.failed=Could not convert annotation to json
227222
annotation.needsEitherSkeletonOrVolume=Annotation needs at least one of skeleton or volume
228223
annotation.addLayer.explorationalsOnly=Could not add annotation layer because it is only allowed for explorational annotations.
229224
annotation.addLayer.nameInUse=An annotation layer with this name already exists in this annotation. Please change it to prevent duplicates.

unreleased_changes/8735.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
### Changed
2+
- Compound annotations (merged views of finished task annotations) now show the task bounding boxes.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/SkeletonTracingController.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ class SkeletonTracingController @Inject()(skeletonTracingService: SkeletonTracin
108108
accessTokenService.validateAccessFromTokenContext(UserAccessRequest.webknossos) {
109109
val tracings: List[Option[SkeletonTracing]] = request.body
110110
for {
111-
mergedTracing <- skeletonTracingService.merge(tracings.flatten, newVersion = 0L).toFox
111+
mergedTracing <- skeletonTracingService
112+
.merge(tracings.flatten, newVersion = 0L, additionalBoundingBoxes = Seq.empty)
113+
.toFox
112114
processedTracing = skeletonTracingService.remapTooLargeTreeIds(mergedTracing)
113115
_ <- skeletonTracingService.saveSkeleton(newTracingId, processedTracing.version, processedTracing)
114116
} yield Ok

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/TSAnnotationController.scala

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ import play.api.mvc.{Action, AnyContent, PlayBodyParsers}
3535

3636
import scala.concurrent.ExecutionContext
3737

38-
case class MergedFromIdsRequest(annotationIds: Seq[ObjectId], ownerIds: Seq[ObjectId])
38+
case class MergedFromIdsRequest(annotationIds: Seq[ObjectId],
39+
ownerIds: Seq[ObjectId],
40+
additionalBoundingBoxes: Seq[NamedBoundingBox])
3941

4042
object MergedFromIdsRequest {
4143
implicit val jsonFormat: OFormat[MergedFromIdsRequest] = Json.format[MergedFromIdsRequest]
@@ -243,7 +245,11 @@ class TSAnnotationController @Inject()(
243245
toTemporaryStore) ?~> "mergeVolumeData.failed"
244246
mergedVolumeOpt <- Fox.runIf(volumeTracings.nonEmpty)(
245247
volumeTracingService
246-
.merge(volumeTracings, mergedVolumeStats, newMappingName, newVersion = newTargetVersion)
248+
.merge(volumeTracings,
249+
mergedVolumeStats,
250+
newMappingName,
251+
newVersion = newTargetVersion,
252+
additionalBoundingBoxes = request.body.additionalBoundingBoxes)
247253
.toFox) ?~> "mergeVolume.failed"
248254
_ <- Fox.runOptional(mergedVolumeOpt)(
249255
volumeTracingService.saveVolume(newVolumeId, version = newTargetVersion, _, toTemporaryStore))
@@ -254,7 +260,11 @@ class TSAnnotationController @Inject()(
254260
}
255261
skeletonTracings = skeletonTracingsAdaptedNested.flatten
256262
mergedSkeletonOpt <- Fox.runIf(skeletonTracings.nonEmpty)(
257-
skeletonTracingService.merge(skeletonTracings, newVersion = newTargetVersion).toFox)
263+
skeletonTracingService
264+
.merge(skeletonTracings,
265+
newVersion = newTargetVersion,
266+
additionalBoundingBoxes = request.body.additionalBoundingBoxes)
267+
.toFox)
258268
_ <- Fox.runOptional(mergedSkeletonOpt)(
259269
skeletonTracingService
260270
.saveSkeleton(newSkeletonId, version = newTargetVersion, _, toTemporaryStore = toTemporaryStore))

0 commit comments

Comments
 (0)