Skip to content

Commit

Permalink
Show Annotation Segment Counts in Dashboard (#7548)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Jan 22, 2024
1 parent 9187eff commit d8a2931
Show file tree
Hide file tree
Showing 32 changed files with 453 additions and 275 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- 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)
- Multiple segments can be dragged and dropped in the segments tab. [#7536](https://github.com/scalableminds/webknossos/pull/7536)
- 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)
- 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)

### Changed
- Improved loading speed of the annotation list. [#7410](https://github.com/scalableminds/webknossos/pull/7410)
Expand Down
9 changes: 7 additions & 2 deletions app/controllers/AnnotationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import com.scalableminds.util.geometry.BoundingBox
import com.scalableminds.util.time.Instant
import com.scalableminds.util.tools.{Fox, FoxImplicits}
import com.scalableminds.webknossos.datastore.models.annotation.AnnotationLayerType.AnnotationLayerType
import com.scalableminds.webknossos.datastore.models.annotation.{AnnotationLayer, AnnotationLayerType}
import com.scalableminds.webknossos.datastore.models.annotation.{
AnnotationLayer,
AnnotationLayerStatistics,
AnnotationLayerType
}
import com.scalableminds.webknossos.datastore.models.datasource.AdditionalAxis
import com.scalableminds.webknossos.datastore.rpc.RPC
import com.scalableminds.webknossos.tracingstore.tracings.volume.ResolutionRestrictions
Expand Down Expand Up @@ -283,7 +287,8 @@ class AnnotationController @Inject()(
List(
AnnotationLayer(TracingIds.dummyTracingId,
AnnotationLayerType.Skeleton,
AnnotationLayer.defaultSkeletonLayerName))
AnnotationLayer.defaultSkeletonLayerName,
AnnotationLayerStatistics.unknown))
)
json <- annotationService.publicWrites(annotation, request.identity) ?~> "annotation.write.failed"
} yield JsonOk(json)
Expand Down
13 changes: 10 additions & 3 deletions app/controllers/AnnotationIOController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.scalableminds.webknossos.datastore.VolumeTracing.{VolumeTracing, Volu
import com.scalableminds.webknossos.datastore.helpers.ProtoGeometryImplicits
import com.scalableminds.webknossos.datastore.models.annotation.{
AnnotationLayer,
AnnotationLayerStatistics,
AnnotationLayerType,
FetchedAnnotationLayer
}
Expand Down Expand Up @@ -159,7 +160,8 @@ class AnnotationIOController @Inject()(
AnnotationLayer(
savedTracingId,
AnnotationLayerType.Volume,
uploadedVolumeLayer.name.getOrElse(AnnotationLayer.defaultVolumeLayerName + idx.toString)
uploadedVolumeLayer.name.getOrElse(AnnotationLayer.defaultVolumeLayerName + idx.toString),
AnnotationLayerStatistics.unknown
)
}
} else { // Multiple annotations with volume layers (but at most one each) was uploaded merge those volume layers into one
Expand All @@ -175,7 +177,8 @@ class AnnotationIOController @Inject()(
AnnotationLayer(
mergedTracingId,
AnnotationLayerType.Volume,
AnnotationLayer.defaultVolumeLayerName
AnnotationLayer.defaultVolumeLayerName,
AnnotationLayerStatistics.unknown
))
}

Expand All @@ -189,7 +192,11 @@ class AnnotationIOController @Inject()(
SkeletonTracings(skeletonTracings.map(t => SkeletonTracingOpt(Some(t)))),
persistTracing = true)
} yield
List(AnnotationLayer(mergedTracingId, AnnotationLayerType.Skeleton, AnnotationLayer.defaultSkeletonLayerName))
List(
AnnotationLayer(mergedTracingId,
AnnotationLayerType.Skeleton,
AnnotationLayer.defaultSkeletonLayerName,
AnnotationLayerStatistics.unknown))
}

