Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show Annotation Segment Counts in Dashboard #7548

Merged
merged 28 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0331255
WIP: save annotation stats per layer
fm3 Jan 12, 2024
8eb9d7d
store annotation stats per layer in postgres
fm3 Jan 12, 2024
b9204b8
generalize annotation stats and send segmentCount for volume tracings
philippotto Jan 15, 2024
b740901
sql migration
fm3 Jan 15, 2024
ea3dc1c
add zeros for new annotation layers
fm3 Jan 15, 2024
46d27d8
integrate segment stats into dashboard and unify with stats in datase…
philippotto Jan 15, 2024
c85ff21
Merge branch 'save-segment-stats' of github.com:scalableminds/webknos…
philippotto Jan 15, 2024
22f4fbd
remove obsolete stats property from annotation object
philippotto Jan 15, 2024
7e830d5
Merge branch 'master' into save-segment-stats
fm3 Jan 16, 2024
60bf2dd
changelog
fm3 Jan 16, 2024
f443629
don't show segment or tree count if a skeleton-only or volume-only an…
philippotto Jan 18, 2024
d7c7b74
fix unit tests
philippotto Jan 18, 2024
89110a8
Merge branch 'master' into save-segment-stats
fm3 Jan 18, 2024
035ff55
test db, snapshots
fm3 Jan 18, 2024
c16933b
rename stats to statistics in listExplorationals json
fm3 Jan 18, 2024
aede04a
fix typing
philippotto Jan 18, 2024
fb1ef2d
Merge branch 'save-segment-stats' of github.com:scalableminds/webknos…
philippotto Jan 18, 2024
e8aff9e
rename statistics to stats in annotation json
fm3 Jan 18, 2024
6b24583
Merge branch 'save-segment-stats' of github.com:scalableminds/webknos…
fm3 Jan 18, 2024
39f0172
re-add stats property to type definition
philippotto Jan 18, 2024
d273939
Merge branch 'save-segment-stats' of github.com:scalableminds/webknos…
philippotto Jan 18, 2024
f72907f
use pluralize for segment and tree count formatting
philippotto Jan 18, 2024
5537668
add missing import
philippotto Jan 18, 2024
1770644
DRY helper type
philippotto Jan 22, 2024
eedff81
Merge branch 'master' into save-segment-stats
fm3 Jan 22, 2024
09d063e
Merge branch 'save-segment-stats' of github.com:scalableminds/webknos…
philippotto Jan 22, 2024
00b0513
Merge branch 'master' into save-segment-stats
MichaelBuessemeyer Jan 22, 2024
1be5fc7
Merge branch 'master' into save-segment-stats
fm3 Jan 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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