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

✨ Project statistic #500

Merged
merged 18 commits into from
Sep 8, 2021
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
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.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import io.vertx.core.eventbus.Message;
import io.vertx.core.json.JsonObject;
import io.vertx.jdbcclient.JDBCPool;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.RowSet;
import lombok.extern.slf4j.Slf4j;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.");
Expand Down Expand Up @@ -655,6 +660,36 @@ public void reloadProject(Message<JsonObject> message)
ImageHandler.loadProjectRootPath(loader);
}

public void getProjectStatistic(Message<JsonObject> 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<JsonObject> 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<Void> promise)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ public class ProjectLoader

@Builder.Default private List<String> unsupportedImageList = new ArrayList<>();

// list of label and unlabelled image
@Builder.Default private List<String> labelledImageList = new ArrayList<>();
@Builder.Default private List<String> unLabelledImageList = new ArrayList<>();

public String getCurrentVersionUuid()
{
return projectVersion.getCurrentVersion().getVersionUuid();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ public void start(Promise<Void> 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);
Expand Down
51 changes: 51 additions & 0 deletions classifai-core/src/main/java/ai/classifai/router/V2Endpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
});

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
* 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<List<JsonArray>> totalImage = new ArrayList<>();

// Handling the number of labeled and unlabeled image
public static void getImageLabeledStatus(Map<String, Annotation> uuidAnnotationDict)
{
Set<String> imageUUID = uuidAnnotationDict.keySet(); // key for each project
List<Annotation> annotationList = getAnnotationList(imageUUID, uuidAnnotationDict);// a list of annotation

List<JsonArray> labelPointData = annotationList.stream()
.map(Annotation::getAnnotationDictDbFormat)
.map(LabelListHandler::getAnnotationData)
.map(Map::values)
.map(String::valueOf)
.map(LabelListHandler::getAnnotationStatus)
.collect(Collectors.toList());

Predicate<JsonArray> isEmpty = JsonArray::isEmpty;
Predicate<JsonArray> notEmpty = isEmpty.negate();

List<JsonArray> labeledImageList = labelPointData.stream()
.filter(notEmpty)
.collect(Collectors.toList());

List<JsonArray> 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();
}

private static LinkedHashMap<String,JsonObject> getAnnotationData(String annotationDict)
{
LinkedHashMap<String, JsonObject> 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, "]"), "[");
JsonObject annotationDataJsonObject = new JsonObject(s); //annotation, img_x, img_y, img_w, img_h

// To get annotation parameters
return annotationDataJsonObject.getJsonArray(ParamConfig.getAnnotationParam());// x1, y1, x2, y2, color, distToImg, label ,id

}

public static JsonArray getLabelPerClassInProject(Map<String, Annotation> uuidAnnotationDict, String projectId)
{
Set<String> imageUUID = uuidAnnotationDict.keySet();
List<Annotation> annotationList = getAnnotationList(imageUUID, uuidAnnotationDict);
JsonArray labelPerClassInProjectJsonArray = new JsonArray();

List<Map<String, Integer>> labelByClassList = annotationList.stream()
.map(Annotation::getAnnotationDictDbFormat)
.map(LabelListHandler::getAnnotationData)
.map(Map::values)
.map(String::valueOf)
.map(LabelListHandler::getAnnotationStatus)
.map(LabelListHandler::getLabelByClass)
.collect(Collectors.toList());

List<Map<String, Integer>> unUsedLabelList = getUnUsedLabelList(projectId, labelByClassList);

labelByClassList.addAll(unUsedLabelList);

Map<String,Integer> 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<String,Integer> getLabelByClass(JsonArray labelPointData)
{
Map<String,Integer> labelByClass = new HashMap<>();

List<String> labels = IntStream.range(0, labelPointData.size())
.mapToObj(labelPointData::getJsonObject)
.map(m -> m.getString("label"))
.collect(Collectors.toList());

Consumer<String> action = s -> labelByClass.put(s, Collections.frequency(labels, s));

labels.forEach(action);

return labelByClass;

}

private static List<Annotation> getAnnotationList(Set<String> imageUUID, Map<String, Annotation> uuidAnnotationDict)
{
return imageUUID.stream().map(uuidAnnotationDict::get).collect(Collectors.toList());
}

private static List<Map<String, Integer>> getUnUsedLabelList (String projectId, List<Map<String, Integer>> labelByClassList)
{
ProjectLoader loader = Objects.requireNonNull(ProjectHandler.getProjectLoader(projectId));
List<String> oriLabelList = loader.getLabelList();
Map<String, Integer> unUsedLabels = new HashMap<>();
List<Map<String, Integer>> unUsedLabelList = new ArrayList<>();

List<String> usedLabel = labelByClassList.stream()
.flatMap(m -> m.entrySet().stream())
.map(Map.Entry::getKey)
.collect(Collectors.toList());

List<String> filterList = oriLabelList.stream()
.filter(s -> !usedLabel.contains(s))
.collect(Collectors.toList());

for(String label : filterList){
unUsedLabels.put(label, 0);
unUsedLabelList.add(unUsedLabels);
}

return unUsedLabelList;
}


}