private def assertNonEmpty(parseSuccesses: List[NmlParseSuccess]) =
Expand Down
15 changes: 11 additions & 4 deletions app/controllers/WKRemoteTracingStoreController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ import com.scalableminds.webknossos.tracingstore.TracingUpdatesReport
import javax.inject.Inject
import models.analytics.{AnalyticsService, UpdateAnnotationEvent, UpdateAnnotationViewOnlyEvent}
import models.annotation.AnnotationState._
import models.annotation.{Annotation, AnnotationDAO, AnnotationInformationProvider, TracingStoreService}
import models.annotation.{
Annotation,
AnnotationDAO,
AnnotationInformationProvider,
AnnotationLayerDAO,
TracingStoreService
}
import models.dataset.{DatasetDAO, DatasetService}
import models.organization.OrganizationDAO
import models.user.UserDAO
Expand All @@ -31,7 +37,8 @@ class WKRemoteTracingStoreController @Inject()(
annotationInformationProvider: AnnotationInformationProvider,
analyticsService: AnalyticsService,
datasetDAO: DatasetDAO,
annotationDAO: AnnotationDAO)(implicit ec: ExecutionContext, playBodyParsers: PlayBodyParsers)
annotationDAO: AnnotationDAO,
annotationLayerDAO: AnnotationLayerDAO)(implicit ec: ExecutionContext, playBodyParsers: PlayBodyParsers)
extends Controller
with FoxImplicits {

Expand All @@ -46,10 +53,10 @@ class WKRemoteTracingStoreController @Inject()(
for {
annotation <- annotationDAO.findOneByTracingId(report.tracingId)
_ <- ensureAnnotationNotFinished(annotation)
_ <- annotationDAO.updateModified(annotation._id, Instant.now)
_ <- Fox.runOptional(report.statistics) { statistics =>
annotationDAO.updateStatistics(annotation._id, statistics)
annotationLayerDAO.updateStatistics(annotation._id, report.tracingId, statistics)
}
_ <- annotationDAO.updateModified(annotation._id, Instant.now)
userBox <- bearerTokenService.userForTokenOpt(report.userToken).futureBox
_ <- Fox.runOptional(userBox)(user => timeSpanService.logUserInteraction(report.timestamps, user, annotation))
_ <- Fox.runOptional(userBox)(user =>
Expand Down
71 changes: 31 additions & 40 deletions app/models/annotation/Annotation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ case class Annotation(
name: String = "",
viewConfiguration: Option[JsObject] = None,
state: AnnotationState.Value = Active,
statistics: JsObject = Json.obj(),
tags: Set[String] = Set.empty,
tracingTime: Option[Long] = None,
typ: AnnotationType.Value = AnnotationType.Explorational,
Expand Down Expand Up @@ -83,7 +82,6 @@ case class AnnotationCompactInfo(id: ObjectId,
teamNames: Seq[String],
teamOrganizationIds: Seq[ObjectId],
modified: Instant,
stats: JsObject,
tags: Set[String],
state: AnnotationState.Value = Active,
dataSetName: String,
Expand All @@ -92,7 +90,8 @@ case class AnnotationCompactInfo(id: ObjectId,
organizationName: String,
tracingIds: Seq[String],
annotationLayerNames: Seq[String],
annotationLayerTypes: Seq[String])
annotationLayerTypes: Seq[String],
annotationLayerStatistics: Seq[JsObject])

object AnnotationCompactInfo {
implicit val jsonFormat: Format[AnnotationCompactInfo] = Json.format[AnnotationCompactInfo]
Expand All @@ -108,14 +107,15 @@ class AnnotationLayerDAO @Inject()(SQLClient: SqlClient)(implicit ec: ExecutionC
AnnotationLayer(
r.tracingid,
typ,
r.name
r.name,
Json.parse(r.statistics).as[JsObject],
)
}

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

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

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

def updateStatistics(annotationId: ObjectId, tracingId: String, statistics: JsObject): Fox[Unit] =
for {
_ <- run(q"""UPDATE webknossos.annotation_layers
SET statistics = $statistics
WHERE _annotation = $annotationId
AND tracingId = $tracingId""".asUpdate)
} yield ()
}

class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: AnnotationLayerDAO)(
Expand Down Expand Up @@ -206,7 +213,6 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
r.name,
viewconfigurationOpt,
state,
Json.parse(r.statistics).as[JsObject],
parseArrayLiteral(r.tags).toSet,
r.tracingtime,
typ,
Expand Down Expand Up @@ -332,7 +338,6 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
STRING_AGG(t.name, ',') AS team_names,
STRING_AGG(t._organization, ',') AS team_orgs,
a.modified,
a.statistics,
a.tags,
a.state,
d.name,
Expand All @@ -342,7 +347,8 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
o.name,
STRING_AGG(al.tracingid, ',') AS tracing_ids,
STRING_AGG(al.name, ',') AS tracing_names,
STRING_AGG(al.typ :: varchar, ',') AS tracing_typs
STRING_AGG(al.typ :: varchar, ',') AS tracing_typs,
ARRAY_AGG(al.statistics) AS annotation_layer_statistics
FROM webknossos.annotations as a
LEFT JOIN webknossos.users_ u
ON u._id = a._user
Expand Down Expand Up @@ -378,11 +384,11 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
String,
String,
String,
String,
Long,
String,
String,
String,
String,
String)])
} yield
rows.toList.map(
Expand All @@ -399,17 +405,18 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
teamNames = Option(r._9).map(_.split(",")).getOrElse(Array[String]()).toSeq,
teamOrganizationIds = parseObjectIdArray(r._10),
modified = r._11,
stats = Json.parse(r._12).validate[JsObject].getOrElse(Json.obj()),
tags = parseArrayLiteral(r._13).toSet,
state = AnnotationState.fromString(r._14).getOrElse(AnnotationState.Active),
dataSetName = r._15,
typ = AnnotationType.fromString(r._16).getOrElse(AnnotationType.Explorational),
visibility = AnnotationVisibility.fromString(r._17).getOrElse(AnnotationVisibility.Internal),
tracingTime = Option(r._18),
organizationName = r._19,
tracingIds = Option(r._20).map(_.split(",")).getOrElse(Array[String]()).toSeq,
annotationLayerNames = Option(r._21).map(_.split(",")).getOrElse(Array[String]()).toSeq,
annotationLayerTypes = Option(r._22).map(_.split(",")).getOrElse(Array[String]()).toSeq
tags = parseArrayLiteral(r._12).toSet,
state = AnnotationState.fromString(r._13).getOrElse(AnnotationState.Active),
dataSetName = r._14,
typ = AnnotationType.fromString(r._15).getOrElse(AnnotationType.Explorational),
visibility = AnnotationVisibility.fromString(r._16).getOrElse(AnnotationVisibility.Internal),
tracingTime = Option(r._17),
organizationName = r._18,
tracingIds = Option(r._19).map(_.split(",")).getOrElse(Array[String]()).toSeq,
annotationLayerNames = Option(r._20).map(_.split(",")).getOrElse(Array[String]()).toSeq,
annotationLayerTypes = Option(r._21).map(_.split(",")).getOrElse(Array[String]()).toSeq,
annotationLayerStatistics = parseArrayLiteral(r._22).map(layerStats =>
Json.parse(layerStats).validate[JsObject].getOrElse(Json.obj()))
)
}
)
Expand Down Expand Up @@ -549,11 +556,11 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
def insertOne(a: Annotation): Fox[Unit] = {
val insertAnnotationQuery = q"""
insert into webknossos.annotations(_id, _dataset, _task, _team, _user, description, visibility,
name, viewConfiguration, state, statistics, tags, tracingTime, typ, othersMayEdit, created, modified, isDeleted)
name, viewConfiguration, state, tags, tracingTime, typ, othersMayEdit, created, modified, isDeleted)
values(${a._id}, ${a._dataset}, ${a._task}, ${a._team},
${a._user}, ${a.description}, ${a.visibility}, ${a.name},
${a.viewConfiguration},
${a.state}, ${a.statistics},
${a.state},
${a.tags}, ${a.tracingTime}, ${a.typ},
${a.othersMayEdit},
${a.created}, ${a.modified}, ${a.isDeleted})
Expand All @@ -577,7 +584,6 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
name = ${a.name},
viewConfiguration = ${a.viewConfiguration},
state = ${a.state},
statistics = ${a.statistics},
tags = ${a.tags.toList},
tracingTime = ${a.tracingTime},
typ = ${a.typ},
Expand Down Expand Up @@ -663,12 +669,6 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
_ <- run(q"update webknossos.annotations set modified = $modified where _id = $id".asUpdate)
} yield ()

def updateStatistics(id: ObjectId, statistics: JsObject)(implicit ctx: DBAccessContext): Fox[Unit] =
for {
_ <- assertUpdateAccess(id)
_ <- run(q"update webknossos.annotations set statistics = $statistics where _id = $id".asUpdate)
} yield ()

def updateUser(id: ObjectId, userId: ObjectId)(implicit ctx: DBAccessContext): Fox[Unit] =
updateObjectIdCol(id, _._User, userId)

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

// Does not use access query (because they dont support prefixes). Use only after separate access check!
def findAllSharedForTeams(teams: List[ObjectId]): Fox[List[Annotation]] =
for {
result <- run(q"""select distinct ${columnsWithPrefix("a.")} from webknossos.annotations_ a
join webknossos.annotation_sharedTeams l on a._id = l._annotation
where l._team in ${SqlToken.tupleFromList(teams)}""".as[AnnotationsRow])
parsed <- Fox.combined(result.toList.map(parse))
} yield parsed

def updateTeamsForSharedAnnotation(annotationId: ObjectId, teams: List[ObjectId])(
implicit ctx: DBAccessContext): Fox[Unit] = {
val clearQuery = q"delete from webknossos.annotation_sharedTeams where _annotation = $annotationId".asUpdate
Expand Down
12 changes: 9 additions & 3 deletions app/models/annotation/AnnotationMerger.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ package models.annotation

import com.scalableminds.util.accesscontext.DBAccessContext
import com.scalableminds.util.tools.{Fox, FoxImplicits}
import com.scalableminds.webknossos.datastore.models.annotation.{AnnotationLayer, AnnotationLayerType}
import com.scalableminds.webknossos.datastore.models.annotation.{
AnnotationLayer,
AnnotationLayerStatistics,
AnnotationLayerType
}
import com.typesafe.scalalogging.LazyLogging

import javax.inject.Inject
Expand Down Expand Up @@ -78,12 +82,14 @@ class AnnotationMerger @Inject()(datasetDAO: DatasetDAO, tracingStoreService: Tr
id =>
AnnotationLayer(id,
AnnotationLayerType.Skeleton,
mergedSkeletonName.getOrElse(AnnotationLayer.defaultSkeletonLayerName)))
mergedSkeletonName.getOrElse(AnnotationLayer.defaultSkeletonLayerName),
AnnotationLayerStatistics.unknown))
mergedVolumeLayer = mergedVolumeTracingId.map(
id =>
AnnotationLayer(id,
AnnotationLayerType.Volume,
mergedVolumeName.getOrElse(AnnotationLayer.defaultVolumeLayerName)))
mergedVolumeName.getOrElse(AnnotationLayer.defaultVolumeLayerName),
AnnotationLayerStatistics.unknown))
} yield List(mergedSkeletonLayer, mergedVolumeLayer).flatten

private def allEqual(str: List[String]): Option[String] =
Expand Down
Loading

0 comments on commit d8a2931

Please sign in to comment.