Model Dermatology RESTful API
Requirements
- Python (version 2.x) ; It may work on python version 3.x, but it is not tested enough.
- Internet access
- Windows or Linux

Download
- api.zip

How to use

- If you have any trouble using this API, please email to whria78@gmail.com

1) Install Python 2.7
Download Anaconda2 32bit or 64bit 2.7 (https://www.continuum.io/downloads#windows) and install.
Set installation path to "c:\anaconda2" or "c:\python27"
Be sure to check "Add anaconda to my PATH environment variable" during installation

2) Download api.zip and extract api.zip

3) Resolve dependencies (numpy, matplotlib, requests, opencv)
c:\api\> pip install --upgrade pip
c:\api\> pip install numpy matplotlib requests opencv-python

4) Run Python scripts (api.py)
c:\api\> python api.py

# Source code - api.py

###
### Model Dermatology API
### Han Seung Seog (whria78@gmail.com, http://whria.net, http://medicalphoto.org)
### 2018-3-25
###
 
import requests
import pickle
import os
import sys
import time
import codecs


# for python2 python3 compatibility
try:
    import urllib.parse as myurl
except ImportError:
    import urllib as myurl
from io import BytesIO
    
# for unicode support
def custom_filename(x):return myurl.quote(x)
 
def modelnail(url,age,sex,image_path):
    url=url+'/api'
    
    f=BytesIO()
    pickle.dump([age,sex.encode('latin1')],f,0)
    
    data_={'user':codecs.encode(f.getvalue(),"base64").decode().replace('\n','')}
    file_={'file':(custom_filename(os.path.basename(image_path)),open(image_path,'rb').read())}
 
    res=requests.post(url,data=data_,files=file_)
    f=BytesIO(res.content)
 
    # for python2 python3 compatibility
    if sys.version_info[0] >= 3:
        result=pickle.load(f,encoding='bytes')
    else:
        result=pickle.load(f)
    return result
 
def get_deploy(url):
    url=url+'/deploy'
    try:
        res=requests.get(url)
    except:
        try:
            print("Connection Error")
            time.sleep(1)
            print("Retry #1")
            res=requests.get(url)
        except:
            print("Connection Error")
            sys.exit(0) 
    f=BytesIO(res.content)
    result=pickle.load(f)
    return result
 
### main server ### 
server_url='http://modelderm.com:8002'

 
### Get list of indexes ###
list_deploy=get_deploy(server_url)
 
### Run Model ###
 
test_filename='test.jpg'
if len(sys.argv)>1: 
    test_filename=str(sys.argv[1])
if (os.path.exists(test_filename)==False):
    print (test_filename+' is not exist')
    sys.exit(0)
    

result=modelnail(server_url,40,'M',test_filename)

success=result[0]
model_result=result[1]
 
if (success!=True):
    print (model_result)
    sys.exit(0)
 
for k,list_deploy_ in enumerate(list_deploy):
    for i,deploy_ in enumerate(list_deploy_):
        if round(model_result[k][i],4)>0:
            print ("%s : %.4f" % (deploy_,model_result[k][i]))


Example 1 - test.jpg

~$ python api.py

nonspecific : 0.0005
abscess : 0.0028
actinickeratosis : 0.0006
alopeciaareata : 0.0001
...
...
nail dystrophy 0.3106
...
...
onychomycosis : 0.2086
...
...
vasculitis : 0.0011
viralexanthem : 0.0001
wart 0.0061

Example 2 - Draw ROC curve, and get AUC results with custom dataset

The api.zip contains test python code, ISIC's 100 images, and 13 nevus images.

