Skip to content

Commit d8a2931

Browse files
fm3philippottoMichaelBuessemeyer
authored
Show Annotation Segment Counts in Dashboard (#7548)
* WIP: save annotation stats per layer * store annotation stats per layer in postgres * generalize annotation stats and send segmentCount for volume tracings * sql migration * add zeros for new annotation layers * integrate segment stats into dashboard and unify with stats in dataset-info-sidebar * remove obsolete stats property from annotation object * changelog * don't show segment or tree count if a skeleton-only or volume-only annotation is present * fix unit tests * test db, snapshots * rename stats to statistics in listExplorationals json * fix typing * rename statistics to stats in annotation json * re-add stats property to type definition * use pluralize for segment and tree count formatting * add missing import * DRY helper type --------- Co-authored-by: Philipp Otto <[email protected]> Co-authored-by: MichaelBuessemeyer <[email protected]>
1 parent 9187eff commit d8a2931

32 files changed

+453
-275
lines changed

CHANGELOG.unreleased.md

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
2222
- When setting up WEBKNOSSOS from the git repository for development, the organization directory for storing datasets is now automatically created on startup. [#7517](https://github.com/scalableminds/webknossos/pull/7517)
2323
- Multiple segments can be dragged and dropped in the segments tab. [#7536](https://github.com/scalableminds/webknossos/pull/7536)
2424
- Added the option to convert agglomerate skeletons to freely modifiable skeletons in the context menu of the Skeleton tab. [#7537](https://github.com/scalableminds/webknossos/pull/7537)
25+
- The annotation list in the dashboard now also shows segment counts of volume annotations (after they have been edited). [#7548](https://github.com/scalableminds/webknossos/pull/7548)
2526

2627
### Changed
2728
- Improved loading speed of the annotation list. [#7410](https://github.com/scalableminds/webknossos/pull/7410)

app/controllers/AnnotationController.scala

+7-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import com.scalableminds.util.geometry.BoundingBox
77
import com.scalableminds.util.time.Instant
88
import com.scalableminds.util.tools.{Fox, FoxImplicits}
99
import com.scalableminds.webknossos.datastore.models.annotation.AnnotationLayerType.AnnotationLayerType
10-
import com.scalableminds.webknossos.datastore.models.annotation.{AnnotationLayer, AnnotationLayerType}
10+
import com.scalableminds.webknossos.datastore.models.annotation.{
11+
AnnotationLayer,
12+
AnnotationLayerStatistics,
13+
AnnotationLayerType
14+
}
1115
import com.scalableminds.webknossos.datastore.models.datasource.AdditionalAxis
1216
import com.scalableminds.webknossos.datastore.rpc.RPC
1317
import com.scalableminds.webknossos.tracingstore.tracings.volume.ResolutionRestrictions
@@ -283,7 +287,8 @@ class AnnotationController @Inject()(
283287
List(
284288
AnnotationLayer(TracingIds.dummyTracingId,
285289
AnnotationLayerType.Skeleton,
286-
AnnotationLayer.defaultSkeletonLayerName))
290+
AnnotationLayer.defaultSkeletonLayerName,
291+
AnnotationLayerStatistics.unknown))
287292
)
288293
json <- annotationService.publicWrites(annotation, request.identity) ?~> "annotation.write.failed"
289294
} yield JsonOk(json)

app/controllers/AnnotationIOController.scala

+10-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.scalableminds.webknossos.datastore.VolumeTracing.{VolumeTracing, Volu
1313
import com.scalableminds.webknossos.datastore.helpers.ProtoGeometryImplicits
1414
import com.scalableminds.webknossos.datastore.models.annotation.{
1515
AnnotationLayer,
16+
AnnotationLayerStatistics,
1617
AnnotationLayerType,
1718
FetchedAnnotationLayer
1819
}
@@ -159,7 +160,8 @@ class AnnotationIOController @Inject()(
159160
AnnotationLayer(
160161
savedTracingId,
161162
AnnotationLayerType.Volume,
162-
uploadedVolumeLayer.name.getOrElse(AnnotationLayer.defaultVolumeLayerName + idx.toString)
163+
uploadedVolumeLayer.name.getOrElse(AnnotationLayer.defaultVolumeLayerName + idx.toString),
164+
AnnotationLayerStatistics.unknown
163165
)
164166
}
165167
} else { // Multiple annotations with volume layers (but at most one each) was uploaded merge those volume layers into one
@@ -175,7 +177,8 @@ class AnnotationIOController @Inject()(
175177
AnnotationLayer(
176178
mergedTracingId,
177179
AnnotationLayerType.Volume,
178-
AnnotationLayer.defaultVolumeLayerName
180+
AnnotationLayer.defaultVolumeLayerName,
181+
AnnotationLayerStatistics.unknown
179182
))
180183
}
181184

@@ -189,7 +192,11 @@ class AnnotationIOController @Inject()(
189192
SkeletonTracings(skeletonTracings.map(t => SkeletonTracingOpt(Some(t)))),
190193
persistTracing = true)
191194
} yield
192-
List(AnnotationLayer(mergedTracingId, AnnotationLayerType.Skeleton, AnnotationLayer.defaultSkeletonLayerName))
195+
List(
196+
AnnotationLayer(mergedTracingId,
197+
AnnotationLayerType.Skeleton,
198+
AnnotationLayer.defaultSkeletonLayerName,
199+
AnnotationLayerStatistics.unknown))
193200
}
194201

195202
private def assertNonEmpty(parseSuccesses: List[NmlParseSuccess]) =

app/controllers/WKRemoteTracingStoreController.scala

+11-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ import com.scalableminds.webknossos.tracingstore.TracingUpdatesReport
99
import javax.inject.Inject
1010
import models.analytics.{AnalyticsService, UpdateAnnotationEvent, UpdateAnnotationViewOnlyEvent}
1111
import models.annotation.AnnotationState._
12-
import models.annotation.{Annotation, AnnotationDAO, AnnotationInformationProvider, TracingStoreService}
12+
import models.annotation.{
13+
Annotation,
14+
AnnotationDAO,
15+
AnnotationInformationProvider,
16+
AnnotationLayerDAO,
17+
TracingStoreService
18+
}
1319
import models.dataset.{DatasetDAO, DatasetService}
1420
import models.organization.OrganizationDAO
1521
import models.user.UserDAO
@@ -31,7 +37,8 @@ class WKRemoteTracingStoreController @Inject()(
3137
annotationInformationProvider: AnnotationInformationProvider,
3238
analyticsService: AnalyticsService,
3339
datasetDAO: DatasetDAO,
34-
annotationDAO: AnnotationDAO)(implicit ec: ExecutionContext, playBodyParsers: PlayBodyParsers)
40+
annotationDAO: AnnotationDAO,
41+
annotationLayerDAO: AnnotationLayerDAO)(implicit ec: ExecutionContext, playBodyParsers: PlayBodyParsers)
3542
extends Controller
3643
with FoxImplicits {
3744

@@ -46,10 +53,10 @@ class WKRemoteTracingStoreController @Inject()(
4653
for {
4754
annotation <- annotationDAO.findOneByTracingId(report.tracingId)
4855
_ <- ensureAnnotationNotFinished(annotation)
56+
_ <- annotationDAO.updateModified(annotation._id, Instant.now)
4957
_ <- Fox.runOptional(report.statistics) { statistics =>
50-
annotationDAO.updateStatistics(annotation._id, statistics)
58+
annotationLayerDAO.updateStatistics(annotation._id, report.tracingId, statistics)
5159
}
52-
_ <- annotationDAO.updateModified(annotation._id, Instant.now)
5360
userBox <- bearerTokenService.userForTokenOpt(report.userToken).futureBox
5461
_ <- Fox.runOptional(userBox)(user => timeSpanService.logUserInteraction(report.timestamps, user, annotation))
5562
_ <- Fox.runOptional(userBox)(user =>

app/models/annotation/Annotation.scala

+31-40
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ case class Annotation(
3333
name: String = "",
3434
viewConfiguration: Option[JsObject] = None,
3535
state: AnnotationState.Value = Active,
36-
statistics: JsObject = Json.obj(),
3736
tags: Set[String] = Set.empty,
3837
tracingTime: Option[Long] = None,
3938
typ: AnnotationType.Value = AnnotationType.Explorational,
@@ -83,7 +82,6 @@ case class AnnotationCompactInfo(id: ObjectId,
8382
teamNames: Seq[String],
8483
teamOrganizationIds: Seq[ObjectId],
8584
modified: Instant,
86-
stats: JsObject,
8785
tags: Set[String],
8886
state: AnnotationState.Value = Active,
8987
dataSetName: String,
@@ -92,7 +90,8 @@ case class AnnotationCompactInfo(id: ObjectId,
9290
organizationName: String,
9391
tracingIds: Seq[String],
9492
annotationLayerNames: Seq[String],
95-
annotationLayerTypes: Seq[String])
93+
annotationLayerTypes: Seq[String],
94+
annotationLayerStatistics: Seq[JsObject])
9695

9796
object AnnotationCompactInfo {
9897
implicit val jsonFormat: Format[AnnotationCompactInfo] = Json.format[AnnotationCompactInfo]
@@ -108,14 +107,15 @@ class AnnotationLayerDAO @Inject()(SQLClient: SqlClient)(implicit ec: ExecutionC
108107
AnnotationLayer(
109108
r.tracingid,
110109
typ,
111-
r.name
110+
r.name,
111+
Json.parse(r.statistics).as[JsObject],
112112
)
113113
}
114114

115115
def findAnnotationLayersFor(annotationId: ObjectId): Fox[List[AnnotationLayer]] =
116116
for {
117117
rows <- run(
118-
q"select _annotation, tracingId, typ, name from webknossos.annotation_layers where _annotation = $annotationId order by tracingId"
118+
q"select _annotation, tracingId, typ, name, statistics from webknossos.annotation_layers where _annotation = $annotationId order by tracingId"
119119
.as[AnnotationLayersRow])
120120
parsed <- Fox.serialCombined(rows.toList)(parse)
121121
} yield parsed
@@ -137,8 +137,8 @@ class AnnotationLayerDAO @Inject()(SQLClient: SqlClient)(implicit ec: ExecutionC
137137
}
138138

139139
private def insertOneQuery(annotationId: ObjectId, a: AnnotationLayer): SqlAction[Int, NoStream, Effect] =
140-
q"""insert into webknossos.annotation_layers(_annotation, tracingId, typ, name)
141-
values($annotationId, ${a.tracingId}, ${a.typ}, ${a.name})""".asUpdate
140+
q"""insert into webknossos.annotation_layers(_annotation, tracingId, typ, name, statistics)
141+
values($annotationId, ${a.tracingId}, ${a.typ}, ${a.name}, ${a.stats})""".asUpdate
142142

143143
def deleteOne(annotationId: ObjectId, layerName: String): Fox[Unit] =
144144
for {
@@ -176,6 +176,13 @@ class AnnotationLayerDAO @Inject()(SQLClient: SqlClient)(implicit ec: ExecutionC
176176
def deleteAllForAnnotationQuery(annotationId: ObjectId): SqlAction[Int, NoStream, Effect] =
177177
q"delete from webknossos.annotation_layers where _annotation = $annotationId".asUpdate
178178

179+
def updateStatistics(annotationId: ObjectId, tracingId: String, statistics: JsObject): Fox[Unit] =
180+
for {
181+
_ <- run(q"""UPDATE webknossos.annotation_layers
182+
SET statistics = $statistics
183+
WHERE _annotation = $annotationId
184+
AND tracingId = $tracingId""".asUpdate)
185+
} yield ()
179186
}
180187

181188
class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: AnnotationLayerDAO)(
@@ -206,7 +213,6 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
206213
r.name,
207214
viewconfigurationOpt,
208215
state,
209-
Json.parse(r.statistics).as[JsObject],
210216
parseArrayLiteral(r.tags).toSet,
211217
r.tracingtime,
212218
typ,
@@ -332,7 +338,6 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
332338
STRING_AGG(t.name, ',') AS team_names,
333339
STRING_AGG(t._organization, ',') AS team_orgs,
334340
a.modified,
335-
a.statistics,
336341
a.tags,
337342
a.state,
338343
d.name,
@@ -342,7 +347,8 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
342347
o.name,
343348
STRING_AGG(al.tracingid, ',') AS tracing_ids,
344349
STRING_AGG(al.name, ',') AS tracing_names,
345-
STRING_AGG(al.typ :: varchar, ',') AS tracing_typs
350+
STRING_AGG(al.typ :: varchar, ',') AS tracing_typs,
351+
ARRAY_AGG(al.statistics) AS annotation_layer_statistics
346352
FROM webknossos.annotations as a
347353
LEFT JOIN webknossos.users_ u
348354
ON u._id = a._user
@@ -378,11 +384,11 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
378384
String,
379385
String,
380386
String,
381-
String,
382387
Long,
383388
String,
384389
String,
385390
String,
391+
String,
386392
String)])
387393
} yield
388394
rows.toList.map(
@@ -399,17 +405,18 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
399405
teamNames = Option(r._9).map(_.split(",")).getOrElse(Array[String]()).toSeq,
400406
teamOrganizationIds = parseObjectIdArray(r._10),
401407
modified = r._11,
402-
stats = Json.parse(r._12).validate[JsObject].getOrElse(Json.obj()),
403-
tags = parseArrayLiteral(r._13).toSet,
404-
state = AnnotationState.fromString(r._14).getOrElse(AnnotationState.Active),
405-
dataSetName = r._15,
406-
typ = AnnotationType.fromString(r._16).getOrElse(AnnotationType.Explorational),
407-
visibility = AnnotationVisibility.fromString(r._17).getOrElse(AnnotationVisibility.Internal),
408-
tracingTime = Option(r._18),
409-
organizationName = r._19,
410-
tracingIds = Option(r._20).map(_.split(",")).getOrElse(Array[String]()).toSeq,
411-
annotationLayerNames = Option(r._21).map(_.split(",")).getOrElse(Array[String]()).toSeq,
412-
annotationLayerTypes = Option(r._22).map(_.split(",")).getOrElse(Array[String]()).toSeq
408+
tags = parseArrayLiteral(r._12).toSet,
409+
state = AnnotationState.fromString(r._13).getOrElse(AnnotationState.Active),
410+
dataSetName = r._14,
411+
typ = AnnotationType.fromString(r._15).getOrElse(AnnotationType.Explorational),
412+
visibility = AnnotationVisibility.fromString(r._16).getOrElse(AnnotationVisibility.Internal),
413+
tracingTime = Option(r._17),
414+
organizationName = r._18,
415+
tracingIds = Option(r._19).map(_.split(",")).getOrElse(Array[String]()).toSeq,
416+
annotationLayerNames = Option(r._20).map(_.split(",")).getOrElse(Array[String]()).toSeq,
417+
annotationLayerTypes = Option(r._21).map(_.split(",")).getOrElse(Array[String]()).toSeq,
418+
annotationLayerStatistics = parseArrayLiteral(r._22).map(layerStats =>
419+
Json.parse(layerStats).validate[JsObject].getOrElse(Json.obj()))
413420
)
414421
}
415422
)
@@ -549,11 +556,11 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
549556
def insertOne(a: Annotation): Fox[Unit] = {
550557
val insertAnnotationQuery = q"""
551558
insert into webknossos.annotations(_id, _dataset, _task, _team, _user, description, visibility,
552-
name, viewConfiguration, state, statistics, tags, tracingTime, typ, othersMayEdit, created, modified, isDeleted)
559+
name, viewConfiguration, state, tags, tracingTime, typ, othersMayEdit, created, modified, isDeleted)
553560
values(${a._id}, ${a._dataset}, ${a._task}, ${a._team},
554561
${a._user}, ${a.description}, ${a.visibility}, ${a.name},
555562
${a.viewConfiguration},
556-
${a.state}, ${a.statistics},
563+
${a.state},
557564
${a.tags}, ${a.tracingTime}, ${a.typ},
558565
${a.othersMayEdit},
559566
${a.created}, ${a.modified}, ${a.isDeleted})
@@ -577,7 +584,6 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
577584
name = ${a.name},
578585
viewConfiguration = ${a.viewConfiguration},
579586
state = ${a.state},
580-
statistics = ${a.statistics},
581587
tags = ${a.tags.toList},
582588
tracingTime = ${a.tracingTime},
583589
typ = ${a.typ},
@@ -663,12 +669,6 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
663669
_ <- run(q"update webknossos.annotations set modified = $modified where _id = $id".asUpdate)
664670
} yield ()
665671

666-
def updateStatistics(id: ObjectId, statistics: JsObject)(implicit ctx: DBAccessContext): Fox[Unit] =
667-
for {
668-
_ <- assertUpdateAccess(id)
669-
_ <- run(q"update webknossos.annotations set statistics = $statistics where _id = $id".asUpdate)
670-
} yield ()
671-
672672
def updateUser(id: ObjectId, userId: ObjectId)(implicit ctx: DBAccessContext): Fox[Unit] =
673673
updateObjectIdCol(id, _._User, userId)
674674

@@ -689,15 +689,6 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
689689
q"insert into webknossos.annotation_contributors (_annotation, _user) values($id, $userId) on conflict do nothing".asUpdate)
690690
} yield ()
691691

692-
// Does not use access query (because they dont support prefixes). Use only after separate access check!
693-
def findAllSharedForTeams(teams: List[ObjectId]): Fox[List[Annotation]] =
694-
for {
695-
result <- run(q"""select distinct ${columnsWithPrefix("a.")} from webknossos.annotations_ a
696-
join webknossos.annotation_sharedTeams l on a._id = l._annotation
697-
where l._team in ${SqlToken.tupleFromList(teams)}""".as[AnnotationsRow])
698-
parsed <- Fox.combined(result.toList.map(parse))
699-
} yield parsed
700-
701692
def updateTeamsForSharedAnnotation(annotationId: ObjectId, teams: List[ObjectId])(
702693
implicit ctx: DBAccessContext): Fox[Unit] = {
703694
val clearQuery = q"delete from webknossos.annotation_sharedTeams where _annotation = $annotationId".asUpdate

app/models/annotation/AnnotationMerger.scala

+9-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ package models.annotation
22

33
import com.scalableminds.util.accesscontext.DBAccessContext
44
import com.scalableminds.util.tools.{Fox, FoxImplicits}
5-
import com.scalableminds.webknossos.datastore.models.annotation.{AnnotationLayer, AnnotationLayerType}
5+
import com.scalableminds.webknossos.datastore.models.annotation.{
6+
AnnotationLayer,
7+
AnnotationLayerStatistics,
8+
AnnotationLayerType
9+
}
610
import com.typesafe.scalalogging.LazyLogging
711

812
import javax.inject.Inject
@@ -78,12 +82,14 @@ class AnnotationMerger @Inject()(datasetDAO: DatasetDAO, tracingStoreService: Tr
7882
id =>
7983
AnnotationLayer(id,
8084
AnnotationLayerType.Skeleton,
81-
mergedSkeletonName.getOrElse(AnnotationLayer.defaultSkeletonLayerName)))
85+
mergedSkeletonName.getOrElse(AnnotationLayer.defaultSkeletonLayerName),
86+
AnnotationLayerStatistics.unknown))
8287
mergedVolumeLayer = mergedVolumeTracingId.map(
8388
id =>
8489
AnnotationLayer(id,
8590
AnnotationLayerType.Volume,
86-
mergedVolumeName.getOrElse(AnnotationLayer.defaultVolumeLayerName)))
91+
mergedVolumeName.getOrElse(AnnotationLayer.defaultVolumeLayerName),
92+
AnnotationLayerStatistics.unknown))
8793
} yield List(mergedSkeletonLayer, mergedVolumeLayer).flatten
8894

8995
private def allEqual(str: List[String]): Option[String] =

0 commit comments

Comments
 (0)