Skip to content

Commit

Permalink
#9 just knocked out the vast majority of target binning and submissio…
Browse files Browse the repository at this point in the history
…n stuff. only major piece left is interop submission
  • Loading branch information
len0rd committed Jan 8, 2019
1 parent 8b23060 commit 564ea69
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 16 deletions.
5 changes: 3 additions & 2 deletions server/src/apis/autonomous_image_classification_handler.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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', {
Expand Down Expand Up @@ -91,7 +93,7 @@ def post(self):
return response


@api.route('/<int:class_id>')
@api.route('/<int:class_id>/info')
@api.doc(params={'class_id': 'Classification ID of the classification entry to update or get info on'}, required=True)
class AutonomousSpecificClassificationHandler(Resource):

Expand All @@ -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'})
Expand Down
2 changes: 2 additions & 0 deletions server/src/apis/crop_image_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
123 changes: 121 additions & 2 deletions server/src/apis/image_submit_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()
Expand All @@ -32,4 +36,119 @@ def get(self):
for target in results:
jsonible.append([ classification.toDict(exclude=('id',)) for classification in target ])

return jsonify(jsonible)
return jsonify(jsonible)

@api.route('/<int:target_id>')
@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
22 changes: 13 additions & 9 deletions server/src/apis/manual_image_classification_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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())

Expand All @@ -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
Expand Down
20 changes: 19 additions & 1 deletion server/src/dao/classification_dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
26 changes: 24 additions & 2 deletions server/src/dao/outgoing_autonomous_dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
6 changes: 6 additions & 0 deletions server/src/dao/outgoing_manual_dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 564ea69

Please sign in to comment.