The 240 test images of SNU dataset is available at (https://figshare.com/articles/SNU_SNU_MELANOMA_and_Reddit_dataset_Quiz/6454973).
Edinburgh Dermofit Image Library is a commercial library (https://licensing.eri.ed.ac.uk/i/software/dermofit-image-library.html)

By default, the currect script (roc.py) performs 4-crop analysis.
If you want to perform 10-crop analysis, please change the line 38 of the roc.py to
        max_size_list=[672,600,560,500,448,400,336,300,224]

"run.bat" - Batch file for testing on MS Windows system
"roc.py" - Test python code
"/isic" - ISIC 100 melanoma, basal cell carcinoma, and squamous cell carcinoma images [cropped]
"/custom" - 13 nevus images ; remove all and replace with your custom images if you want to test with a custom dataset.

~$ python roc.py "test_path1"
--> test with the dataset (test_path1) / Test Engine - Model Dermatology / Multi-crop analysis

~$ python roc.py "test_path1;test_path2;..."
--> test with the dataset (test_path1 + test_path2 + ... ) / Test Engine - Model Dermatology / Multi-crop analysis

~$ python roc.py "test_path1" oldsingle
--> test with the dataset (test_path1) / Test Engile - #70617 / Single-crop analysis

~$ python roc.py "test_path1" oldmulti
--> test with the dataset (test_path1) / Test Engile - #70617 / Multi-crop analysis

~$ run.bat
--> test with the dataset (/isic + /custom)

# Source code - run.bat

@echo off
echo # Online ROC Curve Drawer
echo # The analysis will takes 10~30 minute depending on the network bandwidths. 
python roc.py "%cd%/isic;%cd%/custom"
pause
:END


# Source code - roc.py

###
### Model Dermatology API
### Han Seung Seog (whria78@gmail.com, http://whria.net, http://medicalphoto.org)
### 2018-3-25
###
 
import requests
import pickle
import os
import sys
import time
import codecs
from numpy import trapz
import matplotlib.pyplot as plt
import cv2
import numpy as np


### main server ### 
server_url='http://modelderm.com'
server_port1=':8000'
server_port2=':8002'
print("# Server : %s (%s , %s)" % (server_url,server_port1,server_port2))

# for python2 python3 compatibility
try:
    import urllib.parse as myurl
except ImportError:
    import urllib as myurl
from io import BytesIO
    
# for unicode support
def custom_filename(x):return myurl.quote(x)


def model_jid12dx(server_url,image_path):
    if multi_crop==1:
        max_size_list=[896,448,300,224]
    else:
        max_size_list=[224]

    img_org=cv2.imread(image_path,cv2.IMREAD_COLOR)
    height,width,channels=img_org.shape
    crop_imgs=[]
    prev_img=img_org
    result_list=[]
    for qq,max_size in enumerate(max_size_list):
        
        if (max_size!=224):
            forward_max=max_size_list[qq+1]
            if (height < forward_max):continue
            if (width < forward_max):continue
            if (height < max_size):max_size=height
            if (width < max_size):max_size=width
        
        gap_size=int((max_size-224)/2)
        img_org2 = cv2.resize(prev_img, (max_size, max_size), interpolation = cv2.INTER_CUBIC)
        prev_img=img_org2.copy()
        #[y:y+h,x:x+w]
        crop_img=img_org2[gap_size:gap_size+224,gap_size:gap_size+224]

        img_send = cv2.resize(crop_img, (224, 224), interpolation = cv2.INTER_CUBIC)

        result=model_jid12dx2(server_url,image_path,img_send)
        
        if (result[0]==False): 
            print "Fail"
            return result
        result_list+=[result[1][0]]

    for qq,result_ in enumerate(result_list):
        if (qq==0):
            pred_list_temp=np.asarray(result_).copy()
        else:
            pred_list_temp+=np.asarray(result_)
            
    pred_list_temp=pred_list_temp/len(result_list)
    return [True,[pred_list_temp.tolist()]]
 
def model_jid12dx2(server_url,image_path,image_numpy):
    url=server_url+server_port1
    url=url+'/api'
    
    data_={'user':'HAN'}
    file_={'file':(custom_filename(os.path.basename(image_path)),cv2.imencode('.jpg',image_numpy)[1].tostring())}
 
    res=requests.post(url,data=data_,files=file_)
    f=BytesIO(res.content)
 
    # for python2 python3 compatibility
    if sys.version_info[0] >= 3:
        result=pickle.load(f,encoding='bytes')
    else:
        result=pickle.load(f)
    return result
 
def get_deploy_jid12dx(server_url):
    url=server_url+server_port1
    return get_deploy(url)

def modelderm(server_url,age,sex,image_path):
    url=server_url+server_port2
    url=url+'/api'
    
    f=BytesIO()
    pickle.dump([age,sex.encode('latin1')],f,0)
    
    data_={'user':codecs.encode(f.getvalue(),"base64").decode().replace('\n','')}
    file_={'file':(custom_filename(os.path.basename(image_path)),open(image_path,'rb').read())}
 
    res=requests.post(url,data=data_,files=file_)
    f=BytesIO(res.content)
 
    # for python2 python3 compatibility
    if sys.version_info[0] >= 3:
        result=pickle.load(f,encoding='bytes')
    else:
        result=pickle.load(f)
    return result

def get_deploy_modelderm(server_url):
    url=server_url+server_port2
    return get_deploy(url)
 
def get_deploy(url):
    url=url+'/deploy'
    try:
        res=requests.get(url)
    except:
        try:
            print("Connection Error")
            time.sleep(1)
            print("Retry #1")
            res=requests.get(url)
        except:
            print("Connection Error")
            sys.exit(0) 
    f=BytesIO(res.content)
    result=pickle.load(f)
    return result
 

def get_basenames(img_path):
    basenames=[]
    dirname=os.path.dirname(img_path)
    for alias_ in list_alias:
        dirname=dirname.replace(alias_[0],alias_[1])
    olddir=''
    while dirname != '' and dirname != '/' and olddir!=dirname:
        basenames+=[os.path.basename(dirname)]
        olddir=dirname
        dirname=os.path.dirname(dirname)
    return basenames

def get_rightdx(img_path):
    right_dx_index=-1
    for i,dx_ in enumerate(list_dx):
        if dx_ in get_basenames(img_path):
            right_dx_index=i
            break
    if right_dx_index==-1:
        print ("Fail to find diagnosis : ",img_path)
    return right_dx_index
    
def loadonlinecaffemodel(test_img_paths):
    return_result=[]
    for i,img_path in enumerate(test_img_paths):
        print('(%d/%d) %s' % (i,len(test_img_paths),img_path))

        if test_engine==1:
            default_age=40
            default_sex='M'
            result=modelderm(server_url,default_age,default_sex,img_path)
            success=result[0]
            model_result=result[1][0] # model #1 - [1][0] , model #2 (if exists) - [1][1] ....
        else:
            result=model_jid12dx(server_url,img_path)
            success=result[0]
            model_result=result[1][0] # model #1 - [1][0] , model #2 (if exists) - [1][1] ....
         
        if (success!=True):
            print (model_result)
            sys.exit(0)
            
        return_result+=[(img_path,model_result)]
    print ("\n")
    return return_result


def draw_roc(targetdx,modelresult,use_ratio):
    print ("# "+list_dx[targetdx])
    if len(use_ratio)==2:
        print ("# Use Ratio (%s / %s)" % (list_dx[use_ratio[0]],list_dx[use_ratio[1]]))
    print (" ")

    preds=[]
    for i,img_path in enumerate(test_img_paths):
        model_result=[]
        temp=[]

        for modelresult_ in modelresult:
            if (modelresult_[0]==img_path):
                model_result=modelresult_[1]

        # get right index from folder name
        right_dx_index=get_rightdx(img_path)
        #print "Diagnosis identified by the name of folder: ",list_dx[right_dx_index]

        if len(use_ratio)==2:
            if right_dx_index==use_ratio[0] or right_dx_index==use_ratio[1]:
                preds += [(model_result[use_ratio[0]]/(model_result[use_ratio[0]]+model_result[use_ratio[1]]),right_dx_index)]
        else:
            preds += [(model_result[targetdx],right_dx_index)]


    #
    # Draw ROC
    #

    list_result=[]
    list_result2=[]
    plt_x=[]
    plt_y=[]
    jscores=[]
    max_j=0.0  # Youden index (J score) = sen + spe - 1
    max_senspe=[0.0,0.0,0.0]

    limitarray=[]

    dlimit_array=[0.0]
    for preds_ in sorted(preds):
        dlimit_array+=[preds_[0]]
    dlimit_array+=[1.0]

    for dlimit in dlimit_array:
        correct=0
        correctsen=0.0
        correctspe=0.0
        sen_all=0.0
        spe_all=0.0
        falsepositive=0.0
        truepositive=0.0
        
        for pred_ in preds:
            
            if (pred_[1]!=targetdx and pred_[0]>=dlimit):
                falsepositive+=1
           
            if (pred_[1]==targetdx):sen_all+=1
            if (pred_[1]!=targetdx):spe_all+=1
            if (pred_[0]>=dlimit and pred_[1]==targetdx):
                correct+=1
                correctsen+=1
                truepositive+=1
               
            if (pred_[0] < dlimit and pred_[1]!=targetdx):
                correct+=1
                correctspe+=1
                
        list_result+=[(dlimit,round(correct*100/len(preds),2))]
        jscore=round(correctsen*100/sen_all+correctspe*100/spe_all-100,2)
        if (max_j < jscore):max_j=jscore    
        if ((max_senspe[0]+max_senspe[1]) < (round(correctsen*100/sen_all,2)+round(correctspe*100/spe_all,2))):
            max_senspe=[round(correctsen*100/sen_all,2),round(correctspe*100/spe_all,2),dlimit]
        plt_x+=[round(correctsen*100/sen_all,2)]
        plt_y+=[round(correctspe*100/spe_all,2)]
        jscores+=[jscore]
        limitarray+=[float(dlimit)]
        list_result2+=[(round(correctsen*100/sen_all,2),round(correctspe*100/spe_all,2))]

    # caculate AUC
    auc=trapz(plt_y[::-1],plt_x[::-1])/10000
    
    print ("AUC : %.4f" % auc)
    print ("MAX J score : %.2f" % max_j)
    print ("MAX Sensitivity : %.2f" % max_senspe[0])         
    print ("MAX Specificity : %.2f" % max_senspe[1])
    print ("Optimal Threshold : %.4f" % (float(max_senspe[2])))
    print ("\n")

    # for ROC curve ; 1-specificity
    new_plt_y=[]
    for i in range(0,len(plt_y)):
        new_plt_y+=[100-plt_y[i]]

    ###
    # draw ROC plot
    fig,ax1=plt.subplots(figsize=(8,8))
    plot0,=ax1.plot(new_plt_y,plt_x,color="black",linewidth=1.0,linestyle="-")
    ax1.margins(0)
    for label in (ax1.get_xticklabels()+ax1.get_yticklabels()):
        label.set_fontsize(20)
    
    # Red dot at the point that maximize J score
    # max_senspe[0] = sensitivity
    # max_senspe[1] = specificity
    # max_senspe[2] = threshold

    r1=[(100-max_senspe[1],max_senspe[0])]
    ax1.plot1,=plt.plot(*zip(*r1),color="red",marker="o",linestyle='')
    ax1.set_ylabel('sensitivity',fontsize=25)
    ax1.set_xlabel('1-specificity',fontsize=25)
    plt.text(40,10,"AUC : %.4f %%" % (auc),size=20,ha="center",va="center",color="black")
    plt.text(40,20,"Optimal Specificity : %.2f" % (max_senspe[1]),size=20,ha="center",va="center",color="black")
    plt.text(40,30,"Optimal Sensitivity : %.2f" % (max_senspe[0]),size=20,ha="center",va="center",color="black")
    plt.title(list_dx[targetdx],fontsize=30)
    plt.grid(True)
    
    if len(use_ratio)==2:
        plt.savefig("ROC "+list_dx[targetdx]+" USE RATIO.png",bbox_inches='tight')
    else:
        plt.savefig("ROC "+list_dx[targetdx]+".png",bbox_inches='tight')
    #plt.show()

    ###
    # draw threshold - sensitivity / specificity plot
    ###

    fig,ax1=plt.subplots(figsize=(8,8))
    ax1.margins(0)
    ax2=ax1.twinx()
    ax2.margins(0)
    for label in (ax1.get_xticklabels()+ax1.get_yticklabels()+ax2.get_yticklabels()):
        label.set_fontsize(20)

    #plot1,=ax1.plot(limitarray,jscores,color="black",linewidth=1.5,linestyle="-")
    #params={'legend.fontsize':18,'legend.numpoints':1}
    #plt.rcParams.update(params)
    #plt.legend([plot1], ['Youden Index ( J score )'],  bbox_to_anchor=(0.87, 0.15))

    ax1.plot(limitarray,plt_y,color="r",linewidth=1.0,linestyle="-")
    ax2.plot(limitarray,plt_x,color="b",linewidth=1.0,linestyle="-")
    ax1.set_xlabel('threshold',fontsize=25)
    ax1.set_ylabel('specificity',color='r',fontsize=25)
    ax2.set_ylabel('sensitivity',color='b',fontsize=25)
    plt.title(list_dx[targetdx],fontsize=30)
    plt.grid(True)
    if len(use_ratio)==2:
        plt.savefig("Threshold"+list_dx[targetdx]+" USE RATIO.png",bbox_inches='tight')
    else:
        plt.savefig("Threshold"+list_dx[targetdx]+".png",bbox_inches='tight')
    #plt.show()



### TEST Engine ###
# 0 : 70617 model, 1 : New Online Model Dermatology

test_engine=1    # default = Model Dermatology / OLDSingle - single crop 70617 , OLDMulti - multi crop 70617
multi_crop=0
if (len(sys.argv)>2): 
    temp=str(sys.argv[2])
else:
    temp=''
if 'old' in temp.lower(): test_engine=0
if 'multi' in temp.lower(): multi_crop=1

if test_engine==1:
    print("# Test Engine - Model Dermatology")
else:
    print("# Test Engine - #70617")
    if multi_crop==1:
        print("# Multi-crop Analysis")
    else:
        print("# Single-crop Analysis")

### Get list of indexes ###
if test_engine==1:
    list_dx=get_deploy_modelderm(server_url)[0]
else:
    list_dx=get_deploy_jid12dx(server_url)[0]

list_alias=[('intraepithelialcarcinoma','bowendisease')]


### Get Test paths ###
#test_path   path1;path2;path3

if (len(sys.argv)>1): test_path_list=str(sys.argv[1]).split(';')

for test_path in test_path_list:
    if (os.path.exists(test_path)==False):
        print (test_path+' is not exist')
        sys.exit(0)
    print ("Test Path : ",test_path)


### Get list of indexes ###
print ("DX List : ",list_dx)
    

### Read list of diagnoses and image paths of test folder ###
validation_info=[]
for i in range(0,len(list_dx)):
    validation_info+=[0]
    
test_img_paths=[]
for test_path in test_path_list:
    for root,dirs,files in os.walk(test_path):
        for fname in files:
            ext=(os.path.splitext(fname)[-1]).lower()
            if ext == ".jpg" or ext == ".jpeg" or ext == ".gif" or ext == ".png" : 
                test_img_paths+=[os.path.join(root,fname)]
                
                right_dx_index=get_rightdx(os.path.join(root,fname))
                if (right_dx_index==-1):
                    print ("No matched diagnosis from the name of folder : "+os.path.join(root,fname))
                    sys.exit(0)
                else:
                    validation_info[right_dx_index]+=1

    if (len(test_img_paths)==0):
        print ("No image (.jpg .gif .png) exist at "+test_path)
        sys.exit(0) 

### run model
modelresult=loadonlinecaffemodel(test_img_paths)   

### call drawROC function
nevus_dx=-1
for i,dx_ in enumerate(list_dx):
    if (dx_=='melanocyticnevus'):
        nevus_dx=i
if (nevus_dx==-1):
    print("Fail to find the cases of melanocyticnevus : ",test_path_list)
    
for i,info_ in enumerate(validation_info):
    if info_>0:
        draw_roc(i,modelresult,[])
        if list_dx[i]=='malignantmelanoma' and nevus_dx!=-1:
            if validation_info[nevus_dx]>0:
                draw_roc(i,modelresult,[i,nevus_dx])