diff --git a/CHANGELOG.md b/CHANGELOG.md index 33396731e..d8a5e3ce8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The detailed rules and walkthrough of writing a changelog is located [here](http - Project versioning [WIP] - Project import image from cloud storage [WIP] - Database migration [WIP] +- Project statistic ## [2.0.0-alpha2] - 2021-08-12 ### Added diff --git a/classifai-core/src/main/java/ai/classifai/database/annotation/bndbox/BoundingBoxVerticle.java b/classifai-core/src/main/java/ai/classifai/database/annotation/bndbox/BoundingBoxVerticle.java index 69bde714e..deff46fca 100644 --- a/classifai-core/src/main/java/ai/classifai/database/annotation/bndbox/BoundingBoxVerticle.java +++ b/classifai-core/src/main/java/ai/classifai/database/annotation/bndbox/BoundingBoxVerticle.java @@ -15,23 +15,23 @@ */ package ai.classifai.database.annotation.bndbox; -import ai.classifai.database.DbConfig; -import ai.classifai.database.annotation.AnnotationQuery; -import ai.classifai.database.annotation.AnnotationVerticle; -import ai.classifai.util.ParamConfig; -import ai.classifai.util.message.ErrorCodes; -import ai.classifai.util.type.AnnotationHandler; -import ai.classifai.util.type.AnnotationType; -import ai.classifai.util.type.database.H2; -import ai.classifai.util.type.database.RelationalDb; -import io.vertx.core.Promise; -import io.vertx.core.Vertx; -import io.vertx.core.eventbus.Message; -import io.vertx.core.json.JsonObject; -import io.vertx.jdbcclient.JDBCPool; -import lombok.extern.slf4j.Slf4j; + import ai.classifai.database.DbConfig; + import ai.classifai.database.annotation.AnnotationQuery; + import ai.classifai.database.annotation.AnnotationVerticle; + import ai.classifai.util.ParamConfig; + import ai.classifai.util.message.ErrorCodes; + import ai.classifai.util.type.AnnotationHandler; + import ai.classifai.util.type.AnnotationType; + import ai.classifai.util.type.database.H2; + import ai.classifai.util.type.database.RelationalDb; + import io.vertx.core.Promise; + import io.vertx.core.Vertx; + import io.vertx.core.eventbus.Message; + import io.vertx.core.json.JsonObject; + import io.vertx.jdbcclient.JDBCPool; + import lombok.extern.slf4j.Slf4j; -/** + /** * Bounding Box Verticle * * @author codenamewei diff --git a/classifai-core/src/main/java/ai/classifai/database/portfolio/PortfolioDbQuery.java b/classifai-core/src/main/java/ai/classifai/database/portfolio/PortfolioDbQuery.java index 9adfa76c4..5687d2599 100644 --- a/classifai-core/src/main/java/ai/classifai/database/portfolio/PortfolioDbQuery.java +++ b/classifai-core/src/main/java/ai/classifai/database/portfolio/PortfolioDbQuery.java @@ -58,4 +58,7 @@ public class PortfolioDbQuery @Getter private static final String reloadProject = "SELECT project_path FROM Portfolio WHERE project_id = ?"; @Getter private static final String updateLastModifiedDate = "UPDATE Portfolio SET current_version = ? WHERE project_id = ?"; + + @Getter private static final String retrieveProjectStatistic = "retrieveProjectStatistic"; + } \ No newline at end of file diff --git a/classifai-core/src/main/java/ai/classifai/database/portfolio/PortfolioVerticle.java b/classifai-core/src/main/java/ai/classifai/database/portfolio/PortfolioVerticle.java index a8ddf01b6..02612e862 100644 --- a/classifai-core/src/main/java/ai/classifai/database/portfolio/PortfolioVerticle.java +++ b/classifai-core/src/main/java/ai/classifai/database/portfolio/PortfolioVerticle.java @@ -35,6 +35,7 @@ import ai.classifai.util.collection.ConversionHandler; import ai.classifai.util.collection.UuidGenerator; import ai.classifai.util.data.ImageHandler; +import ai.classifai.util.data.LabelListHandler; import ai.classifai.util.data.StringHandler; import ai.classifai.util.message.ErrorCodes; import ai.classifai.util.message.ReplyHandler; @@ -127,6 +128,10 @@ else if(action.equals(PortfolioDbQuery.getUpdateLastModifiedDate())) { this.updateLastModifiedDate(message); } + else if(action.equals(PortfolioDbQuery.getRetrieveProjectStatistic())) + { + this.getProjectStatistic(message); + } else { log.error("Portfolio query error. Action did not have an assigned function for handling."); @@ -655,6 +660,36 @@ public void reloadProject(Message message) ImageHandler.loadProjectRootPath(loader); } + public void getProjectStatistic(Message message) + { + String projectId = message.body().getString(ParamConfig.getProjectIdParam()); + + ProjectLoader loader = Objects.requireNonNull(ProjectHandler.getProjectLoader(projectId)); + + File projectPath = loader.getProjectPath(); + + LabelListHandler.getImageLabeledStatus(loader.getUuidAnnotationDict()); + JsonArray labelPerClassInProject = LabelListHandler.getLabelPerClassInProject(loader.getUuidAnnotationDict(), projectId); + + List result = new ArrayList<>(); + + if (!projectPath.exists()) + { + log.info(String.format("Root path of project [%s] is missing! %s does not exist.", loader.getProjectName(), loader.getProjectPath())); + } + + result.add(new JsonObject() + .put(ParamConfig.getProjectNameParam(), loader.getProjectName()) + .put(ParamConfig.getLabeledImageParam(), LabelListHandler.getNumberOfLabeledImage()) + .put(ParamConfig.getUnlabeledImageParam(), LabelListHandler.getNumberOfUnLabeledImage()) + .put(ParamConfig.getLabelPerClassInProject(), labelPerClassInProject)); + + JsonObject response = ReplyHandler.getOkReply(); + response.put(ParamConfig.getStatisticData(), result); + + message.replyAndRequest(response); + } + @Override public void stop(Promise promise) { diff --git a/classifai-core/src/main/java/ai/classifai/loader/ProjectLoader.java b/classifai-core/src/main/java/ai/classifai/loader/ProjectLoader.java index 75d3be75e..62c9ba378 100644 --- a/classifai-core/src/main/java/ai/classifai/loader/ProjectLoader.java +++ b/classifai-core/src/main/java/ai/classifai/loader/ProjectLoader.java @@ -102,6 +102,10 @@ public class ProjectLoader @Builder.Default private List unsupportedImageList = new ArrayList<>(); + // list of label and unlabelled image + @Builder.Default private List labelledImageList = new ArrayList<>(); + @Builder.Default private List unLabelledImageList = new ArrayList<>(); + public String getCurrentVersionUuid() { return projectVersion.getCurrentVersion().getVersionUuid(); diff --git a/classifai-core/src/main/java/ai/classifai/router/EndpointRouter.java b/classifai-core/src/main/java/ai/classifai/router/EndpointRouter.java index 7d6480cd2..964eaf1cc 100644 --- a/classifai-core/src/main/java/ai/classifai/router/EndpointRouter.java +++ b/classifai-core/src/main/java/ai/classifai/router/EndpointRouter.java @@ -161,6 +161,8 @@ public void start(Promise promise) router.put("/v2/close").handler(v2::closeClassifai); + router.get("/v2/:annotation_type/projects/:project_name/statistic").handler(v2::getProjectStatistic); + //*******************************Cloud******************************* router.put("/v2/:annotation_type/wasabi/projects/:project_name").handler(cloud::createWasabiCloudProject); diff --git a/classifai-core/src/main/java/ai/classifai/router/V2Endpoint.java b/classifai-core/src/main/java/ai/classifai/router/V2Endpoint.java index 3959a587b..2af3cbdd1 100644 --- a/classifai-core/src/main/java/ai/classifai/router/V2Endpoint.java +++ b/classifai-core/src/main/java/ai/classifai/router/V2Endpoint.java @@ -698,4 +698,55 @@ public void renameData(RoutingContext context) }); } + + + /** + * Retrieve number of labeled Image, unlabeled Image and total number of labels per class in a project + * + * GET http://localhost:{port}/v2/:annotation_type/projects/:project_name/statistic + * + * Example: + * GET http://localhost:{port}/v2/bndbox/projects/demo/statistic + * + */ + public void getProjectStatistic (RoutingContext context){ + + AnnotationType type = AnnotationHandler.getTypeFromEndpoint(context.request().getParam(ParamConfig.getAnnotationTypeParam())); + + String projectName = context.request().getParam(ParamConfig.getProjectNameParam()); + + log.debug("Get project statistic : " + projectName + " of annotation type: " + type.name()); + + ProjectLoader loader = ProjectHandler.getProjectLoader(projectName, type); + + if(helper.checkIfProjectNull(context, loader, projectName)) return; + + if(loader == null) + { + HTTPResponseHandler.configureOK(context, ReplyHandler.reportUserDefinedError("Failure in retrieving statistic of project: " + projectName)); + } + + JsonObject jsonObject = new JsonObject().put(ParamConfig.getProjectIdParam(), Objects.requireNonNull(loader).getProjectId()); + + //load label list + DeliveryOptions statisticDataOptions = new DeliveryOptions().addHeader(ParamConfig.getActionKeyword(), PortfolioDbQuery.getRetrieveProjectStatistic()); + + vertx.eventBus().request(PortfolioDbQuery.getQueue(), jsonObject, statisticDataOptions, statisticReply -> + { + if (statisticReply.succeeded()) { + + JsonObject statisticResponse = (JsonObject) statisticReply.result().body(); + + if (ReplyHandler.isReplyOk(statisticResponse)) + { + HTTPResponseHandler.configureOK(context, statisticResponse); + } + else + { + HTTPResponseHandler.configureOK(context, ReplyHandler.reportUserDefinedError("Failed to retrieve statistic for project " + projectName)); + } + } + }); + + } } diff --git a/classifai-core/src/main/java/ai/classifai/util/ParamConfig.java b/classifai-core/src/main/java/ai/classifai/util/ParamConfig.java index b3fe440af..8e54d58c6 100644 --- a/classifai-core/src/main/java/ai/classifai/util/ParamConfig.java +++ b/classifai-core/src/main/java/ai/classifai/util/ParamConfig.java @@ -95,6 +95,12 @@ public class ParamConfig @Getter private static final String createdDateParam = "created_date"; @Getter private static final String lastModifiedDate = "last_modified_date"; @Getter private static final String isRootPathValidParam = "root_path_valid"; + @Getter private static final String labeledImageParam = "labeled_image"; + @Getter private static final String unlabeledImageParam = "unlabeled_image"; + @Getter private static final String labelPerClassInProject = "label_per_class_in_project"; + @Getter private static final String labelParam = "label"; + @Getter private static final String labelCountParam = "count"; + @Getter private static final String statisticData = "statistic_data"; @Getter private static final String statusParam = "status"; diff --git a/classifai-core/src/main/java/ai/classifai/util/data/LabelListHandler.java b/classifai-core/src/main/java/ai/classifai/util/data/LabelListHandler.java new file mode 100644 index 000000000..b72794ecb --- /dev/null +++ b/classifai-core/src/main/java/ai/classifai/util/data/LabelListHandler.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2021 CertifAI Sdn. Bhd. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package ai.classifai.util.data; + +import ai.classifai.database.versioning.Annotation; +import ai.classifai.loader.ProjectLoader; +import ai.classifai.util.ParamConfig; +import ai.classifai.util.project.ProjectHandler; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * Getting information of labeled and unlabeled image + * + * @author ken479 + */ + +@Slf4j +public class LabelListHandler { + + private LabelListHandler() + { + throw new IllegalStateException("Utility class"); + } + + private static final List> totalImage = new ArrayList<>(); + + // Handling the number of labeled and unlabeled image + public static void getImageLabeledStatus(Map uuidAnnotationDict) + { + Set imageUUID = uuidAnnotationDict.keySet(); // key for each project + List annotationList = getAnnotationList(imageUUID, uuidAnnotationDict);// a list of annotation + + // To get a list of JsonArray, whereby each JsonArray contain annotation parameters of an image + List labelPointData = annotationList.stream() + .map(Annotation::getAnnotationDictDbFormat) + .map(LabelListHandler::getAnnotationData) + .map(Map::values) + .map(String::valueOf) + .map(LabelListHandler::getAnnotationStatus) + .collect(Collectors.toList()); + + Predicate isEmpty = JsonArray::isEmpty; + Predicate notEmpty = isEmpty.negate(); + + // Checking If a JsonArray is not empty, annotation was performed on an image + List labeledImageList = labelPointData.stream() + .filter(notEmpty) + .collect(Collectors.toList()); + + // Checking if a JsonArray is empty, annotation was not performed on an image + List unlabeledImageList = labelPointData.stream() + .filter(isEmpty) + .collect(Collectors.toList()); + + totalImage.add(0, labeledImageList); + totalImage.add(1, unlabeledImageList); + + } + + public static Integer getNumberOfLabeledImage() + { + return totalImage.get(0).size(); + } + + public static Integer getNumberOfUnLabeledImage() + { + return totalImage.get(1).size(); + } + + // To extract the annotation data and version uuid of an Image + private static LinkedHashMap getAnnotationData(String annotationDict) + { + LinkedHashMap annotationDataMap = new LinkedHashMap<>(); + String s = StringUtils.removeStart(StringUtils.removeEnd(annotationDict, "]"), "["); + + JsonObject annotationDictJsonObject = new JsonObject(s); + String versionUuid = annotationDictJsonObject.getString(ParamConfig.getVersionUuidParam()); + JsonObject annotationData = annotationDictJsonObject.getJsonObject(ParamConfig.getAnnotationDataParam()); + + annotationDataMap.put(versionUuid, annotationData); + + return annotationDataMap; + + } + + private static JsonArray getAnnotationStatus(String annotationDictValue) + { + String s = StringUtils.removeStart(StringUtils.removeEnd(annotationDictValue, "]"), "["); + + // To get the data : annotation, img_x, img_y, img_w, img_h + JsonObject annotationDataJsonObject = new JsonObject(s); + + // To get annotation parameters : x1, y1, x2, y2, color, distToImg, label ,id + return annotationDataJsonObject.getJsonArray(ParamConfig.getAnnotationParam()); + + } + + public static JsonArray getLabelPerClassInProject(Map uuidAnnotationDict, String projectId) + { + Set imageUUID = uuidAnnotationDict.keySet(); + List annotationList = getAnnotationList(imageUUID, uuidAnnotationDict); + JsonArray labelPerClassInProjectJsonArray = new JsonArray(); + + // To get list of map whereby each map represent a label and its number of occurrence on an Image + List> labelByClassList = annotationList.stream() + .map(Annotation::getAnnotationDictDbFormat) + .map(LabelListHandler::getAnnotationData) + .map(Map::values) + .map(String::valueOf) + .map(LabelListHandler::getAnnotationStatus) + .map(LabelListHandler::getLabelByClass) + .collect(Collectors.toList()); + + // Collect all the labels that not used in annotation + List> unUsedLabelList = getUnUsedLabelList(projectId, labelByClassList); + + labelByClassList.addAll(unUsedLabelList); + + // To sum all occurrences of each label of respective class in the project + Map sumLabelByClass = labelByClassList.stream() + .flatMap(m -> m.entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, Integer::sum)); + + sumLabelByClass.entrySet().stream() + .map(m -> getJsonObject(m.getKey(), m.getValue())) + .forEach(labelPerClassInProjectJsonArray::add); + + return labelPerClassInProjectJsonArray; + + } + + private static JsonObject getJsonObject(String key, Integer value) + { + JsonObject jsonObject = new JsonObject(); + jsonObject.put(ParamConfig.getLabelParam(), key); + jsonObject.put(ParamConfig.getLabelCountParam(), value); + + return jsonObject; + } + + private static Map getLabelByClass(JsonArray labelPointData) + { + Map labelByClass = new HashMap<>(); + + // To get a list label on an Image + List labels = IntStream.range(0, labelPointData.size()) + .mapToObj(labelPointData::getJsonObject) + .map(m -> m.getString("label")) + .collect(Collectors.toList()); + + // To get each label with its occurrence into a map + Consumer action = s -> labelByClass.put(s, Collections.frequency(labels, s)); + + labels.forEach(action); + + return labelByClass; + + } + + private static List getAnnotationList(Set imageUUID, Map uuidAnnotationDict) + { + return imageUUID.stream().map(uuidAnnotationDict::get).collect(Collectors.toList()); + } + + private static List> getUnUsedLabelList (String projectId, List> labelByClassList) + { + ProjectLoader loader = Objects.requireNonNull(ProjectHandler.getProjectLoader(projectId)); + List originalLabelList = loader.getLabelList(); + Map unUsedLabels = new HashMap<>(); + List> unUsedLabelList = new ArrayList<>(); + + // To get a list of label that used in annotation + List usedLabel = labelByClassList.stream() + .flatMap(m -> m.entrySet().stream()) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + // To filter out the unused label from original label list + List filterList = originalLabelList.stream() + .filter(s -> !usedLabel.contains(s)) + .collect(Collectors.toList()); + + for(String label : filterList){ + unUsedLabels.put(label, 0); + unUsedLabelList.add(unUsedLabels); + } + + return unUsedLabelList; + } + + +}