diff --git a/server/src/apis/autonomous_image_classification_handler.py b/server/src/apis/autonomous_image_classification_handler.py index d3a9089..d421e65 100644 --- a/server/src/apis/autonomous_image_classification_handler.py +++ b/server/src/apis/autonomous_image_classification_handler.py @@ -1,5 +1,6 @@ import os, time from werkzeug.utils import secure_filename +from werkzeug.datastructures import FileStorage from config import defaultConfigPath, defaultCroppedImgPath, allowedFileType from flask import request, jsonify, abort, make_response from flask_restplus import Namespace, Resource, fields @@ -11,6 +12,7 @@ imageIDParser = api.parser() imageIDParser.add_argument('X-Image-Id', location='headers', type=int, required=True, help='The original raw image_id this classification comes from') +imageIDParser.add_argument('cropped_image', type=FileStorage, location='files', required=True, help='The cropped image file') # for documentation purposes. Defines the response for some of the methods below classificationModel = api.model('Autonomous Classification', { @@ -91,7 +93,7 @@ def post(self): return response -@api.route('/') +@api.route('//info') @api.doc(params={'class_id': 'Classification ID of the classification entry to update or get info on'}, required=True) class AutonomousSpecificClassificationHandler(Resource): @@ -108,7 +110,6 @@ def get(self, class_id): return jsonify(result.toDict()) - @api.doc(description='Update information for the specified classification entry') @api.response(200, 'OK', classificationModel) @api.doc(responses={400:'X-Manual header not specified', 404:'Could not find classification with given ID'}) diff --git a/server/src/apis/crop_image_handler.py b/server/src/apis/crop_image_handler.py index 3ddcf9c..add63cf 100644 --- a/server/src/apis/crop_image_handler.py +++ b/server/src/apis/crop_image_handler.py @@ -5,12 +5,14 @@ from dao.model.point import point from config import defaultConfigPath, defaultCroppedImgPath, allowedFileType from werkzeug.utils import secure_filename +from werkzeug.datastructures import FileStorage import os, time api = Namespace('image/crop', description="All cropped image calls route through here") imageIDParser = api.parser() imageIDParser.add_argument('X-Image-Id', location='headers', type=int, required=True, help='Specify the associated image id for this image.') +imageIDParser.add_argument('cropped_image', type=FileStorage, location='files', required=True, help='The cropped image file') croppedImageModel = api.model('Crop Image Info', { 'id': fields.Integer(description='Auto-generated id for the cropped image', example=1234), diff --git a/server/src/apis/image_submit_handler.py b/server/src/apis/image_submit_handler.py index 38ee1cd..b181976 100644 --- a/server/src/apis/image_submit_handler.py +++ b/server/src/apis/image_submit_handler.py @@ -4,6 +4,10 @@ from dao.outgoing_autonomous_dao import OutgoingAutonomousDAO from dao.model.outgoing_manual import outgoing_manual from dao.model.outgoing_autonomous import outgoing_autonomous +from dao.manual_cropped_dao import ManualCroppedDAO +from dao.model.manual_cropped import manual_cropped +from dao.auvsi_odlc_file_dao import AuvsiOdlcDao +from dao.model.outgoing_target import outgoing_target from config import defaultConfigPath from apis.helper_methods import checkXManual, getClassificationDAO @@ -18,9 +22,9 @@ class PendingSubmissionHandler(Resource): @api.doc(description='See all classifications pending submission. Grouped by targets') @api.doc(responses={200:'OK', 400:'X-Manual header not specified', 404:'Could not find any targets pending submission'}) + @api.expect(classificationParser) def get(self): manual = checkXManual(request) - print(manual) dao = getClassificationDAO(manual) results = dao.getPendingTargets() @@ -32,4 +36,119 @@ def get(self): for target in results: jsonible.append([ classification.toDict(exclude=('id',)) for classification in target ]) - return jsonify(jsonible) \ No newline at end of file + return jsonify(jsonible) + +@api.route('/') +@api.doc(params={'target_id': 'Target id of the target group to get info on/submit'}) +class SubmissionHandler(Resource): + + @api.doc(description='Get the submitted target for the given target id') + @api.expect(classificationParser) + @api.doc(responses={200:'OK', 400:'X-Manual header not specified', 404:'Could not find a SUBMITTED target with the given id (note targets will only be successfully retrieved if they have already been submitted by a POST)'}) + def get(self, target_id): + manual = checkXManual(request) + dao = getClassificationDAO(manual) + + resultTarget = dao.getSubmittedTarget(target_id) + if resultTarget is None: + return {'message': f'Failed to retrieve target {target_id}'}, 404 + + return jsonify(resultTarget.toDict()) + + @api.doc(description="""Submit the specified target. Returns that target information that was submitted + after averaging all the classification values for the target. The structure of the returned json depends + on the type of target submitted""") + @api.expect(classificationParser) + @api.doc(responses={200:'OK', 400:'X-Manual header not specified', 404: 'Failed to find any targets with the given id waiting to be submitted'}) + def post(self, target_id): + manual = checkXManual(request) + dao = getClassificationDAO(manual) + + allTargetIds = dao.listTargetIds() + + if allTargetIds is None or not allTargetIds: + return {'message': 'No classifications in outgoing (table empty)!'}, 404 + elif target_id not in allTargetIds: + return {'message': 'Failed to find any targets with id {} to submit'.format(target_id)}, 404 + + resultTarget = None + try: + resultTarget = dao.submitPendingTarget(target_id) + except Exception as e: + # something failed, make sure the target classifications are reset to 'unsubmitted' + dao.resetTargetSubmissionStatus(target_id) + raise # rethrow the same exception + + if resultTarget is None: + return {'message': 'Something went wrong while trying to submit target {}'.format(target_id)}, 500 + + prettyTargetOut = None + try: + prettyTargetOut = writeTargetToODLCFile(resultTarget, manual) + except Exception as e: + # something failed, make sure the target classifications are reset to 'unsubmitted' + dao.resetTargetSubmissionStatus(target_id) + raise # rethrow the same exception + + if prettyTargetOut is None and manual: + dao.resetTargetSubmissionStatus(target_id) + return {'message': 'Unable to find cropped_image entry with id: {}'.format(resultTarget.crop_id)}, 404 + + # TODO: send to interop client?? + return jsonify(prettyTargetOut.toJson()) + + +@api.route('/all') +class AllSubmissionHandler(Resource): + + @api.doc(description="""Submit all targets that do not yet have a 'submitted' status. Returns a list of targets + successfully submitted. As with single target submission, the structure of this json depends on the type of + target submitted""") + @api.expect(classificationParser) + @api.doc(responses={200:'OK', 400:'X-Manual header not specified', 404: 'Failed to find any targets waiting to be submitted'}) + def post(self): + manual = checkXManual(request) + dao = getClassificationDAO(manual) + + # cant really do any fancy checks like above, just go for submission: + resultingTargets = dao.submitAllPendingTargets() + + if resultingTargets is None: + return {'message': 'Either something went wrong, or there are not pending targets to submit'}, 404 + + finalRet = [] + for result in resultingTargets: + try: + prettyTargetOut = writeTargetToODLCFile(result, manual) + if prettyTargetOut is None and manual: + dao.resetTargetSubmissionStatus(result.target) + print("WARN: Unable to find cropped_image entry with id {}".format(result.crop_id)) + if prettyTargetOut is not None: + finalRet.append(prettyTargetOut.toJson()) + # TODO: send to interop client?? + except Exception as e: + # something failed, make sure the target classifications are reset to 'unsubmitted' + dao.resetTargetSubmissionStatus(result.target) + raise # rethrow the same exception + + return jsonify(finalRet) + + +def writeTargetToODLCFile(target, manual): + imagePath = None + if manual: + # then we need to get the cropped path + croppedDao = ManualCroppedDAO(defaultConfigPath()) + croppedInfo = croppedDao.getImage(target.crop_id) + + if croppedInfo is None: + return None + imagePath = croppedInfo.cropped_path + else: + # autonomous we can just go + imagePath = target.crop_path + + prettyTargetOut = outgoing_target(target, manual) + auvsiDao = AuvsiOdlcDao() + auvsiDao.addTarget(prettyTargetOut, imagePath) + return prettyTargetOut \ No newline at end of file diff --git a/server/src/apis/manual_image_classification_handler.py b/server/src/apis/manual_image_classification_handler.py index 92ccb4d..9deaef7 100644 --- a/server/src/apis/manual_image_classification_handler.py +++ b/server/src/apis/manual_image_classification_handler.py @@ -6,10 +6,13 @@ api = Namespace('image/class/manual', description='Image classification calls for manual clients route through here') +cropIDParser = api.parser() +cropIDParser.add_argument('X-Crop-Id', location='headers', type=int, required=True, help='The cropped_id this classification is associated with') + # for documentation purposes. Defines the response for some of the methods below classificationModel = api.model('Manual Classification', { 'target': fields.Integer(required=False, description='which target number this is. This field is automatically managed by the server. It groups classifications together based on alphanumeric, shape and type', example=1), - 'image_id': fields.Integer(reqired=True, description='Id of the cropped image this classification originally comes from', example=123), + 'crop_id': fields.Integer(reqired=True, description='Id of the cropped image this classification originally comes from', example=123), 'type': fields.String(required=False, description='Classification type(standard, off_axis, or emergent)', example="standard"), 'latitude': fields.Float(required=False, description='Latitude coordinate of object', example=40.246354), 'longitude': fields.Float(required=False, description='longitude coordinate of object', example=-111.647553), @@ -44,13 +47,16 @@ def get(self): class ClassifiedImageHandler(Resource): @api.doc(description='Automatically add a new classifcation to the server') @api.doc(responses={200:'OK', 400:'Improper image post', 500: 'Something failed server-side'}) - @api.header('X-Class-Id', 'Crop ID of the image if successfully inserted. This WILL be different from the Image-ID provided in the request') + # @api.doc(body=classificationModel) + # @api.expect(cropIDParser) + @api.expect(classificationModel) + @api.header('X-Class-Id', 'Classification ID of the image if successfully inserted. This WILL be different from the Crop-ID provided in the request') def post(self): prevId = -1 - if 'X-Prev-Id' in request.headers: - prevId = request.headers.get('X-Prev-Id') + if 'X-Crop-Id' in request.headers: + prevId = request.headers.get('X-Crop-Id') else: - abort(400, "Need to specify header 'X-Prev-Id'!") + abort(400, "Need to specify header 'X-Crop-Id'!") dao = OutgoingManualDAO(defaultConfigPath()) @@ -76,21 +82,19 @@ class SpecificClassificationHandler(Resource): def get(self, class_id): dao = OutgoingManualDAO(defaultConfigPath()) - result = dao.getClassification(class_id) if result is None: return {'message': 'Failed to locate classification with id {}'.format(class_id)}, 404 - return jsonify(result.toDict()) @api.doc(description='Update information for the specified classification entry') @api.response(200, 'OK', classificationModel) - @api.doc(responses={400:'X-Manual header not specified', 404:'Could not find classification with given ID'}) + @api.doc(body=classificationModel) + @api.doc(responses={404:'Could not find classification with given ID'}) def put(self, class_id): dao = OutgoingManualDAO(defaultConfigPath()) - result = dao.updateClassification(class_id, request.get_json()) if result is None: return {'message': 'No image with id {} found with a classification to update or your input was invalid (or was there a server error?)'.format(class_id)}, 404 diff --git a/server/src/dao/classification_dao.py b/server/src/dao/classification_dao.py index 10abc1b..a8dcc86 100644 --- a/server/src/dao/classification_dao.py +++ b/server/src/dao/classification_dao.py @@ -338,7 +338,7 @@ def submitAllPendingTargets(self, modelGenerator): allSubmitted.append(resultModel) if not allSubmitted: - print(f'Failed to submit all the targets in this list: {unsubmittedTargetIds}') + print('Failed to submit all the targets in this list: {}'.format(unsubmittedTargetIds)) return None return allSubmitted @@ -429,6 +429,24 @@ def getSubmittedClassification(self, modelGenerator, target): return finalModel + def resetTargetSubmissionStatus(self, target): + """ + Resets all classifications for the given target to an 'unsubmitted' state. + This is useful if something at a higher level fails and the target in fact failed + to submit + + @type target: int + @param target: the target_id to reset + """ + + resetTargetSql = "UPDATE " + self.outgoingTableName + """ + SET submitted = 'unsubmitted' + WHERE target = %s;""" + + cur = self.conn.cursor() + cur.execute(resetTargetSql, (target,)) + cur.close() + def calcClmnAvg(self, classifications, clmnNum): """ Calculate the average of the specified column diff --git a/server/src/dao/outgoing_autonomous_dao.py b/server/src/dao/outgoing_autonomous_dao.py index 42c3bfa..ed781de 100644 --- a/server/src/dao/outgoing_autonomous_dao.py +++ b/server/src/dao/outgoing_autonomous_dao.py @@ -75,11 +75,33 @@ def updateClassificationByUID(self, id, updateClass): def getAllDistinct(self): return super(OutgoingAutonomousDAO, self).getAllDistinct(self) - def getAllDistinctPending(self): + def getPendingTargets(self): """ + See classification_dao docs Get images grouped by distinct targets pending submission (ei: submitted = false) """ - return super(OutgoingAutonomousDAO, self).getAllDistinct(self, whereClause="WHERE submitted=FALSE ") + return super(OutgoingAutonomousDAO, self).getAllTargets(self, whereClause=" submitted = 'unsubmitted' ") + + def getSubmittedTarget(self, target): + return super(OutgoingAutonomousDAO, self).getSubmittedClassification(self, target) + + def submitAllPendingTargets(self): + """ + See classification_dao docs + """ + return super(OutgoingAutonomousDAO, self).submitAllPendingTargets(self) + + def submitPendingTarget(self, target): + """ + See classification_dao docs + Submit the specified pending target to the judges. + + @return: an outgoing_manual object that can be used to submit the final classification + """ + return super(OutgoingAutonomousDAO, self).submitPendingTargetClass(self, target) + + def listTargetIds(self): + return super(OutgoingAutonomousDAO, self).getAllTargetIDs() def newModelFromRow(self, row): """ diff --git a/server/src/dao/outgoing_manual_dao.py b/server/src/dao/outgoing_manual_dao.py index 32748bd..0822f9f 100644 --- a/server/src/dao/outgoing_manual_dao.py +++ b/server/src/dao/outgoing_manual_dao.py @@ -82,6 +82,9 @@ def getPendingTargets(self): """ return super(OutgoingManualDAO, self).getAllTargets(self, whereClause=" submitted = 'unsubmitted' ") + def getSubmittedTarget(self, target): + return super(OutgoingManualDAO, self).getSubmittedClassification(self, target) + def submitAllPendingTargets(self): """ See classification_dao docs @@ -97,6 +100,9 @@ def submitPendingTarget(self, target): """ return super(OutgoingManualDAO, self).submitPendingTargetClass(self, target) + def listTargetIds(self): + return super(OutgoingManualDAO, self).getAllTargetIDs() + def newModelFromRow(self, row): """ Kinda a reflective function for the classification dao. Pass self up