Tensorflow的圖像操作

 圖像數據解析

圖像編碼

import tensorflow as tf
import base64

if __name__ == "__main__":

    path = "/Users/admin/Documents/111.jpeg"
    img = tf.io.read_file(path)
    print("圖像字節編碼:{}".format(img))

    img1 = tf.io.gfile.GFile(path, 'rb')
    print("圖像字節編碼:{}".format(img1.readlines()))

    img_64 = tf.io.encode_base64(img)
    img_64 = format(img_64)[2:-1]
    print("網絡安全Base_64編碼:{}".format(img_64))

    img_b64 = list(img_64)
    img_b64_list = []
    for i in range(len(img_b64)):
        if img_b64[i] == "-":
            img_b64_list.append("+")
        elif img_b64[i] == "_":
            img_b64_list.append("/")
        else:
            img_b64_list.append(img_b64[i])
    if len(img_b64_list) % 4 != 0:
        remainder = len(img_b64_list) % 4
        for i in range(remainder):
            img_b64_list.append("=")
    img_b64 = "".join(img_b64_list)
    print("標準Base_64編碼:{}".format(img_b64))

    with open(path, "rb") as f:
        img3 = f.read()
        img_stand_b64 = base64.b64encode(img3)
        img_stand_b64_str = img_stand_b64.decode("utf8")
    print("標準Base_64編碼:{}".format(img_stand_b64_str))

    img_w64 = list(img_stand_b64_str)
    img_w64_list = []
    for i in range(len(img_w64)):
        if img_w64[i] == "+":
            img_w64_list.append("-")
        elif img_w64[i] == "/":
            img_w64_list.append("_")
        elif img_w64[i] == "=":
            break
        else:
            img_w64_list.append(img_w64[i])
    img_w64 = "".join(img_w64_list)
    print("網絡安全Base_64編碼:{}".format(img_w64))

運行結果(部分截取)

圖像字節編碼:b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xfe\x00*Intel(R) JPEG Library, version 1,5,4,36\x00\xff\xdb\x00C\x00\x05\x03\x04\x04\x04\x03\x05\x04\x04\x04\x05\x05\x05\x06\x07\x0c\x08\x07\x07\x07\x07\x0f\x0b\x0b\t\x0c\x11\x0f\x12\x12\x11\x0f\x11\x11\x13\x16\x1c\x17\x13\x14\x1a\x15\x11\x11\x18!\x18\x1a\x1d\x1d\x1f\x1f\x1f\x13\x17"$"\x1e$\x1c\x1e\x1f\x1e\xff\xdb\x00C\x01\x05\x05\x05\x07\x06\x07\x0e\x08\x08\x0e\x1e\x14\x11\x14\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\xff\xc4\x01\xa2\x00\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x10\x00\x02\x01\x03\x03\x02\x04\x03\x05\x05\x04\x04\x00\x00\x01}\x01\x02\x03\x00\x04\x11\x05\x12!1A\x06\x13Qa\x07"q\x142\x81\x91\xa1\x08#B\xb1\xc1\x15R\xd1\xf0$3br\x82\t\n\x16\x17\x18\x19\x1a%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\x83\x84\x85\x86\x87\x88\x89\x8a\x92\x93\x94\x95\x96\x97\x98\x99\x9a\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\x01\x00\x03\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x11\x00\x02\x01\x02\x04\x04\x03\x04\x07\x05\x04\x04\x00\x01\x02w\x00\x01\x02\x03\x11\x04\x05!1\x06\x12AQ\x07aq\x13"2\x81\x08\x14B\x91\xa1\xb1\xc1\t#3R\xf0\x15br\xd1\n\x16$4\xe1%\xf1\x17\x18\x19\x1a&\'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x92\x93\x94
圖像字節編碼:b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xfe\x00*Intel(R) JPEG Library, version 1,5,4,36\x00\xff\xdb\x00C\x00\x05\x03\x04\x04\x04\x03\x05\x04\x04\x04\x05\x05\x05\x06\x07\x0c\x08\x07\x07\x07\x07\x0f\x0b\x0b\t\x0c\x11\x0f\x12\x12\x11\x0f\x11\x11\x13\x16\x1c\x17\x13\x14\x1a\x15\x11\x11\x18!\x18\x1a\x1d\x1d\x1f\x1f\x1f\x13\x17"$"\x1e$\x1c\x1e\x1f\x1e\xff\xdb\x00C\x01\x05\x05\x05\x07\x06\x07\x0e\x08\x08\x0e\x1e\x14\x11\x14\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\xff\xc4\x01\xa2\x00\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\'
網絡安全Base_64編碼:_9j_4AAQSkZJRgABAQAAAQABAAD__gAqSW50ZWwoUikgSlBFRyBMaWJyYXJ5LCB2ZXJzaW9uIDEsNSw0LDM2AP_bAEMABQMEBAQDBQQEBAUFBQYHDAgHBwcHDwsLCQwRDxISEQ8RERMWHBcTFBoVEREYIRgaHR0fHx8TFyIkIh4kHB4fHv_bAEMBBQUFBwYHDggIDh4UERQeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHv_EAaIAAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKCxAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4-Tl5ufo6erx8vP09fb3-Pn6AQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgsRAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5-jp6vLz9PX29_j5-v_AABEIBDgHgAMBEQACEQEDEQH_2gAMAwEAAhEDEQA_APqOS3yOlYygjiGRwbW6UowEX4lwM10QQXJ4zg1ZSJGGRRYttFS4zjIqZGLKwY85rILlO5fnFYyepJSmJxzWbQmVzzzWdiAXJaq5RouQLnBq4I0Rq2vAFdMSjSt8GtkUi4tDNUPzxSLIZjxTRLMq-YYIrCaJOU1RgGbmvOq7iktDCuRv71zNkOJTZfWkmZiFa1TIZBMh9Klk3K7rjmspBzETnis2y1ZhbP8APzQmUolxnB71aY-UiYmi4mhFcjFS7mMkW42OMitoPQxlct27ZXmruJMdKM1LkDRVkBzQtRWHx8rzVxjctMkSIZrTksUpFqLjHFNI0uWkbNVYpMJQWHSnY1iyvswc0WNUy_aKrAZoZpFllYlzWVjVDWAWi5ZBM-7oaNAKFyT2PNGhJUaRgetKwiMy56mpaEKjjNCQtR5bPNXylIktSTKAam4M6K2i3IMV1U9hWLMUTBq1CxdRXA70XYWK2oJKycEis6jYuVGd5Mmfmya5nfqWkMkh9qVhkDw8GpAytQgyDxTFYxLlWAzSAzpZWHGTUtjSJIJW9aaYza02RiOprS6CzN-1JIGaTGWCAetQ0MryxEZxUFIrOCKaYxoOBVIBhbmqEG7kUh2LMZzik2FizGM8ipuBYWImqSJFMDZziqC47yWK9K1ixEUiEDpV3EyrMvBzWEzOb0Ma7jPmc1hI45biwjbSSIuaEEuO9OxJP5wx1oGhrTN2NM2ih0UhJ61UTeJbgdia0NEX4fmwaCrFhRkVaAYyVdgIZFrJoCu61NiWiBxtbNSxFqCQFacWFidHP41qpFJEwmYd6rmQWHwSMzcmi47F2FeQaLCZpwrkCmkRclKHFNoV0QTLkYqWhGfcQ56io5SkzOnt8Z4pOBaZRmhFZyiWmVJExms5IdysYdzVNhFu1tR3FXFAjQgiCjFapDsLJFmraERPHhTWbRLM68QmsnEVhLCMhuRRGIrGvCgwMVqolEqLg0yrDpeRzSauSVmQ5rFw1ESBDtp8orETRjuKB8q6lO9tgVJArOUOphOCsc_ch4pCMEDNZp2OOUbMlhYtSvcEX9NtpLu6WFc-p-lVSpuUikjoL02dtarFtCsOVAHIrevyQXmdtKnzHK-IPF32WIwLcfMBjiuP2tSeh6lHDW1ZwWq6vd6hIcu-3ue2K2pUm9WdllEi05Ybdsuu8nqTmuiMiXE0vtK4x8uO2K0tcXKIJZZsrEhDe_StFYTRPa2Jl-eeT6jNWiC0FsbdwVKBj196TkUlcjm1eFCVjU_lWbmzSNPuZ9zr1yD-7VyPYUK5bp2KAvNYuZDsZlU9Ce1bQM5pFlbDWpcJJqEu08q2f61ucstzXtfDYu7ZU1QCYA5DhiGH4itEZyRtWnhjTUtlSRdw6KcnIqiLsnTQtOigfdahgAeRk5p3JY200zSi5hmsXbAyp2k5HtRzCNCGz04SBkgZZF6OM81cWTJF2aK08tXRGhlPBBztcehHTNDRKZhalb23WO72Nz5eT37qaEDucHrGo3elaghiHm2jn59pyYm9QPT_AApSjcpG3odw3mB4kVEY7mEY-U564q2Zt6m5qmol4vshk3SIm6Pn_WJjsfUf0NIDkLDW_O1CXS548zrzHv4DDr09apRuFyF4LGa6ljDNCrDEkL_w57j_ABqXApSM-70W_gYIwjvIW-66Eb09_eiKCUirHp9wpJt5p7O6U8jJCOPUUSRUHfctR61qtsRFqCmYIciUcSL6MD3xWehfod14P-IEMgWO4lYuh2tuGGx_UUmUj0rSdVhmbzImDhudy9_rimRKBdukgvd27DRN3HVG9fakJuyMx0ktY9zncqtgTDpj0b0q4
標準Base_64編碼:/9j/4AAQSkZJRgABAQAAAQABAAD//gAqSW50ZWwoUikgSlBFRyBMaWJyYXJ5LCB2ZXJzaW9uIDEsNSw0LDM2AP/bAEMABQMEBAQDBQQEBAUFBQYHDAgHBwcHDwsLCQwRDxISEQ8RERMWHBcTFBoVEREYIRgaHR0fHx8TFyIkIh4kHB4fHv/bAEMBBQUFBwYHDggIDh4UERQeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHv/EAaIAAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKCxAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6AQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgsRAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/AABEIBDgHgAMBEQACEQEDEQH/2gAMAwEAAhEDEQA/APqOS3yOlYygjiGRwbW6UowEX4lwM10QQXJ4zg1ZSJGGRRYttFS4zjIqZGLKwY85rILlO5fnFYyepJSmJxzWbQmVzzzWdiAXJaq5RouQLnBq4I0Rq2vAFdMSjSt8GtkUi4tDNUPzxSLIZjxTRLMq+YYIrCaJOU1RgGbmvOq7iktDCuRv71zNkOJTZfWkmZiFa1TIZBMh9Klk3K7rjmspBzETnis2y1ZhbP8APzQmUolxnB71aY+UiYmi4mhFcjFS7mMkW42OMitoPQxlct27ZXmruJMdKM1LkDRVkBzQtRWHx8rzVxjctMkSIZrTksUpFqLjHFNI0uWkbNVYpMJQWHSnY1iyvswc0WNUy/aKrAZoZpFllYlzWVjVDWAWi5ZBM+7oaNAKFyT2PNGhJUaRgetKwiMy56mpaEKjjNCQtR5bPNXylIktSTKAam4M6K2i3IMV1U9hWLMUTBq1CxdRXA70XYWK2oJKycEis6jYuVGd5Mmfmya5nfqWkMkh9qVhkDw8GpAytQgyDxTFYxLlWAzSAzpZWHGTUtjSJIJW9aaYza02RiOprS6CzN+1JIGaTGWCAetQ0MryxEZxUFIrOCKaYxoOBVIBhbmqEG7kUh2LMZzik2FizGM8ipuBYWImqSJFMDZziqC47yWK9K1ixEUiEDpV3EyrMvBzWEzOb0Ma7jPmc1hI45biwjbSSIuaEEuO9OxJP5wx1oGhrTN2NM2ih0UhJ61UTeJbgdia0NEX4fmwaCrFhRkVaAYyVdgIZFrJoCu61NiWiBxtbNSxFqCQFacWFidHP41qpFJEwmYd6rmQWHwSMzcmi47F2FeQaLCZpwrkCmkRclKHFNoV0QTLkYqWhGfcQ56io5SkzOnt8Z4pOBaZRmhFZyiWmVJExms5IdysYdzVNhFu1tR3FXFAjQgiCjFapDsLJFmraERPHhTWbRLM68QmsnEVhLCMhuRRGIrGvCgwMVqolEqLg0yrDpeRzSauSVmQ5rFw1ESBDtp8orETRjuKB8q6lO9tgVJArOUOphOCsc/ch4pCMEDNZp2OOUbMlhYtSvcEX9NtpLu6WFc+p+lVSpuUikjoL02dtarFtCsOVAHIrevyQXmdtKnzHK+IPF32WIwLcfMBjiuP2tSeh6lHDW1ZwWq6vd6hIcu+3ue2K2pUm9WdllEi05Ybdsuu8nqTmuiMiXE0vtK4x8uO2K0tcXKIJZZsrEhDe/StFYTRPa2Jl+eeT6jNWiC0FsbdwVKBj196TkUlcjm1eFCVjU/lWbmzSNPuZ9zr1yD+7VyPYUK5bp2KAvNYuZDsZlU9Ce1bQM5pFlbDWpcJJqEu08q2f61ucstzXtfDYu7ZU1QCYA5DhiGH4itEZyRtWnhjTUtlSRdw6KcnIqiLsnTQtOigfdahgAeRk5p3JY200zSi5hmsXbAyp2k5HtRzCNCGz04SBkgZZF6OM81cWTJF2aK08tXRGhlPBBztcehHTNDRKZhalb23WO72Nz5eT37qaEDucHrGo3elaghiHm2jn59pyYm9QPT/AApSjcpG3odw3mB4kVEY7mEY+U564q2Zt6m5qmol4vshk3SIm6Pn/WJjsfUf0NIDkLDW/O1CXS548zrzHv4DDr09apRuFyF4LGa6ljDNCrDEkL/w57j/ABqXApSM+70W/gYIwjvIW+66Eb09/eiKCUirHp9wpJt5p7O6U8jJCOPUUSRUHfctR61qtsRFqCmYIciUcSL6MD3xWehfod14P+IEMgWO4lYuh2tuGGx/UUmUj0rSdVhmbzImDhudy9/rimRKBdukgvd27DRN3HVG9fakJuyMx0ktY9zncqtgTDpj0b0q4+Zm5XLxBuAgU7jj5Tu+YUNFQutyItNbT7/uHo2Rw31qbtIptMSW1hvw4VVEp5
標準Base_64編碼:/9j/4AAQSkZJRgABAQAAAQABAAD//gAqSW50ZWwoUikgSlBFRyBMaWJyYXJ5LCB2ZXJzaW9uIDEsNSw0LDM2AP/bAEMABQMEBAQDBQQEBAUFBQYHDAgHBwcHDwsLCQwRDxISEQ8RERMWHBcTFBoVEREYIRgaHR0fHx8TFyIkIh4kHB4fHv/bAEMBBQUFBwYHDggIDh4UERQeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHv/EAaIAAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKCxAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6AQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgsRAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/AABEIBDgHgAMBEQACEQEDEQH/2gAMAwEAAhEDEQA/APqOS3yOlYygjiGRwbW6UowEX4lwM10QQXJ4zg1ZSJGGRRYttFS4zjIqZGLKwY85rILlO5fnFYyepJSmJxzWbQmVzzzWdiAXJaq5RouQLnBq4I0Rq2vAFdMSjSt8GtkUi4tDNUPzxSLIZjxTRLMq+YYIrCaJOU1RgGbmvOq7iktDCuRv71zNkOJTZfWkmZiFa1TIZBMh9Klk3K7rjmspBzETnis2y1ZhbP8APzQmUolxnB71aY+UiYmi4mhFcjFS7mMkW42OMitoPQxlct27ZXmruJMdKM1LkDRVkBzQtRWHx8rzVxjctMkSIZrTksUpFqLjHFNI0uWkbNVYpMJQWHSnY1iyvswc0WNUy/aKrAZoZpFllYlzWVjVDWAWi5ZBM+7oaNAKFyT2PNGhJUaRgetKwiMy56mpaEKjjNCQtR5bPNXylIktSTKAam4M6K2i3IMV1U9hWLMUTBq1CxdRXA70XYWK2oJKycEis6jYuVGd5Mmfmya5nfqWkMkh9qVhkDw8GpAytQgyDxTFYxLlWAzSAzpZWHGTUtjSJIJW9aaYza02RiOprS6CzN+1JIGaTGWCAetQ0MryxEZxUFIrOCKaYxoOBVIBhbmqEG7kUh2LMZzik2FizGM8ipuBYWImqSJFMDZziqC47yWK9K1ixEUiEDpV3EyrMvBzWEzOb0Ma7jPmc1hI45biwjbSSIuaEEuO9OxJP5wx1oGhrTN2NM2ih0UhJ61UTeJbgdia0NEX4fmwaCrFhRkVaAYyVdgIZFrJoCu61NiWiBxtbNSxFqCQFacWFidHP41qpFJEwmYd6rmQWHwSMzcmi47F2FeQaLCZpwrkCmkRclKHFNoV0QTLkYqWhGfcQ56io5SkzOnt8Z4pOBaZRmhFZyiWmVJExms5IdysYdzVNhFu1tR3FXFAjQgiCjFapDsLJFmraERPHhTWbRLM68QmsnEVhLCMhuRRGIrGvCgwMVqolEqLg0yrDpeRzSauSVmQ5rFw1ESBDtp8orETRjuKB8q6lO9tgVJArOUOphOCsc/ch4pCMEDNZp2OOUbMlhYtSvcEX9NtpLu6WFc+p+lVSpuUikjoL02dtarFtCsOVAHIrevyQXmdtKnzHK+IPF32WIwLcfMBjiuP2tSeh6lHDW1ZwWq6vd6hIcu+3ue2K2pUm9WdllEi05Ybdsuu8nqTmuiMiXE0vtK4x8uO2K0tcXKIJZZsrEhDe/StFYTRPa2Jl+eeT6jNWiC0FsbdwVKBj196TkUlcjm1eFCVjU/lWbmzSNPuZ9zr1yD+7VyPYUK5bp2KAvNYuZDsZlU9Ce1bQM5pFlbDWpcJJqEu08q2f61ucstzXtfDYu7ZU1QCYA5DhiGH4itEZyRtWnhjTUtlSRdw6KcnIqiLsnTQtOigfdahgAeRk5p3JY200zSi5hmsXbAyp2k5HtRzCNCGz04SBkgZZF6OM81cWTJF2aK08tXRGhlPBBztcehHTNDRKZhalb23WO72Nz5eT37qaEDucHrGo3elaghiHm2jn59pyYm9QPT/AApSjcpG3odw3mB4kVEY7mEY+U564q2Zt6m5qmol4vshk3SIm6Pn/WJjsfUf0NIDkLDW/O1CXS548zrzHv4DDr09apRuFyF4LGa6ljDNCrDEkL/w57j/ABqXApSM+70W/gYIwjvIW+66Eb09/eiKCUirHp9wpJt5p7O6U8jJCOPUUSRUHfctR61qtsRFqCmYIciUcSL6MD3xWehfod14P+IEMgWO4lYuh2tuGGx/UUmUj0rSdVhmbzImDhudy9/rimRKBdukgvd27DRN3HVG9fakJuyMx0ktY9zncqtgTDpj0b0q4+Zm5XLxBuAgU7jj5Tu+YUNFQutyItNbT7/uHo2Rw31qbtIptMSW1hvw4VVEp5
網絡安全Base_64編碼:_9j_4AAQSkZJRgABAQAAAQABAAD__gAqSW50ZWwoUikgSlBFRyBMaWJyYXJ5LCB2ZXJzaW9uIDEsNSw0LDM2AP_bAEMABQMEBAQDBQQEBAUFBQYHDAgHBwcHDwsLCQwRDxISEQ8RERMWHBcTFBoVEREYIRgaHR0fHx8TFyIkIh4kHB4fHv_bAEMBBQUFBwYHDggIDh4UERQeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHv_EAaIAAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKCxAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4-Tl5ufo6erx8vP09fb3-Pn6AQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgsRAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5-jp6vLz9PX29_j5-v_AABEIBDgHgAMBEQACEQEDEQH_2gAMAwEAAhEDEQA_APqOS3yOlYygjiGRwbW6UowEX4lwM10QQXJ4zg1ZSJGGRRYttFS4zjIqZGLKwY85rILlO5fnFYyepJSmJxzWbQmVzzzWdiAXJaq5RouQLnBq4I0Rq2vAFdMSjSt8GtkUi4tDNUPzxSLIZjxTRLMq-YYIrCaJOU1RgGbmvOq7iktDCuRv71zNkOJTZfWkmZiFa1TIZBMh9Klk3K7rjmspBzETnis2y1ZhbP8APzQmUolxnB71aY-UiYmi4mhFcjFS7mMkW42OMitoPQxlct27ZXmruJMdKM1LkDRVkBzQtRWHx8rzVxjctMkSIZrTksUpFqLjHFNI0uWkbNVYpMJQWHSnY1iyvswc0WNUy_aKrAZoZpFllYlzWVjVDWAWi5ZBM-7oaNAKFyT2PNGhJUaRgetKwiMy56mpaEKjjNCQtR5bPNXylIktSTKAam4M6K2i3IMV1U9hWLMUTBq1CxdRXA70XYWK2oJKycEis6jYuVGd5Mmfmya5nfqWkMkh9qVhkDw8GpAytQgyDxTFYxLlWAzSAzpZWHGTUtjSJIJW9aaYza02RiOprS6CzN-1JIGaTGWCAetQ0MryxEZxUFIrOCKaYxoOBVIBhbmqEG7kUh2LMZzik2FizGM8ipuBYWImqSJFMDZziqC47yWK9K1ixEUiEDpV3EyrMvBzWEzOb0Ma7jPmc1hI45biwjbSSIuaEEuO9OxJP5wx1oGhrTN2NM2ih0UhJ61UTeJbgdia0NEX4fmwaCrFhRkVaAYyVdgIZFrJoCu61NiWiBxtbNSxFqCQFacWFidHP41qpFJEwmYd6rmQWHwSMzcmi47F2FeQaLCZpwrkCmkRclKHFNoV0QTLkYqWhGfcQ56io5SkzOnt8Z4pOBaZRmhFZyiWmVJExms5IdysYdzVNhFu1tR3FXFAjQgiCjFapDsLJFmraERPHhTWbRLM68QmsnEVhLCMhuRRGIrGvCgwMVqolEqLg0yrDpeRzSauSVmQ5rFw1ESBDtp8orETRjuKB8q6lO9tgVJArOUOphOCsc_ch4pCMEDNZp2OOUbMlhYtSvcEX9NtpLu6WFc-p-lVSpuUikjoL02dtarFtCsOVAHIrevyQXmdtKnzHK-IPF32WIwLcfMBjiuP2tSeh6lHDW1ZwWq6vd6hIcu-3ue2K2pUm9WdllEi05Ybdsuu8nqTmuiMiXE0vtK4x8uO2K0tcXKIJZZsrEhDe_StFYTRPa2Jl-eeT6jNWiC0FsbdwVKBj196TkUlcjm1eFCVjU_lWbmzSNPuZ9zr1yD-7VyPYUK5bp2KAvNYuZDsZlU9Ce1bQM5pFlbDWpcJJqEu08q2f61ucstzXtfDYu7ZU1QCYA5DhiGH4itEZyRtWnhjTUtlSRdw6KcnIqiLsnTQtOigfdahgAeRk5p3JY200zSi5hmsXbAyp2k5HtRzCNCGz04SBkgZZF6OM81cWTJF2aK08tXRGhlPBBztcehHTNDRKZhalb23WO72Nz5eT37qaEDucHrGo3elaghiHm2jn59pyYm9QPT_AApSjcpG3odw3mB4kVEY7mEY-U564q2Zt6m5qmol4vshk3SIm6Pn_WJjsfUf0NIDkLDW_O1CXS548zrzHv4DDr09apRuFyF4LGa6ljDNCrDEkL_w57j_ABqXApSM-70W_gYIwjvIW-66Eb09_eiKCUirHp9wpJt5p7O6U8jJCOPUUSRUHfctR61qtsRFqCmYIciUcSL6MD3xWehfod14P-IEMgWO4lYuh2tuGGx_UUmUj0rSdVhmbzImDhudy9_rimRKBdukgvd27DRN3HVG9fakJuyMx0ktY9zncqtgTDpj0b0q4-Zm5XLxBuAgU7jj5Tu-YUNFQutyItNbT7_uHo2Rw31qbtIptMSW1hvw4VVEp5K_3qynSUibdzn3tbnT7l4GDeQ549Yz615tWDgxoRy0uUYbZlGceoqVLuDQQPuXJ4I6j0quYyY4ON-M0XJHBCeoq0ZyRZt4-elUiC_DHWiAl8vLUASiKiwDljwaaAlGAKqxYx5AO9A0yF5OetSykRPJx1qHYCpK-TnNYyKK7HJ4NZ31M5PURie9Nk3CNuapBcsxtxTKTJUapbsWhXaocxkbOcVlzgRmQijmGSQznODWsJGbLAcGtbkjlJzVpiJlGRTEDIMZqQImFDQyncdDUMTM

圖像解碼

import tensorflow as tf
import base64
import cv2
import numpy as np

if __name__ == "__main__":

    path = "/Users/admin/Documents/111.jpeg"
    img = tf.io.read_file(path)
    print("圖像字節編碼:{}".format(img))

    img1 = tf.io.gfile.GFile(path, 'rb')
    print("圖像字節編碼:{}".format(img1.readlines()))

    img_64 = tf.io.encode_base64(img)
    img_64 = format(img_64)[2:-1]
    print("網絡安全Base_64編碼:{}".format(img_64))

    img_b64 = list(img_64)
    img_b64_list = []
    for i in range(len(img_b64)):
        if img_b64[i] == "-":
            img_b64_list.append("+")
        elif img_b64[i] == "_":
            img_b64_list.append("/")
        else:
            img_b64_list.append(img_b64[i])
    if len(img_b64_list) % 4 != 0:
        remainder = len(img_b64_list) % 4
        for i in range(remainder):
            img_b64_list.append("=")
    img_b64 = "".join(img_b64_list)
    print("標準Base_64編碼:{}".format(img_b64))

    with open(path, "rb") as f:
        img3 = f.read()
        img_stand_b64 = base64.b64encode(img3)
        img_stand_b64_str = img_stand_b64.decode("utf8")
    print("標準Base_64編碼:{}".format(img_stand_b64_str))

    img_w64 = list(img_stand_b64_str)
    img_w64_list = []
    for i in range(len(img_w64)):
        if img_w64[i] == "+":
            img_w64_list.append("-")
        elif img_w64[i] == "/":
            img_w64_list.append("_")
        elif img_w64[i] == "=":
            break
        else:
            img_w64_list.append(img_w64[i])
    img_w64 = "".join(img_w64_list)
    print("網絡安全Base_64編碼:{}".format(img_w64))
    # 網絡安全Base64解碼
    img_w64_tensor = tf.convert_to_tensor(img_w64)
    img_w64_decode = tf.io.decode_base64(img_w64_tensor)
    img_matrix = tf.io.decode_image(img_w64_decode)
    img_matrix = np.uint8(img_matrix)
    r, g, b = cv2.split(img_matrix)
    img_matrix = cv2.merge((b, g, r))
    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    while True:
        cv2.imshow('img', img_matrix)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

運行結果

圖像數據處理

圖像壓縮

import tensorflow as tf
import base64
import cv2

if __name__ == "__main__":

    path = "/Users/admin/Documents/111.jpeg"
    img = tf.io.read_file(path)
    print("圖像字節編碼:{}".format(img))

    img1 = tf.io.gfile.GFile(path, 'rb')
    print("圖像字節編碼:{}".format(img1.readlines()))

    img_64 = tf.io.encode_base64(img)
    img_64 = format(img_64)[2:-1]
    print("網絡安全Base_64編碼:{}".format(img_64))

    img_b64 = list(img_64)
    img_b64_list = []
    for i in range(len(img_b64)):
        if img_b64[i] == "-":
            img_b64_list.append("+")
        elif img_b64[i] == "_":
            img_b64_list.append("/")
        else:
            img_b64_list.append(img_b64[i])
    if len(img_b64_list) % 4 != 0:
        remainder = len(img_b64_list) % 4
        for i in range(remainder):
            img_b64_list.append("=")
    img_b64 = "".join(img_b64_list)
    print("標準Base_64編碼:{}".format(img_b64))

    with open(path, "rb") as f:
        img3 = f.read()
        img_stand_b64 = base64.b64encode(img3)
        img_stand_b64_str = img_stand_b64.decode("utf8")
    print("標準Base_64編碼:{}".format(img_stand_b64_str))

    img_w64 = list(img_stand_b64_str)
    img_w64_list = []
    for i in range(len(img_w64)):
        if img_w64[i] == "+":
            img_w64_list.append("-")
        elif img_w64[i] == "/":
            img_w64_list.append("_")
        elif img_w64[i] == "=":
            break
        else:
            img_w64_list.append(img_w64[i])
    img_w64 = "".join(img_w64_list)
    print("網絡安全Base_64編碼:{}".format(img_w64))
    # 網絡安全Base64解碼
    img_w64_tensor = tf.convert_to_tensor(img_w64)
    img_w64_decode = tf.io.decode_base64(img_w64_tensor)
    img_matrix = tf.io.decode_image(img_w64_decode)
    # 圖像壓縮的8種方法
    # bilinear雙線性插值,lanczos3爲lanczos插值,卷積核尺寸爲3*3
    # lanczos5爲lanczos插值,卷積核尺寸爲5*5
    # bicubic雙三次插值,gaussian高斯插值,nearest最近鄰插值
    # area區域插值,mitchellcubic米切爾立方插值
    methords1 = ["bilinear", "lanczos3", "lanczos5", "bicubic"]
    methords2 = ["gaussian", "nearest", "area", "mitchellcubic"]
    if img_matrix.dtype != tf.float32:
        img_matrix = tf.image.convert_image_dtype(img_matrix, dtype=tf.float32)
    for i in range(4):
        # tensorflow壓縮圖像
        img_matrix1 = tf.image.resize(img_matrix, [400, 400], method=methords1[i])
        if img_matrix1.dtype == tf.float32:
            img_matrix1 = tf.image.convert_image_dtype(img_matrix1, dtype=tf.uint8)
        img_matrix1 = img_matrix1.numpy()
        r, g, b = cv2.split(img_matrix1)
        img_matrix1 = cv2.merge((b, g, r))
        cv2.namedWindow('img' + str(i), cv2.WINDOW_NORMAL)
        while True:
            cv2.imshow('img' + str(i), img_matrix1)
            key = cv2.waitKey()
            if key & 0xFF == ord('q'):
                break
        cv2.destroyAllWindows()

運行結果

可能這些壓縮方式在我們肉眼看起來是沒有什麼太大區別的。

圖像風格轉換

圖像風格轉換-V1

圖像風格轉換的過程有一點像word2vec,它也是初始化一個隨機圖片,裏面的像素值可能都是噪音,但不斷地與我們希望轉變的圖片的圖像矩陣在每一個卷積層的激活值建立相似度計算,通過建立損失函數與梯度下降法來達到最終的效果,變成我們希望變成的圖像的樣子。有關word2vec的詳細內容請參考Tensorflow深度學習算法整理(二)

  1. 內容特徵,首先對於圖像風格轉換有兩張圖去組合,第一張圖叫內容圖,我們需要從這張圖片裏面提取內容。最後合成的東西主要內容還是這張圖片。從卷積神經網絡來說,就是圖像輸入到CNN得到的某一層的激活值。
  2. 風格特徵,第二張圖,我們需要從這張圖片中提取出它的風格的抽象信息。圖像輸入到CNN得到的某一層的激活值之間的關聯。風格特徵一定是在所有內容之上的都普遍存在的共性。爲了計算出這種共性,我們需要將各個不同內容之間的共性給提取出來,反映到卷積神經網絡中,就是在CNN某一層的激活值之間建立出來的一種關聯關係。
  3. 風格轉換,然後將風格特徵與內容特徵進行融合。

我們可以試着用風格特徵和內容特徵去重建圖片,如果使用內容特徵生成圖像,和卷積神經網絡的訓練(輸入圖像不變,調整參數適應分類結果)相反,保持卷積神經網絡的參數不變,調整圖像,使得生成的圖像和真實的圖像在某一層的特徵上更相近。調整圖像x的像素值,使之與y圖像在CNN中的內容特徵距離變小。損失函數如下

這裏的爲圖像輸入到卷積神經網絡中去得到的某一層的激活值,爲待生成的圖像(初始爲隨機像素值的圖像)輸入到卷積神經網絡中得到的某一層的激活值。再通過均方誤差建立損失函數,來求得最小值。

風格特徵的計算——它是基於一種關聯性去計算,而這種關聯性就是Gram矩陣——圖像在某一層上的激活值,它會有很多個特徵圖,因爲卷積神經網絡中的每一層都會有多個卷積核,每一個卷積核都對應着一種特徵提取手段,所以它就會得到多個神經元的輸出圖,這裏假設有k個卷積核,這k個神經元的輸出兩兩之間去做相似度的計算,即做內積,就得到了一個k*k的矩陣,矩陣裏面的每一個值都代表了兩個特徵圖的相似度。Gram矩陣如下

我們建立了Gram矩陣之後,再通過Gram矩陣建立損失函數

這裏是真實圖像的Gram矩陣,是待生成圖像的Gram矩陣,再去計算均方誤差損失,並求其最小值。

然後,由於卷積神經網絡有多個層次,再將多個層次的損失函數的最小值再進行加權平均。

從上圖中,我們可以看到,當使用內容特徵去重建圖片的時候,使用不同層級的特徵會得到不同效果的圖片,而且使用的層級越高,重建出來的圖片效果就越模糊。使用越底層重建出來的圖片的效果就越好越精細。而當使用風格特徵去重建圖片的時候,當層級比較低的時候,它重建的圖像是一個只有噪聲的圖像,看不出來有什麼效果。當層級比較高層的時候,它重建出來的圖像就跟風格圖像非常的相近。

在上圖的右上角有,表示將內容特徵和風格特徵加權平均起來就作爲總特徵。然後去重建中間下面的灰度圖,然後就能得到,它既有風格特徵圖的風格,又有內容特徵圖的內容。

這裏需要注意的是如果中若ß=0,則就是完全使用內容圖像去重建內容,而如果α=0,則就是完全使用風格圖像去重建風格。

  • V1的優劣
  1. 缺點:慢-由於這裏有一個求損失函數的最小值,都要進行梯度下降的反向傳播的過程,每張圖都要逐步求導。
  2. 優點:逐步調整,能夠產生多張效果圖供我們挑選。因爲這是一個無監督的問題,所以沒有一個標準來衡量生成的圖片到底是好還是不好。

我們這裏要處理的圖像如下,第一張是內容圖像,第二張是風格圖像

現在我們來看一下風格圖像在各個卷積層的特徵體現

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, applications, models
import numpy as np
import matplotlib.pyplot as plt
import datetime

def image_read(image_path):
    '''
    圖像讀取
    :param image_path:
    :return:
    '''
    img = tf.io.read_file(image_path)
    img = tf.io.encode_base64(img)
    img = tf.io.decode_base64(img)
    img = tf.io.decode_image(img)
    img = tf.image.convert_image_dtype(img, dtype=tf.float32)
    # 給圖像的三個維度(高,寬,通道數)增加一個樣本數的維度
    img = img[np.newaxis, :]
    return img

def layers_names():
    '''
    預訓練卷積神經網絡層
    :return:
    '''
    # 提取圖像內容層
    pre_contents_layers = ["block4_conv1"]
    # 提取圖像風格層
    pre_styles_layers = ["block1_conv1", "block2_conv1", "block3_conv1", "block4_conv1"]
    num_style_layers = len(pre_styles_layers)
    num_contents_layers = len(pre_contents_layers)
    return pre_contents_layers, pre_styles_layers, num_style_layers, num_contents_layers

def pre_vgg16(layers_name):
    '''
    預訓練神經網絡提取信息
    :param layers_name:
    :return:
    '''
    # 獲取一個VGG16的已經訓練好的預處理模型
    vgg16 = applications.VGG16(include_top=False, weights='imagenet')
    # 該模型不可訓練
    vgg16.trainable = False
    # 獲取該模型中"block1_conv1", "block2_conv1", "block3_conv1", "block4_conv1"
    # 這些卷積層的輸出圖像
    outputs = [vgg16.get_layer(name).output for name in layers_name]
    # 構建一個多輸出模型
    model = models.Model([vgg16.input], outputs)
    return model

def pre_res_single(layers_name, outputs):
    '''
    輸出預訓練神經網絡特徵
    :param layers_name:
    :param outputs:
    :return:
    '''
    i = 0
    for name, output in zip(layers_name, outputs):
        i += 1
        plt.figure(i)
        print("網絡層:" + name)
        print("輸出圖像特徵維度:" + str(output.numpy().shape))
        print("輸出圖像特徵值:" + str(output.numpy()))
        for j in range(4):
            plt.subplot(2, 2, j + 1)
            plt.subplots_adjust(wspace=0.5, hspace=0.8)
            plt.imshow(output.numpy()[0][:, :, j])
            plt.title(name + "-" + str(j + 1))
        plt.savefig("/Users/admin/Documents/feature-{}.png".format(i), format='png', dpi=300)
        plt.show()

if __name__ == "__main__":

    stamp = datetime.datetime.now().strftime("%Y%m%d-%H:%M:%S")
    img_content_path = "/Users/admin/Documents/111.jpeg"
    img_style_path = "/Users/admin/Documents/222.jpeg"
    img_content = image_read(img_content_path)
    print("內容圖像數據維度:", img_content.shape)
    img_style = image_read(img_style_path)
    print("風格圖像數據維度:", img_style.shape)
    pre_contents_layers, pre_styles_layers, num_style_layers, num_content_layers = layers_names()
    # 獲取風格圖像的預處理模型
    pre_model = pre_vgg16(pre_styles_layers)
    print(pre_model.summary())
    # 對輸入圖像進行處理,生成各層的特徵圖像,這裏由於img_style是float32的
    # 所以要乘以255轉成uint8的
    pre_styles = pre_model(img_style * 255)
    # 顯示各層的特徵圖像
    pre_res_single(pre_styles_layers, pre_styles)

運行結果

內容圖像數據維度: (1, 1080, 1920, 3)
風格圖像數據維度: (1, 1080, 1920, 3)
Model: "model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         [(None, None, None, 3)]   0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, None, None, 64)    1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, None, None, 64)    36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, None, None, 64)    0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, None, None, 128)   73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, None, None, 128)   147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, None, None, 128)   0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, None, None, 256)   295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, None, None, 256)   590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, None, None, 256)   590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, None, None, 256)   0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, None, None, 512)   1180160   
=================================================================
Total params: 2,915,648
Trainable params: 0
Non-trainable params: 2,915,648
_________________________________________________________________
None
網絡層:block1_conv1
輸出圖像特徵維度:(1, 1080, 1920, 64)
輸出圖像特徵值:[[[[0.00000000e+00 3.50734215e+01 0.00000000e+00 ... 0.00000000e+00
    2.67500877e+01 0.00000000e+00]
   [0.00000000e+00 3.91990547e+01 6.90585327e+01 ... 0.00000000e+00
    2.54579147e+02 1.30025497e+02]
   [0.00000000e+00 3.87475510e+01 6.86380920e+01 ... 0.00000000e+00
    2.52212418e+02 1.29513153e+02]
   ...
   [0.00000000e+00 9.01276703e+01 7.36156693e+01 ... 0.00000000e+00
    2.72500763e+02 1.35146271e+02]
   [0.00000000e+00 9.00991058e+01 7.37777023e+01 ... 0.00000000e+00
    2.72544952e+02 1.35678818e+02]
   [2.39254618e+00 6.18333473e+01 1.48342178e+02 ... 0.00000000e+00
    4.33243164e+02 3.26002716e+02]]

  [[0.00000000e+00 4.76682587e+01 0.00000000e+00 ... 0.00000000e+00
    0.00000000e+00 0.00000000e+00]
   [7.45549393e+00 5.06021614e+01 5.74963608e+01 ... 0.00000000e+00
    1.06799955e+01 1.42447872e+01]
   [8.08931255e+00 4.99451828e+01 5.74932022e+01 ... 0.00000000e+00
    1.48615904e+01 1.64684925e+01]
   ...
   [4.95698547e+00 1.24211044e+02 5.85898209e+01 ... 0.00000000e+00
    1.22000992e+00 2.53382540e+00]
   [4.23387146e+00 1.24196045e+02 5.83764114e+01 ... 0.00000000e+00
    1.35891068e+00 2.39634371e+00]
   [3.16444244e+02 8.34078140e+01 1.74379440e+02 ... 0.00000000e+00
    3.19646149e+02 3.12334381e+02]]

  [[0.00000000e+00 4.76078606e+01 0.00000000e+00 ... 0.00000000e+00
    0.00000000e+00 0.00000000e+00]
   [7.85782814e+00 5.04593315e+01 5.65473328e+01 ... 0.00000000e+00
    6.05578423e+00 1.15193357e+01]
   [7.05079889e+00 4.97525864e+01 5.68007965e+01 ... 0.00000000e+00
    1.05671988e+01 1.40185022e+01]
   ...
   [4.26544046e+00 1.24133041e+02 5.87148438e+01 ... 0.00000000e+00
    2.61668086e+00 3.42590523e+00]
   [3.68372965e+00 1.24107971e+02 5.85036774e+01 ... 0.00000000e+00
    1.95210719e+00 3.12355614e+00]
   [3.16029480e+02 8.33877335e+01 1.74063797e+02 ... 0.00000000e+00
    3.18094147e+02 3.11799530e+02]]

  ...

  [[0.00000000e+00 7.22020626e+00 0.00000000e+00 ... 0.00000000e+00
    0.00000000e+00 0.00000000e+00]
   [0.00000000e+00 8.99872398e+00 6.30364037e+00 ... 0.00000000e+00
    1.03156404e+01 5.13160896e+00]
   [0.00000000e+00 8.67577267e+00 7.56701469e+00 ... 0.00000000e+00
    1.26641912e+01 8.03653526e+00]
   ...
   [0.00000000e+00 1.32920628e+01 3.31297898e+00 ... 0.00000000e+00
    0.00000000e+00 0.00000000e+00]
   [0.00000000e+00 1.27987690e+01 3.74767375e+00 ... 0.00000000e+00
    0.00000000e+00 0.00000000e+00]
   [2.54575653e+01 8.21284676e+00 1.36057062e+01 ... 0.00000000e+00
    2.56714516e+01 2.49409065e+01]]

  [[0.00000000e+00 7.56999588e+00 0.00000000e+00 ... 0.00000000e+00
    0.00000000e+00 0.00000000e+00]
   [2.20301819e+00 9.11592102e+00 7.63547754e+00 ... 0.00000000e+00
    6.09224987e+00 5.36004353e+00]
   [9.23264217e+00 8.68169403e+00 9.38160229e+00 ... 0.00000000e+00
    1.03795977e+01 1.00003605e+01]
   ...
   [0.00000000e+00 1.32920628e+01 3.31297898e+00 ... 0.00000000e+00
    0.00000000e+00 0.00000000e+00]
   [0.00000000e+00 1.27987690e+01 3.74767375e+00 ... 0.00000000e+00
    0.00000000e+00 0.00000000e+00]
   [2.54575653e+01 8.21284676e+00 1.36057062e+01 ... 0.00000000e+00
    2.56714516e+01 2.49409065e+01]]

  [[0.00000000e+00 5.23716497e+00 2.57816613e-02 ... 0.00000000e+00
    0.00000000e+00 0.00000000e+00]
   [2.88349552e+01 6.12702703e+00 9.30899715e+00 ... 0.00000000e+00
    0.00000000e+00 7.93819904e+00]
   [3.14419498e+01 5.75989151e+00 9.71562386e+00 ... 0.00000000e+00
    0.00000000e+00 1.00123549e+01]
   ...
   [1.83225994e+01 8.99242115e+00 5.53800106e+00 ... 0.00000000e+00
    0.00000000e+00 2.27055025e+00]
   [1.96522484e+01 8.66088772e+00 5.98857498e+00 ... 0.00000000e+00
    0.00000000e+00 3.34856749e+00]
   [3.51342316e+01 5.51944590e+00 1.22695465e+01 ... 0.00000000e+00
    1.01250381e+01 2.10005398e+01]]]]

以上是使用VGG16模型進行預處理的過程。這裏我們重點來看一下VGG16的網絡結構,它是有12層的網絡結構(包括池化層),這裏輸入的圖像和各層輸出的圖像都是四個維度的,第一個維度是樣本數量,第二個維度是圖像的高,第三個維度是圖像的寬,第四個維度是通道數。從

block1_conv1 (Conv2D)        (None, None, None, 64)    1792   

來看,說明它的輸出是64通道的圖像,說明該層有64個卷積核,每一個通道代表一種特徵,我們將其中前4個通道的圖像顯示了出來。但是我們需要注意的是,可以顯示的遠遠不止4個通道的圖像。比方說我們增加顯示2個通道,共顯示6個通道的圖像如下

def pre_res_single(layers_name, outputs):
    '''
    輸出預訓練神經網絡特徵
    :param layers_name:
    :param outputs:
    :return:
    '''
    i = 0
    for name, output in zip(layers_name, outputs):
        i += 1
        plt.figure(i)
        print("網絡層:" + name)
        print("輸出圖像特徵維度:" + str(output.numpy().shape))
        print("輸出圖像特徵值:" + str(output.numpy()))
        for j in range(6):
            plt.subplot(2, 3, j + 1)
            plt.subplots_adjust(wspace=0.5, hspace=0.8)
            plt.imshow(output.numpy()[0][:, :, j])
            plt.title(name + "-" + str(j + 1))
        plt.savefig("/Users/admin/Documents/feature-{}.png".format(i), format='png', dpi=300)
        plt.show()

當然我們也可以使用該VGG16模型來進行圖像預測,雖然這個跟我們這裏的主題無關。我們這裏使用三張不同的動物圖片

def vgg16_prediction(image):
    '''
    圖像預測
    :param image:
    :return:
    '''
    img = applications.vgg16.preprocess_input(image * 255)
    img = tf.image.resize(img, [224, 224])
    model = applications.VGG16(include_top=True, weights='imagenet')
    prediction = model(img)
    pre_top_5 = applications.vgg16.decode_predictions(prediction.numpy())[0]
    return pre_top_5
img1 = image_read("/Users/admin/Documents/01.jpeg")
img2 = image_read("/Users/admin/Documents/02.jpeg")
img3 = image_read("/Users/admin/Documents/03.jpeg")
pre_top_5_1 = vgg16_prediction(img1)
pre_top_5_2 = vgg16_prediction(img2)
pre_top_5_3 = vgg16_prediction(img3)
print("預測結果:", pre_top_5_1)
print("預測結果:", pre_top_5_2)
print("預測結果:", pre_top_5_3)

運行結果

預測結果: [('n02128385', 'leopard', 0.76419365), ('n02128925', 'jaguar', 0.23070367), ('n02130308', 'cheetah', 0.004478214), ('n02128757', 'snow_leopard', 0.00038728424), ('n02127052', 'lynx', 0.00014762013)]
預測結果: [('n02391049', 'zebra', 0.99762017), ('n01798484', 'prairie_chicken', 0.0004466429), ('n01968897', 'chambered_nautilus', 0.00036860464), ('n02643566', 'lionfish', 0.00014743306), ('n02129604', 'tiger', 0.00012518844)]
預測結果: [('n02509815', 'lesser_panda', 0.9999169), ('n02443114', 'polecat', 8.1369915e-05), ('n02441942', 'weasel', 1.3608259e-06), ('n02443484', 'black-footed_ferret', 2.4797896e-07), ('n02132136', 'brown_bear', 3.5758706e-08)]

這裏我們可以看到第一張圖片預測第一名的爲leopard,即爲豹子,預測正確;第二張圖片預測第一名的爲zebra,即爲斑馬,預測正確;第三張圖片預測第一名的爲lesser_panda,即爲小熊貓,預測正確。

 現在我們來今進行風格轉換的實現

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, applications, models, optimizers, metrics, Model
import numpy as np
import matplotlib.pyplot as plt
import datetime
import time

# 風格權重
style_weight = 1e-2
# 內容權重
content_weight = 1e4

class Vgg16StyleContentModel(Model):

    def __init__(self, style_layers, content_layers):
        super(Vgg16StyleContentModel, self).__init__()
        self.vgg16 = pre_vgg16(style_layers + content_layers)
        self.style_layers = style_layers
        self.content_layers = content_layers
        self.num_style_layers = len(style_layers)
        self.vgg16.trainable = False

    def call(self, inputs, training=None, mask=None):
        # 進行正向計算
        inputs = inputs * 255.0
        prepocessed_input = applications.vgg16.preprocess_input(inputs)
        outputs = self.vgg16(prepocessed_input)
        style_outputs, content_outputs = (outputs[:self.num_style_layers],
                                          outputs[self.num_style_layers:])
        # 將風格類的圖像(包含真實圖像和待生成圖像)的特徵圖像輸出轉換成Gram矩陣
        style_outputs = [gram_matrix(style_output) for style_output in style_outputs]
        # 將內容特徵輸出圖像和風格特徵輸出圖像放入字典中
        content_dict = {content_name: value for content_name, value in zip(self.content_layers,
                                                                          content_outputs)}
        style_dict = {style_name: value for style_name, value in zip(self.style_layers,
                                                                    style_outputs)}
        return {"content": content_dict, "style": style_dict}

def image_read(image_path):
    '''
    圖像讀取
    :param image_path:
    :return:
    '''
    img = tf.io.read_file(image_path)
    img = tf.io.encode_base64(img)
    img = tf.io.decode_base64(img)
    img = tf.io.decode_image(img)
    # 將圖像轉成0~1之間的浮點型
    img = tf.image.convert_image_dtype(img, dtype=tf.float32)
    # 給圖像的三個維度(高,寬,通道數)增加一個樣本數的維度
    img = img[np.newaxis, :]
    return img

def layers_names():
    '''
    預訓練卷積神經網絡層
    :return:
    '''
    # 提取圖像內容層
    pre_contents_layers = ["block4_conv1"]
    # 提取圖像風格層
    pre_styles_layers = ["block1_conv1", "block2_conv1", "block3_conv1", "block4_conv1"]
    num_style_layers = len(pre_styles_layers)
    num_contents_layers = len(pre_contents_layers)
    return pre_contents_layers, pre_styles_layers, num_style_layers, num_contents_layers

def gram_matrix(inputs):
    '''
    Gram矩陣
    :param inputs:
    :return:
    '''
    res = tf.linalg.einsum("bijc,bijd->bcd", inputs, inputs)
    inputs_shape = tf.shape(inputs).numpy()
    num = tf.convert_to_tensor(inputs_shape[1] * inputs_shape[2], dtype=tf.float32)
    return res / num

def pre_vgg16(layers_name):
    '''
    預訓練神經網絡提取信息
    :param layers_name:
    :return:
    '''
    # 獲取一個VGG16的已經訓練好的預處理模型
    vgg16 = applications.VGG16(include_top=False, weights='imagenet')
    # 該模型不可訓練
    vgg16.trainable = False
    # 獲取該模型中"block1_conv1", "block2_conv1", "block3_conv1", "block4_conv1"
    # 這些卷積層的輸出圖像
    outputs = [vgg16.get_layer(name).output for name in layers_name]
    # 構建一個多輸出模型
    model = models.Model([vgg16.input], outputs)
    return model

def pre_res_single(layers_name, outputs):
    '''
    輸出預訓練神經網絡特徵
    :param layers_name:
    :param outputs:
    :return:
    '''
    i = 0
    for name, output in zip(layers_name, outputs):
        i += 1
        plt.figure(i)
        print("網絡層:" + name)
        print("輸出圖像特徵維度:" + str(output.numpy().shape))
        print("輸出圖像特徵值:" + str(output.numpy()))
        for j in range(4):
            plt.subplot(2, 2, j + 1)
            plt.subplots_adjust(wspace=0.5, hspace=0.8)
            plt.imshow(output.numpy()[0][:, :, j])
            plt.title(name + "-" + str(j + 1))
        plt.savefig("/Users/admin/Documents/feature-{}.png".format(i), format='png', dpi=300)
        plt.show()

def vgg16_prediction(image):
    '''
    圖像預測
    :param image:
    :return:
    '''
    img = applications.vgg16.preprocess_input(image * 255)
    img = tf.image.resize(img, [224, 224])
    model = applications.VGG16(include_top=True, weights='imagenet')
    prediction = model(img)
    pre_top_5 = applications.vgg16.decode_predictions(prediction.numpy())[0]
    return pre_top_5

def loss(outputs, style_targets, content_targets, num_style_layers, num_content_layers):
    '''
    構建損失函數
    :param outputs: 圖像風格信息和內容信息
    :param style_targets: 目標風格信息
    :param content_targets: 目標內容信息
    :param num_style_layers: 風格層數量
    :param num_content_layers: 內容層數量
    :return:
    '''
    style_outputs = outputs["style"]
    content_outputs = outputs["content"]
    # 將各層的風格損失值相加
    style_loss = tf.add_n([tf.math.reduce_mean((style_outputs[name] -
                                                style_targets[name])**2)
                           for name in style_outputs])
    # 分配風格損失
    style_loss *= style_weight / num_style_layers
    # 將各層的內容損失值相加
    content_loss = tf.add_n([tf.math.reduce_mean((content_outputs[name] -
                                                  content_targets[name])**2)
                             for name in content_outputs])
    # 分配內容損失
    content_loss *= content_weight / num_content_layers
    # 總體損失
    loss_value = style_loss + content_loss
    return loss_value, style_loss, content_loss

def grad_loss(inputs, style_targets, content_targets, pre_model, num_style_layers, num_content_layers):
    '''
    獲取訓練變量
    :param inputs: 輸入圖像
    :param style_targets: 目標風格信息
    :param content_targets: 目標內容信息
    :param pre_model: 預訓練模型
    :param num_style_layers: 風格層數
    :param num_content_layers: 內容層數
    :return:
    '''
    with tf.GradientTape() as tape:
        # 獲取各層的特徵圖像
        outputs = pre_model(inputs)
        # 獲取損失函數
        loss_value, style_loss, content_loss = loss(outputs, style_targets, content_targets,
                                                    num_style_layers, num_content_layers)
    # tape.gradient(loss_value, inputs)爲求梯度(即各個參數的偏導數)
    return loss_value, style_loss, content_loss, tape.gradient(loss_value, inputs)

def optimizer_loss(inputs, style_targets, content_targets, pre_model, num_style_layers,
                   num_content_layers):
    '''
    梯度下降
    :param inputs: 輸入圖像
    :param style_targets: 目標風格信息
    :param content_targets: 目標內容信息
    :param pre_model: 預訓練模型
    :param num_style_layers: 風格層數
    :param num_content_layers: 內容層數
    :return:
    '''
    # 創建一個梯度下降優化器
    optimizer = optimizers.Adam(learning_rate=0.02)
    # 獲取損失函數和梯度
    loss_value, style_loss, content_loss, grads = grad_loss(inputs, style_targets,
                                                            content_targets, pre_model,
                                                            num_style_layers, num_content_layers)
    # 進行梯度下降,優化訓練變量
    optimizer.apply_gradients([(grads, inputs)])
    # 將圖像數據處理爲0.0~1.0之間範圍數據
    inputs.assign(tf.clip_by_value(inputs, clip_value_min=0.0, clip_value_max=1.0))
    return loss_value, style_loss, content_loss, inputs

def train(log_path, inputs, style_target, content_target, pre_model, num_style_layers,
          num_content_layers):
    '''
    訓練變量
    :param log_path:
    :param inputs:
    :param style_target:
    :param content_target:
    :param pre_model:
    :param num_style_layers:
    :param num_content_layers:
    :return:
    '''
    i = 0
    summary_writer = tf.summary.create_file_writer(log_path)
    for epoch in range(900):
        time_start = int(time.time())
        epoch_loss_avg = metrics.Mean()
        style_loss_avg = metrics.Mean()
        content_loss_avg = metrics.Mean()
        # 通過梯度下降獲取損失值以及每次優化的訓練變量(圖像)
        loss_value, style_loss, content_loss, style_img = optimizer_loss(inputs,
            style_target, content_target, pre_model, num_style_layers, num_content_layers)
        # 對所有損失值取均值
        epoch_loss_avg(loss_value)
        style_loss_avg(style_loss)
        content_loss_avg(content_loss)
        time_end = int(time.time())
        time_cost = (time_end - time_start) / 60
        if (epoch + 1) % 100 == 0:
            i += 1
            print("epoch:", i)
            plt.figure()
            plt.imshow(style_img.read_value()[0])
            plt.savefig("/Users/admin/Documents/transed-{}.png".format(i), format='png', dpi=300)
            plt.close()
        # 將summary保存到磁盤,以便tensorboard顯示
        with summary_writer.as_default():
            tf.summary.scalar("style_loss", style_loss_avg.result(), step=epoch)
        with summary_writer.as_default():
            tf.summary.scalar("content_loss", content_loss_avg.result(), step=epoch)
        with summary_writer.as_default():
            tf.summary.scalar("total_loss", epoch_loss_avg.result(), step=epoch)
        print("訓練次數:{},總體損失:{:.3f},風格損失:{:.3f},內容損失:{:.3f},訓練時間:{:.1f}min".format(epoch + 1,
            epoch_loss_avg.result(), style_loss_avg.result(), content_loss_avg.result(), time_cost))

if __name__ == "__main__":

    stamp = datetime.datetime.now().strftime("%Y%m%d-%H:%M:%S")
    img_content_path = "/Users/admin/Documents/111.jpeg"
    img_style_path = "/Users/admin/Documents/222.jpeg"
    img_content = image_read(img_content_path)
    print("內容圖像數據維度:", img_content.shape)
    img_style = image_read(img_style_path)
    print("風格圖像數據維度:", img_style.shape)
    pre_contents_layers, pre_styles_layers, num_style_layers, num_content_layers = layers_names()
    # 獲取風格圖像的預處理模型
    pre_model = pre_vgg16(pre_styles_layers)
    print(pre_model.summary())
    # 對輸入圖像進行處理,生成各層的特徵圖像,這裏由於img_style是float32的
    # 所以要乘以255轉成uint8的
    pre_styles = pre_model(img_style * 255)
    # 顯示各層的特徵圖像
    # pre_res_single(pre_styles_layers, pre_styles)
    vgg16_pre_model = Vgg16StyleContentModel(pre_styles_layers, pre_contents_layers)
    # 獲取風格圖像的"block1_conv1", "block2_conv1", "block3_conv1", "block4_conv1"
    # 各層的特徵圖像作爲待生成圖像的風格特徵
    style_targets = vgg16_pre_model(img_style)["style"]
    # 獲取內容圖像的block4_conv1層的特徵圖像作爲待生成圖像的內容特徵
    content_targets = vgg16_pre_model(img_content)["content"]
    input_img = tf.Variable(img_content)
    log_path = "/Users/admin/Documents/style-transfer" + stamp
    # 開始訓練
    train(log_path, input_img, style_targets, content_targets, vgg16_pre_model,
          num_style_layers, num_content_layers)
    # img1 = image_read("/Users/admin/Documents/01.jpeg")
    # img2 = image_read("/Users/admin/Documents/02.jpeg")
    # img3 = image_read("/Users/admin/Documents/03.jpeg")
    # pre_top_5_1 = vgg16_prediction(img1)
    # pre_top_5_2 = vgg16_prediction(img2)
    # pre_top_5_3 = vgg16_prediction(img3)
    # print("預測結果:", pre_top_5_1)
    # print("預測結果:", pre_top_5_2)
    # print("預測結果:", pre_top_5_3)

運行結果

內容圖像數據維度: (1, 1080, 1920, 3)
風格圖像數據維度: (1, 1080, 1920, 3)
Model: "model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         [(None, None, None, 3)]   0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, None, None, 64)    1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, None, None, 64)    36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, None, None, 64)    0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, None, None, 128)   73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, None, None, 128)   147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, None, None, 128)   0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, None, None, 256)   295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, None, None, 256)   590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, None, None, 256)   590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, None, None, 256)   0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, None, None, 512)   1180160   
=================================================================
Total params: 2,915,648
Trainable params: 0
Non-trainable params: 2,915,648
_________________________________________________________________
None
訓練次數:1,總體損失:2142520.500,風格損失:2142520.500,內容損失:0.000,訓練時間:0.5min
訓練次數:2,總體損失:215264576.000,風格損失:1656810.500,內容損失:213607760.000,訓練時間:0.3min
訓練次數:3,總體損失:154764064.000,風格損失:1669266.500,內容損失:153094800.000,訓練時間:0.3min
訓練次數:4,總體損失:148153136.000,風格損失:2167140.500,內容損失:145986000.000,訓練時間:0.3min
訓練次數:5,總體損失:147730752.000,風格損失:2233537.750,內容損失:145497216.000,訓練時間:0.4min

  第一次生成的圖像

第九次生成的圖像

經過對比,我們可以發現,第九次生成的圖像確實在學習風格圖像上有了一定的變化。

目標檢測

目標檢測問題定義

目標檢測是在圖片中對可變數量的目標進行查找和分類

  1. 目標種類與數量問題,目標種類可能是多個不同種類的物體,比如行人、車輛、機動車、非機動車、路標、紅綠燈等等各種不同種類的物體。在數量問題上也會有稠密和稀疏兩種不同場景下的目標檢測問題。
  2. 目標尺度問題。如果存在目標非常小且綢密的情況下,目標檢測的難度也比較大。
  3. 外在環境干擾。

目標檢測VS目標分類

 

上圖的左圖是經過目標檢測後的一副圖片,圖片標註出了需要檢測物體的座標位置,而框上有一個小數值,代表了檢測物體類別的置信度。而置信度也說明了我們需要查找的目標的一個概率值。通常我們會給出一個閾值,通過這個閾值來過濾掉一些檢測錯誤的目標。右圖是一個圖像分類的數據集。不管是圖像分類還是圖像檢測,我們在使用深度學習的技術來處理的時候,都需要完成特徵提取的環節。對於經典的傳統的機器學習來說,我們可能會設計一些手動的特徵來完成特徵提取這樣一個步驟。而深度學習,我們往往通過卷積神經網這樣一個方式來完成特徵的抽取

目標檢測VS目標分割

上圖中的a圖則表示了圖像分類,b圖表示了目標檢測,我們不僅要對目標進行分類還要定位到它所處的位置。目標分割不同與目標定位,目標分割需要找到當前的目標所佔的區域。c圖和d圖分別表示了兩種不同的分割方法,c圖表示了語義分割,d圖表示了實例分割。對於語義分割我們只需要找到同一類物體所佔的區域。實例分割比語義分割更加精細,它不僅要區分不同語義的物體,而且需要對於同一語義的物體,我們也需要劃分不同的實例。

目標檢測問題方法

傳統目標檢測方法到深度學習目標檢測方法的變遷

傳統的機器學習包括VJ、HOG、DPM等,這些方法遵循了手動設計特徵,並結合滑動窗口的方式來進行目標檢測和定位。這樣的方式存在的問題可以概括爲:

通過傳統方式手動設計特徵,往往特徵很難設計,設計出來的特徵會存在各種各樣的問題,比如對於某些特定條件不適應,設計出來的特徵會不魯棒。手動設計特徵效率也會存在瓶頸。通過滑動窗口的方式來提取目標框,通過目標框來對目標進行分類判定對策略同樣在提取滑動窗口的時候,整個流程也是非常繁瑣,非常慢,會比較耗時。因此在2008年DPM提出之後,目標檢測算法遇到了一個非常大的瓶頸。DPM之後除了一些級聯的方法在某些場景下會有一些性能的提升以外,基本上DPM在ImageNet上的數據集已經處於統治地位,傳統的目標檢測方法很難再往上提升。

2012年卷積神經網興起之後,利用卷積神經網來代替傳統手工設計特徵的方式並完成目標檢測的任務,這也是基於深度學習目標檢測的一個里程碑的事件。例如Overfeat和RCNN,這些方法在使用深度學習的時候,只是利用卷積神經網來進行特徵的提取,並沒有從本質上改變搜索框提取目標區域這樣一個策略。因此這些方法在速度上依然存在瓶頸。直到Fast RCNN這樣的方法提出以後,通過RPN網絡來代替原始的滑動窗口的策略,也標誌着基於深度學習的目標檢測方法徹底的完成了一個端到端的過程。整個過程只要通過一個網絡就能夠完成。這樣的結果就使得基於深度學習的目標檢測方法不僅在性能上得到了非常大的提升,而且在速度上同樣得到了非常大的提升。再到YOLO、SSD,不採用提取候選框的策略,採用直接回歸目標框的位置的策略,來完成目標檢測和定位。能夠再一次對目標檢測算法速度進行進一步的提升。而且能夠保證同原先的基於Proposal策略的檢測算法基本上一致的檢測精度。

這個時候基於深度學習的目標檢測算法分成了兩大類,一個就是包括Proposal這樣的一個目標檢測方法,另外一種就是直接回歸目標檢測位置的目標檢測方法。

基於傳統的或者基於深度學習的目標檢測方法,首先輸入一副圖像,通過候選框來進行特徵提取,然後利用分類器來進行判定,判定是否屬於目標或者背景。最後利用NMS來進行候選框的合併,最終得到目標的輸出結果。另外一種就是採用直接提取加目標框迴歸的方法來進行目標區域的提取,最後同樣利用NMS的策略來對目標框進行合併,得到最終的輸出結果。其中直接回歸的方式爲深度學習的方法,通過候選框的方式同樣也是深度學習的方法,但是這樣的一個框架同樣適用於傳統的目標檢測的方法。只不過是區別在於我們在進行目標提取的時候,是採用卷積神經網還是採用一些手動設計的特徵,比如說HOG特徵、積分圖特徵等等。再就是提取候選框的時候會有一些不同的策略。

  • 傳統目標檢測方法
  1. Viola-Jones,VJ算法主要是採用積分圖特徵結合AdaBoost這樣的一個分類器來進行人臉檢測這樣的一些目標檢測的任務。
  2. HOG+SVM,主要用於行人檢測這樣的一個任務。通過對行人目標候選區域取HOG特徵,並結合SVM分類器,來進行判定。
  3. DPM,同樣基於HOG特徵的一種變種,不同的地方在於在DPM中,會加入非常多的一些額外的策略來提升檢測的精度。DPM方法也是目標非深度學習效果或者性能最優的一種方法。但相對深度學習依然會存在較大的差距。但是DPM可以稱爲傳統的目標檢測算法的巔峯之作。在後續的深度學習目標檢測算法中會用到非常多的在DPM中提到的一些策略,比如說包圍框策略等等一些相應的概念。
  • 深度學習的目標檢測算法
  1. One-stage(YOLO和SSD系列),通過直接回歸目標的位置這樣的一種方法和策略來進行目標的檢測和定位。
  2. Two-stage(Faster RCNN系列),通過利用RPN網絡來對候選區域進行推薦。

 

  • 傳統和深度學習的比較
傳統目標檢測算法 深度學習目標檢測算法
手動設計特徵 深度網絡學習特徵
滑動窗口 Proposal或者直接回歸
傳統分類器 深度網絡
多步驟 端到端
準確度和實時性差 準確度高和實時性好

目標檢測的應用場景

在很多的場景,目標檢測算法作爲一個最基本的任務,爲後續的任務提供了一個最基本的支撐。比如說人臉檢測,能夠爲後續的性別、識別、刷臉支付等各種各樣的任務提供支撐。在ADAS(自動駕駛)場景中,需要對機動車、非機動車、行人等等各種目標來進行檢測,爲後續的跟蹤、異常行爲分析等等問題來提供基本的支撐。文本檢測對於文檔解析來說也是非常重要的基礎。自然場景下的一些通用物體的識別,這些通用物體的識別對於我們理解當下這幅圖片的內容是具有非常大的幫助的,因此對於圖像內容的理解提供了非常大的支撐。

再有比如說一些包、鞋子、褲子、服裝等一些圖像檢測可以用於商品搜索這樣的一個任務。在衛星圖像理解中同樣具有非常重要的地位。

傳統目標檢測基本流程

當我們使用滑動窗口對圖像進行特徵提取的時候,會對每一個窗口中的局部的圖像信息進行特徵抽取,特徵提取的方法通常會採用一些經典的計算機視覺模式識別中的關於圖像特徵表示的一些方法,常見的分別爲基於顏色的方法、基於紋理的方法、基於形狀的方法以及一些中層次或者高層次語義特徵的方法,這些方法有的是需要經過學習來得到的方法,比如說在抽取最基本的直方圖特徵以及常見的紋理特徵,如HOG特徵或者說Gamma特徵等一些稠密的特徵之後,會通過一個PCA算法進行特徵的降維。或者採用LDA算法來對特徵進行空間的投影來對抽取出的特徵來進行基本的學習,來挖掘一些更加魯棒的特徵出來。

因此對傳統的計算機視覺的特徵提取的方法往往會分爲三大類。第一類就是底層特徵,底層特徵就是顏色、紋理這些最基本的特徵。第二類就是中層次特徵,中層次特徵主要是指基於這些底層特徵進行機器學習的方法來進行特徵挖掘、特徵學習這樣的一個過程之後得到的特徵,這些特徵通常包括了比如說PCA特徵,LDA學習之後的特徵等等一些基於優化理論來完成的特徵的學習的過程。第三類是基於高層次的特徵,高層次的特徵通常是將低層次或者中層次的特徵進行進一步的挖掘和表示,將它們表示爲比如說對於一個人可以採用他是否戴帽子、是否戴眼鏡、頭髮的長短、所穿的服裝的樣式等等這樣的一些語義特徵來進行表示,這些特徵就被稱之爲高層次的特徵。當然我們在目標檢測的方法通常集中在低層次的特徵和中層次的特徵這兩種特徵中,就是說基於手工設計的特徵以及基於學習的特徵。

接下來就是對候選區域所提取出來的這些特徵,對於這些特徵來進行分類判定。這個分類器需要我們經過事先的學習和訓練得到。對於單類別目標的檢測,我們只需要區分當前的窗口中所包含的對象是否爲背景還是是否爲我們需要檢測的目標,這樣的一個二分類問題就可以了。對於多分類問題,我們需要去進一步的區分當前的窗口中是否爲背景。如果不爲背景的話,它屬於哪一類。也就是一個多分類的問題。我們在經過對候選框進行判定之後,就會得到一系列的可能爲當前的檢測目標的候選框,這些候選框可能會存在一些重疊的狀況,就需要一個NMS(非極大抑制)的算法,來對候選框進行合併,最後得到我們需要檢測的目標,是我們最終的算法輸出的結果。

常見傳統目標檢測方法

  • Viola-Jones(人臉檢測)
  1. Haar特徵抽取,Haar實際上也是紋理特徵的一種
  2.  訓練人臉分類器(Adaboost算法等),有關Adaboost算法可以參考機器學習算法整理(四)
  3. 滑動窗口,候選框依然採用滑動窗口的策略來進行提取,滑動窗口會存在滑動窗口的大小和滑動窗口的步長這樣的兩個問題。對於滑動窗口的問題通常會因爲尺度的變化和步長的變化會導致本身算法會出現大量的冗餘的候選框,這些冗餘的候選框會造成目標檢測算法性能或者說速度的下降。這些冗餘的候選框就成爲了目標檢測算法在速度上的一個比較重要的瓶頸。

Haar特徵(value=白-黑)

這些特徵的表現形式會表示成一個直方圖的方式。這個直方圖需要統計上圖中四種不同類別的積分圖特徵。對於不同種類的積分圖特徵,它的一個最原始的最飢餓的算法就表示爲對於每一個像素點,我們會計算出一個值,這個值就通過用白色的區域減去黑色的區域得到的結果來作爲我們需要表示的值,這個值也是我們統計直方圖時會用到的。第一種特徵表示相鄰的兩個像素點,在進行差的時候會對四個方向來進行差值。這四個方向分別爲0度,180度,45度和135度。將這四個方向的像素點對來作爲黑白兩個不同的像素點來進行差值求解這樣一個value。

上圖總共分成了4種類型的特徵,邊緣特徵,線性特徵,中心特徵和對角特徵。現在我們來看一下積分圖

積分圖主要的思想是將圖像從起點開始到各個點所形成的矩形區域像素之和作爲一個數組的元素保存在內存中,當要計算某個區域的像素和時可以直接索引數組的元素,不用重新計算這個區域的像素和,從而加快了計算(這有個相應的稱呼,叫做動態規劃算法)。積分圖能夠在多種尺度下,使用相同的時間(常數時間)來計算不同的特徵,因此大大提高了檢測速度。當滑動窗口在圖像上移動的時候,就可以隨時得到窗口內的矩形特徵值。

當VJ算法檢測到人臉圖像的時候

如上圖所示,會通過特徵積分圖分別求出黑色區域和白色區域的特徵像素值的和,然後再求白色區域和黑色區域像素值和的差。對於一副圖像,它會提取所有的這四種類型的Haar特徵,然後傳遞給Adaboost分類器。AdaBoost 將一系列的弱分類器通過線性組合,構成一個強分類器。弱分類器,其爲一個簡單的閾值函數:\theta _j爲閾值,s_j\in \left\{-1,1 \right\}\alpha _j爲係數。而所有這些弱分類器構成一個強分類器

Adaboost會對所有的這些不同類型的Haar特徵按照類型分組,然後再進行一個從小到大的排序,然後使用弱分類器,隨機選擇一個特徵值作爲閾值。把所有元素分爲兩部分,小於閾值的一部分分類爲人臉,大於閾值的一部分分類爲非人臉。如上圖所示,紅色圓圈數據代表人臉,藍色圓圈數據代表非人臉。由於是有監督學習,我們的訓練圖像事先是我們手工挑選並打上標籤的,並在初始時被分配一個相同的權值。每次進行分類都會有錯誤分類的數據作爲下一次分類的負樣本,並對正負樣本賦予不同的權值。當我們訓練好了一個adaboost模型後,就可以對新的圖像的掃描窗口中的圖像進行判別是否是人臉了。

級聯分類器

在正常的圖像中,人臉區域只是佔了很小的一部分,如果使用所有的特徵進行訓練的話,運算量非常大。

級爲了簡化任務,把若干個adaboost 分類器級聯起來,一開始使用少量的特徵將大部分的非人臉區域剔除掉,後面再利用更復雜的特徵將更復雜的非人臉區域剔除掉。

  • HOG+SVM(行人檢測)

HOG+SVM跟VJ是類似的傳統檢測方法,只不過它將Haar特徵換成了HOG特徵,Adaboost換成了SVM分類器

  1. 提取HOG特徵
  2. 訓練SVM分類器,有關SVM(支撐向量機)算法的內容請參考機器學習算法整理(三)
  3. 利用滑動窗口提取目標區域,進行分類判斷
  4. NMS
  5. 輸出檢測結果

HOG特徵,同樣是紋理特徵的一種

  1. 灰度化+Gamma變換,HOG特徵主要用在灰度圖上,然後進行Gamma變換,Gamma變換就是對像素值進行根號求解,整個像素值就會變小,實際上也是數據進行一個平滑。
  2. 計算梯度map
  3. 圖像劃分成小的cell,統計每個cell梯度直方圖
  4. 多個cell組成一個block,特徵歸一化
  5. 多個block串聯,並歸一化

梯度分爲橫向梯度和縱向梯度,設爲(fx,fy)fx指的是一個像素在橫向相鄰的兩個像素的差值,縱向梯度fy也是一樣。

比如在這幅圖像上的一個像素點相鄰的橫向項素值爲70、120;縱向像素值爲100、50。則fx=120-70=50;fy​=100-50=50;則該像素點的梯度大小爲=,而且該像素點梯度的方向爲arctan(fy​/fx)=arctan(50/50)=45°。

我們以圖像上的一個8*8的區域來看

這裏每一個格子都代表一個像素的HOG特徵,然後組合成一個Cell,每個Cell有64個特徵向量,然後將這64個特徵按照特徵向量的角度分別放入一個直方圖中

這裏的橫座標代表角度的範圍,但其實應該有18個分段,代表0~360度,而上圖只有9個分段,只到180度。每個HOG特徵向量根據自身的角度放入到這個直方圖的不同位置中,而縱座標代表HOG特徵向量大小的累加。也就是說每個Cell有18維的特徵。然後每4個Cell構成一個block,這樣就變成了4*18=72維。雖然這四個Cell的維度沒有變化,但組合的意義在於特徵歸一化,能夠使特徵對一些峯值的變化會更加的魯棒。然後將多個block進行串聯,再經過歸一化處理得到最終的HOG特徵。實際上HOG特徵的特徵維度通常跟量化的角度,量化的程度有關係。另外就是跟Cell的大小有關。如果Cell的大小越小,那麼對於每一個Feature Map,我們所得到的Cell的數量就越多,梯度直方圖中向量的維度也會越長,HOG特徵的維度也就越大。HOG特徵往往會在幾千維的維度。特徵向量的維度很高帶來的問題就是在直接用HOG特徵進行計算的時候,計算難度會加大。因此我們在使用HOG特徵的時候會採用PCA降維的方法來進行降維。這樣的思想在DPM算法中也會被用到。

在提取HOG特徵之後會使用SVM分類器來對行人和背景進行建模。SVM這裏就不詳細介紹了。

  • NMS(非極大值抑制算法)

  1. 目的:爲了消除多餘的框,找到最佳的物體檢測的位置。
  2. 思想:選取那些鄰域裏分數最高的窗口,同時抑制那些分數低的窗口
  3. Soft-NMS

噹噹前的檢測框同得分最高的檢測框M,它們的iou大於某一個閾值的時候,我們就直接將它們的分數置爲0,將當前的檢測框進行刪除。

Soft-NMS(非極大值抑制算法)

  1. 相鄰區域內的檢測框的分數進行調整而非徹底抑制,從而提高了高檢索率情況下的準確率
  2. 在低檢索率時仍能對物體檢測性能有明顯提升

噹噹前的檢測框同得分最高的檢測框M,它們的iou大於某一個閾值的時候,我們會用1減去該值,並對它們的分數進行更新。實際上Soft-NMS是在深度學習目標檢測中提出的一個小的技巧。

  • Two-stage基本流程

首先我們會輸入一幅圖片,然後通過一個卷積神經網絡進行深度特徵的提取,我們稱這個卷機神經網爲主幹網絡,包括了VGG、ResNet等。再然後我們會通過一個RPN網絡去完成之前滑動窗口所完成的任務,產生候選區域。

並且在進行候選框區域提取的時候,會完成一個對候選框區域的分類,這個分類的過程就將候選區域分爲背景目標兩種不同的類別。並且在RPN網絡產生候選區域的時候,會對目標的位置進行初步的預測,也就是說區域分類和位置精修兩個環節。然後通過roi_pooling層進行位置的精確的迴歸和修正。我們可以將roi_pooling層理解爲摳圖的意思。對於一個feature maps,我們想要不去重複的計算CNN特徵,往往就需要一個roi_pooling層來完成一個摳圖的操作。接下來我們得到候選目標對應到feature maps上的一段區域,或者說特徵之後,我們會通過一個fc層(全連接層)來進一步對候選區域的特徵進行一層表示。再通過分類和迴歸兩個分支來分別完成對候選目標它的類別的判定以及對候選目標位置的精修。這裏的類別不同於RPN網絡的類別,這裏通常會得到物體真實的類別。比如說對於VOC數據集我們通常會去判別21個類別。對於一些單詞檢測,我們可能只會用到兩個類別。對於Coco數據集,我們可能會有一些更多的類別去進行目標的分類。 而回歸則主要得到當前物體具體的座標位置。實際上具體的座標位置會表示爲一個矩形框。

Two-stage常見算法

RCNN、Fast RCNN、Faster RCNN、Faster RCNN變種。

這些變種分別對應了主幹網絡,RPN網絡,ROI_Pooling策略,數據樣本後處理,更大的mini-Batch來對Faster RCNN進行改進。

  • Two-stage核心組件

CNN主幹網可以參考Tensorflow深度學習算法整理 ,這裏不再贅述

RPN網絡

  1. 區域推薦(Anchor機制)
  2. ROI Pooling
  3. 分類和迴歸

在上圖中,我們可以看到,圖片經過主幹網絡之後,提取出了feature maps,feature maps通常爲n、c、w、h四維的一個卷積特徵,n爲batch size的數量,也就是當前處理樣本的數量;c表示了feature maps的個數,也就是channl通道數;w、h代表了feature maps的寬和高,這個寬和高可能會和原始圖片的寬和高相差一定的倍數。產生的原因一方面當進行卷積操作的時候可能會產生一些寬高的損失,這些損失的前提是基於沒有使用padding的策略;另一方面是在進行池化層操作的時候也會對圖像採用下采樣,來得到更大的感受野,但是會縮小圖片的尺寸。再就是這裏的c是對應到了不同的卷積核。

通過卷積之後得到的feature maps之後,會通過一個RPN(Region Proposal Network)網絡來完成區域推薦和候選目標的篩選。這個步驟就相當於是我們在利用傳統的目標檢測算法進行目標檢測時會用到的滑動窗口的策略,這裏需要我們重點去了解的是RPN網絡的區域推薦算法(Anchor機制)。對候選區域篩選之後,會利用ROI Pooling來提取一個候選目標,然後通過分類和迴歸網絡對候選區域進行一個精確的分類和座標位置的迴歸

區域推薦(Anchor機制)

我們對於當前的feature map,它的大小爲n*c*w*h,Anchor是對feature map的w*h的範圍內的每一個像素點來作爲一個錨點,這個錨點就是候選區域的中心點。我們以每一個點作爲中心點去提取候選區域,這樣的每一個點都稱之爲一個Anchor。當我們以這個點爲中心去提取候選區域,通常會按照一定比例去提取候選區域。對於Faster RCNN會使用9個尺度來提取候選區域,換句話說對於一個feature map會提取出w*h*9個候選區域。提取出來之後,會通過針對於候選區域和真值(GT)來對候選區域進行篩選,經過篩選之後得到正樣本和負樣本,其中正樣本就是包含了候選目標的區域,負樣本就表示爲不包含候選目標的區域。候選區域是否包含候選目標是通過IOU來進行判定真值和候選區域的重疊面積,如果候選區域同真值的覆蓋面積超過了0.7的話,這個時候這個候選區域會被認爲是一個正樣本;如果小於0.3的話會被認爲是一個負樣本。這裏的0.7和0.3是一組超參數,是可以被人爲設置的。對於0.3~0.7中間這一段區域的內容通常不會用來訓練RPN網絡分類器,也就是說對於區分目標和背景的時候,這一段我們是不用的。

當前的feature map大小爲w*h,而真值候選區域目標的座標是針對於原始的圖像來講的。而當前的feature map同真值的候選區域的重疊面積會用到卷積神經網絡中的池化層,當池化層在對圖像進行一定的下采樣的時候實際上也是在按照一定的倍數在進行下采樣。對於原始圖片中的目標區域可以通過池化層的下采樣的過程來找到在當前的feature map上所對應的真值的候選區域。比如說我們使用池化層對原始圖像8倍進行了下采樣,也就是說feature map也就是原始圖像的1/8。這個時候原始圖像中的目標區域的大小也會下采樣1/8。它的座標的位置可以理解成一個相對的位置。我們在計算IOU的時候會根據這樣一個相對位置來進行計算。

ROI Pooling

  1. 輸入:特徵圖、rois(1*5*1*1)以及ROI參數
  2. 輸出:固定尺寸的feature map

ROI Pooling實際上是RPN網絡中的一個層,對於這個層的輸入包括了特徵圖也就是feature map,rois(1*5*1*1)區域座標以及ROI參數,這個參數比如說池化層的下采樣是原始圖像的8倍,那麼當前的feature map爲原始圖像的1/8,那麼這個參數的值就是1/8,表示當前的feature map爲原始圖像的1/8,在計算區域的時候就會按照1/8的比例去摳圖。而rois的第二維度爲什麼是5的原因是它不僅包含了一個區域的矩形框的x、y、w、h之外,還包含了一個當前的roi信息所對應到的當前的batch size中的哪一個圖片,實際上這是一個索引i,而這個i是放在這5個值的第一個位置的。我們在進行網絡訓練的時候通常不會只處理一張圖,我們可能會處理n張圖,對於這個batch size的數據,我們需要知道對於每一個roi所對應的特徵圖是哪一張。我們對於一個特徵圖可能會包括多個目標,多個roi可能會指向一個feature map,這個時候就需要一個索引值來標示當前的roi的信息屬於哪一個特徵圖的信息。在經過ROI Pooling之後會輸出一個固定尺寸的feature map,這個固定尺寸的大小也通過ROI的參數來設定。比如ROI參數設定爲8*8,那麼這個輸出的尺寸就爲m*c*8*8,這個m是roi的個數,c是通道數。ROI Pooling可以理解爲摳圖+resize的操作。摳圖就是將目標區域給摳出來,而resize是因爲對於下一步的fc全連接層需要一個統一尺寸的大小作爲輸入參數,否則會出錯,所以需要將feature map統一到一個相同的尺寸大小。

  • One-stage基本流程

對於One-stage目標檢測算法,在給定一個圖像的輸入,再通過一個主幹網絡來完成CNN特徵的抽取,再接下來會完成區域的迴歸和目標的分類。整個過程就可以概括爲

相對於Two-stage,我們會發現這裏少了RPN網絡,它們最大的區別就在於是否包含了候選區域目標推薦的這樣一個過程。從這個流程上來看,這個流程更加簡單,速度更加快。在很多嵌入式產品中可能會更加的傾向於One-stage目標檢測算法。當然我們在優先考慮速度的時候就可能會損失部分精度。這個也是很難去避免的問題。

One-stage常見算法

YoloV1/V2/V3、SSD/DSSD等、Retina-Net等等

  • One-stage核心組件
  1. CNN網絡——主幹網絡
  2. 迴歸網絡,One-stage核心的創新點就體現在了迴歸網絡上。在SSD和YOLO上設計迴歸網絡會有一些相應的技巧。

CNN網絡(略)

迴歸網絡

  1. 區域迴歸(置信度、位置、類別)
  2. Anchor機制(SSD)

迴歸網絡主要是在卷積特徵提取之後得到的feature map來作爲迴歸網絡的輸入,在迴歸網絡中主要是完成了區域迴歸和目標區域類別的判定,區域迴歸主要是指通過迴歸網絡直接輸出最終目標的Bounding Box的位置信息。

在上圖的左圖中,紅框和藍框都是我們要求的位置信息,也就是區域迴歸最終的輸出結果,這個區域信息也是我們整個迴歸網絡最終想要得到的目標,所以迴歸網絡屬於One-stage檢測目標信息中的最重要的核心組件。除了得到位置信息以外,還會得到置信度和類別,置信度表達了當前的Bounding Box中是否存在目標,如果存在目標就是一個目標區域,如果不存在目標就是一個背景區域。

在上圖的右圖中,是一個YOLO算法的示意圖,在YOLO算法中,我們可以看到整個圖片被分成了一個一個非常小的格子,這裏每一個小的格子都會進行區域迴歸來輸出當前的這個格子所對應的目標區域的位置信息、置信度和類別。而置信度,我們可以看到對於給出的紅色的區域和黃色區域外的任意一個小塊區域,很明顯這個紅色區域屬於目標的一個區域,對比紅色區域和黃色區域外的任意一個小塊區域,明顯紅色區域會比那些區域的置信度更高。而類別,紅色區域屬於這隻狗所對應的一個子區域,而黃色區域外的一個小塊區域則屬於背景區域,所以我們對於紅色區域和其他這些區域在進行類別迴歸的時候,紅色區域所得到的概率分佈上,狗所對應的類別所對應的概率分佈的值就會更高。而黃色區域外的一小塊區域則是背景所對應的概率值會更高。除去區域迴歸這個核心組件之外,通常還會利用Anchor機制同樣去找到不同的推薦區域,Anchor機制屬於RPN網絡的一個核心組件。One-stage和Two-stage最主要的區別就在於是否存在RPN網絡,在One-stage整個算法中是沒有推薦區域提取過程的,但是這並不影響它使用Two-stage目標檢測算法中的一些核心的思想,比如說Anchor機制,在SSD算法中有着非常重要的應用。

在上圖的左圖中也提供了在SSD中目標迴歸中的Anchor機制所對應的示意圖,它同樣通過Anchor機制來回歸目標區域。對於主幹網絡經過卷積之後所得到的feature map,考慮feature map中的每一個點都是一個Anchor,基於這個Anchor來提取不同尺度的長寬比,對於不同尺度的長寬比所對應的目標區域來利用這個目標區域進行位置的迴歸和類別的判定。Anchor機制是SSD算法的一個核心。不同於SSD算法的YOLO算法的V1算法中是不存在Anchor機制的。它的整個思路是針對於圖片的格子的劃分,然後對於每個格子來分別進行區域的迴歸。這個格子除了要得出右圖中的5個值之外還包含一個置信度的值,置信度的值代表了這個格子是否包含了目標區域的值,包含和不包含就是一個概率的值。置信度的值越偏向於0,就越傾向於不包含,置信度的值越傾向於1則越傾向於包含。當然在YOLO V3算法中同樣使用了Anchor機制來進行了改進。

迴歸網絡預測過程(Yolo)

Yolo網絡對於輸入的圖片進行整張圖片的劃分,針對於每一個網格來分別預測當前這個網格爲中心的目標區域的位置信息。在上圖中的中間的上面這幅圖中,我們可以看到有非常多的Bounding box的矩形框,這些矩形框實際上就對應到了輸入的這個s*s的網格狀的圖像,其中的每一個格子所預測出來的帶檢索區域的位置,此時我們認爲當前的這個格子爲矩形框的中心,再預測出這個格子的寬和高,就能在圖片中找到一個檢測區域。

除了預測出檢測區域外還會預測出一個置信度,描述了當前的格子中,是否存在目標的一個概率值。如果這個概率值接近1的話,說明這個格子所預測出來的Bounding box所包含待檢測目標的可能性就會越高。如果越趨向於0,當前的Bounding box所包含目標區域的可能性就會越低。除了每個格子所預測出來的區域和置信度以外,還會針對於每一個格子來預測出當前的格子所屬的目標的類別的概率分佈值,也就是上圖中間部分下面的圖的不同顏色格子的一副圖片。這裏我們可以看到同一個類別所對應的格子的顏色是同一個值,如果說這裏的類別數爲c類,Yolo算法中每一個格子最終迴歸出來的值就是——置信度+座標(4個值)+類別c這樣一個長度的向量。值得注意的是,在Yolo中,每一個格子會預測出B個Bounding box以及對應的置信度的值,所以整幅圖片最終就是——[B*(置信度+座標(4個值))+類別c]*s*s,也就是迴歸網絡迴歸出來的結果。這裏s*s就是一副圖片包含的格子的總數量。

在拿到最終輸出之後再利用每一個網格預測的類別信息和Bounding box所對應的置信度信息就能夠獲得每一個Bounding box所對應的類別置信度信息。利用類別置信度信息再結合NMS算法對預測出的所有的Bounding box再進行篩選和過濾得到最終預測的結果。對於SSD和Faster RCNN這樣的檢測網絡而言,最終輸出的Bouning box本身所預測出的概率分佈實際上就可以直接用於NMS算法所需要的類別置信度分數。在Yolo算法中需要額外處理的一步就是將上圖中間上下兩個圖的結果進行融合,將兩個置信度進行相乘得到最終的分數置信度作爲NMS的輸入。

相比於Faster RCNN和SSD來說,Yolo算法是一個純粹的端到端的迴歸網絡,整個算法的流程相對而言也是更加的簡單。因此Yolo算法比SSD算法檢測的效率會更高。但由於在Yolo算法中進行了格子的劃分,認爲每一個格子點都是目標檢測區域的中心點,有可能我們劃分的格子都不是目標的中心點,因此基於中心點來預測目標區域所對應的Bounding box的信息前提假設會導致最終預測出來的檢測框相比於SSD和Faster RCNN而言會更加的低。在Yolo算法中在劃分格子的時候,可能會忽略掉其中的小物體,比如說在檢測鳥羣的時候,一個格子中可能會包含兩隻鳥,這個時候在檢測的時候會產生漏檢的情況,這也是Yolo算法比不上SSD算法和Faster RCNN算法的一個主要原因。但是Yolo算法整體的檢測速度非常快,尤其是在一些實時任務中,Yolo算法具有它獨特的優勢。

  • One-stage與Two-stage優缺點對比
One-stage One-stage缺點 Two-stage優點 Two-stage缺點
速度快 精度低(定位、檢出率) 精度高(定位、檢出率) 速度慢
避免背景錯誤,產生false positives 小物體的檢出效果不好 Anchor機制 訓練時間長
學到物體的泛化特徵   共享計算量 誤報高

具體的算法選型,通常會依賴於我們計算的瓶頸,在產品中會分配多少資源用於目標檢測。如果很難做到資源的任意分配,我們需要平衡算法的性能以及算法模型大小,功耗等等。這樣我們就需要對這兩種算法有一個更加詳細的瞭解。在一些終端產品上用One-stage目標檢測算法會多一點,對於一些雲端的產品可能會更多考慮Two-stage,這樣可以做到更高的性能,更高的準確度。在終端上會考慮更多的功耗,更輕量級的網絡,考慮計算量更小。對於不同的應用會選用不同的算法。

SSD系列算法

算法原理介紹

  1. 主幹網絡:VGGNet、ResNet、MobileNets
  2. 將VGG最後兩個FC改成卷積,並增加4個卷積層。
  3. 多尺度Feature Map預測
  4. Default bounding boxes的類別分數、偏移量

首先SSD網絡會包括一個主幹網絡,上圖中爲VGG-16,原始圖像(300*300)作用到了第5個尺度的第3個卷積層的結果。它的輸出結果也作爲接下來的幾個卷積層的輸入。而多尺度的預測是指對接下來的6個不同的尺寸的feature map進行預測。在上圖中,我們可以看到它的第一個卷積層的feature map的尺寸爲38*38。接下來是19*19,再就是10*10,這裏從19*19到10*10會通過一個下采樣的操作來完成下采樣,但是我們在進行下采樣的時候會採用padding來進行補充。再接下來的幾個尺度分別爲5*5、3*3、1*1。這樣6個不同的尺度會分別作爲檢測預測層的輸入,最後通過NMS來對檢測結果進行合併和篩選。

這裏通過對每一層在檢測預測層進行Default bounding boxes的提取,而Default bounding box的獲取就涉及到了Anchor機制。我們如何採用多個尺度的預測以及每個尺度上提取多少個Default bounding box,這些都是在SSD相應的配置文件裏來進行配置。具體配置也是通過相應層的name來完成配置,每個層的name會作爲尺度參數的輸入來決定哪一個層來作爲後續預測的輸入層。

  • 多尺度Feature Map預測
  1. 不同層的feature map
  2. Prior box:類別概率和座標(x,y,w,h)

不同尺度的feature map通常會使用pooling的方法來進行下采樣,對於每一層的featue map會輸入到相應的預測網絡中,在預測網絡中會包括Prior box層的提取過程。這裏的Prior box對應到了在Faster RCNN中Anchor的概念,也就是說對於每個層的feature map它的相對應的一個點,我們都會將它作爲一個Cell,這樣一個Cell,我們也會將它命名爲一個Anchor,以這個Cell爲中心會通過等比的放縮方法來找到它在原始圖像中的位置,並且以這個點爲中心來提取不同尺度的bounding box,這些不同尺度的bounding box則定義爲Prior box。對於每一個Prior box通過和真值的比較就能夠拿到它的Label。對於每一個Prior box,我們會分別預測它相應的類別的概率和座標值。對於每一個Cell(或Anchor、或者feature map中的一個點),我們會將它對應到不同的Prior box(Anchor與Prior box是一對多的關係),分別來預測它所對應的類別的概率分佈和座標值。假設我們預測出的類別爲c類,則最終的輸出爲c+4的維度輸出。假設feature map的大小爲m*n的話,每一個Anchor都提取出k個Prior box,則整個feature map的輸出爲m*n*k*(c+4)

  • Prior Box Layer
  1. 每個feature map有m*n個Cell
  2. 每個Cell上生成固定scale(尺寸)和aspect ratio(長寬比率)的box,每個Cell對應k個default box。

Scale:

這裏是尺寸的一個計算公式,其中=0.2,代表最底層的Scale爲0.2;=0.9,代表最高層的scale爲0.9,代入上面的公式中就可以得到各個不同的bounding box的尺寸。

aspect ratio:

具體的長寬比率在原始的SSD中主要採用5個這樣的比率。當aspect ratio=1的時候,增加一種scale的default box:,也就是說每個feature map cell定義6種default box。

最終的寬的尺寸,高的尺寸:

實際上對於feature maps爲38*38的時候,每一個點會提取4個Prior Box;19*19的時候,會提取6個,10*10的時候會提取6個,5*5的時候會提取6個,3*3的時候會提取4個,1*1的時候會提取4個。最終總的Prior Box的數量就爲38*38*4+19*19*6+10*10*6+5*5*6+3*3*4+1*1*4=8732個prior box。具體提取多少個prior box以及我們需要如何定義尺寸的大小以及長寬比率,通常我們也會在相應的配置文件中來進行配置。

對於Prior Box,shape(形狀)數量越多,效果越好。

在上圖中,我們可以看到,其實SSD的Prior Box層,它整個過程跟Faster RCNN的Anchor機制的原理是保持一致的。對於每一個提取出來的Prior Box,都會預測出座標的偏移以及在不同類別上的概率分佈。

每一個feature map cell不是k個default box都取,我們在實際計算的時候並不會對每一個prior box都去計算一個loss,會通過一定的策略來對prior box來進行一定的篩選,簡單來說就是使用GT box真值來對prior box來進行一定的篩選,如果IOU>閾值,作爲正樣本;對於IOU<負樣本閾值的作爲負樣本;作爲正負樣本閾值中間的部分將其拋棄掉。在訓練的過程中確保prior box的分類準確且儘可能迴歸到GT box真值。

  • 樣本構造
  1. 正樣本:從GT box出發找到最匹配的prior box放入候選正樣本集;從prior box集出發,尋找與GT box滿足IOU>0.5的最大prior box放入候選正樣本集。
  2. 負樣本:對於不滿足正樣本條件的樣本可以作爲負樣本。在訓練網絡模型的時候需要儘可能去挖掘一些難例,正負樣本比儘量在1:3。我們需要對prior box中的負樣本同樣進行篩選,如果當前的prior box同真值IOU閾值<定義的負樣本閾值,此時將這些樣本作爲最終的負樣本。通過這種簡單的策略來找到更加明確的負樣本。
  • 難例挖掘

我們在難例挖掘的時候通常會採用訓練的這樣一個模型來進行分析,首先會使用預測的loss來對prior box進行排序,通過預測的loss來排序當前prior box集合,選擇最高的幾個prior box同正樣本進行匹配,如果匹配成功就將這幾個prior box放到正樣本中。負樣本也是同樣。

  • 數據增強
  1. 隨機採樣多個path(區域),與物體之間最小的jaccard overlap(IOU)爲:0.1,0.3,0.5,0.7與0.9。
  2. 採樣的path比例是[0.3,1.0],aspect ratio在0.5或2.
  3. GT box中心在採樣path中且面積大於0,以上3條都是在說明採樣的區域一定要與真值區域有交集,而且交集的面積不能太小
  4. Resize到固定大小
  5. 以0.5的概率隨機的水平翻轉
  • 損失函數
  1. 分類loss+迴歸loss
  2. 分類loss:Softmax Loss
  3. 迴歸loss:Smooth L1 Loss

對於每一個prior box最終會預測出一個類別以及座標偏移量,SSD算法的損失函數則包括了兩個部分,它包括了置信度誤差和位置誤差的加權和。

  • SSD系列網絡結構

在上圖的網絡結構中,主幹網絡爲

這一部分,它輸出的feature maps會輸入給用於分類和迴歸的網絡爲

而Prior Box層則定義在這裏

在總圖上,我們可以看到主幹網絡的feature maps輸出會作爲Prior Box層的輸入,而真值也會作爲Prior Box層的輸入。而Prior Box層的輸出則會用於後續Loss層的計算。最終我們計算Loss的時候的維度會同Prior Box層保持一致。也就是說我們提取了多少的Prior Box,在經過卷積之後輸入到Loss層的時候,需要保持兩者的一致。

  • 損失函數的計算

基本思路:讓每一個prior box迴歸到GT box,這個過程的調控我們需要損失層的幫助,它會計算真實值和預測值之間的誤差,從而指導學習的走向。

  • 使用細節
  1. 數據增強時,Crop(關注)採樣大小
  2. 多任務網絡(分類任務+迴歸任務)的權重。
  3. 正負樣本比例,通常採用1:3的比例。
  4. 難例挖掘方式默認只取64個最高predictions loss來從中尋找負樣本。

現在我們來看一下SSD在幾個不同的圖像測試集上的表現

SSD算法在2016年被提出,在對VOC2007數據集進行測試,當時在比較Faster RCNN的時候,可以看到SSD會有一些性能上的提升,尤其是在輸入尺寸變大的時候,在輸入尺寸爲500*500的時候,它的mAP值達到了75.1,在輸入尺寸爲300*300的時候,相對於Faster RCNN而言會稍微低1個百分點。這裏我們就會發現對於輸入尺寸越大,SSD對應的mAP值也會越高。

在對VOC2012數據集進行測試的時候,YOLO算法的mAP值只有57.9,而在300*300的輸入尺寸下,SSD只比Faster RCNN低了0.1的百分點,在500*500尺寸下達到了73.1。

在COCO test-dev2015的數據集上,在500*500的尺寸下,SSD相對於Faster RCNN會有1%的提升。但在300*300下會有所降低。

從另外一個指標FPS(運行速度)而言,相對於YOLO算法而言,SSD算法會有所降低,也就是說SSD的速度會有所損失。但是相比於Faster RCNN而言,在整體的速度上有了一個較大的提升。整體的mAP的值也有一個較大性能上的提升。

同樣對比SSD算法的不同模塊,我們可以發現

  1. 數據增強對於結果的提升非常明顯
  2. 使用更多的feature maps對結果提升更大
  3. 使用更多的default box,結果也越好
  4. Atrous(空洞卷積,不採用pooling的時候,我們如何去增大卷積核的感受野,這裏採用了帶孔的卷積)使得SSD又好又快。

對於SSD算法而言,圖像尺寸越大它的mAP越好,但也意味着檢測速度的下降。在具體選型的時候需要綜合考量這兩個不同的指標。

Tensorflow+SSD實戰

Tensorflow+SSD環境搭建

先在tensorflow的github官網https://github.com/tensorflow/models將代碼拉取下來。

遵照安裝步驟進行安裝https://github.com/tensorflow/models/blob/f87a58cd96d45de73c9a8330a06b2ab56749a7fa/research/object_detection/g3doc/installation.md

代碼拉取下來後進入models-master/research目錄下

cd models-master/research
# 將setup.py拷貝到該目錄下
cp object_detection/packages/tf2/setup.py ./
python setup.py build
python setup.py install
pip install tf_slim
# 編譯protobuffer文件成py
protoc object_detection/protos/*.proto --python_out=.
# 將slim路徑添加到環境變量
export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/slim
# 進行安裝測試
python object_detection/builders/model_builder_tf2_test.py 

運行完最後一個命令會打印一堆日誌。另外會用到谷歌的protobuffer序列化的相關內容,可以參考Netty整合Protobuffer

Running tests under Python 3.7.6: /opt/anaconda3/bin/python
[ RUN      ] ModelBuilderTF2Test.test_create_center_net_deepmac
2021-12-02 02:30:38.381300: I tensorflow/core/platform/cpu_feature_guard.cc:142] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  AVX2 FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
/Users/admin/Downloads/models-master/research/object_detection/builders/model_builder.py:1100: DeprecationWarning: The 'warn' function is deprecated, use 'warning' instead
  logging.warn(('Building experimental DeepMAC meta-arch.'
W1202 02:30:38.568902 4583968192 model_builder.py:1100] Building experimental DeepMAC meta-arch. Some features may be omitted.
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_create_center_net_deepmac): 0.48s
I1202 02:30:38.849614 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_create_center_net_deepmac): 0.48s
[       OK ] ModelBuilderTF2Test.test_create_center_net_deepmac
[ RUN      ] ModelBuilderTF2Test.test_create_center_net_model0 (customize_head_params=True)
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_create_center_net_model0 (customize_head_params=True)): 0.52s
I1202 02:30:39.365669 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_create_center_net_model0 (customize_head_params=True)): 0.52s
[       OK ] ModelBuilderTF2Test.test_create_center_net_model0 (customize_head_params=True)
[ RUN      ] ModelBuilderTF2Test.test_create_center_net_model1 (customize_head_params=False)
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_create_center_net_model1 (customize_head_params=False)): 0.2s
I1202 02:30:39.568368 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_create_center_net_model1 (customize_head_params=False)): 0.2s
[       OK ] ModelBuilderTF2Test.test_create_center_net_model1 (customize_head_params=False)
[ RUN      ] ModelBuilderTF2Test.test_create_center_net_model_from_keypoints
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_create_center_net_model_from_keypoints): 0.18s
I1202 02:30:39.749634 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_create_center_net_model_from_keypoints): 0.18s
[       OK ] ModelBuilderTF2Test.test_create_center_net_model_from_keypoints
[ RUN      ] ModelBuilderTF2Test.test_create_center_net_model_mobilenet
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_create_center_net_model_mobilenet): 1.26s
I1202 02:30:41.005450 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_create_center_net_model_mobilenet): 1.26s
[       OK ] ModelBuilderTF2Test.test_create_center_net_model_mobilenet
[ RUN      ] ModelBuilderTF2Test.test_create_experimental_model
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_create_experimental_model): 0.0s
I1202 02:30:41.006211 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_create_experimental_model): 0.0s
[       OK ] ModelBuilderTF2Test.test_create_experimental_model
[ RUN      ] ModelBuilderTF2Test.test_create_faster_rcnn_from_config_with_crop_feature0 (True)
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_create_faster_rcnn_from_config_with_crop_feature0 (True)): 0.02s
I1202 02:30:41.022876 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_create_faster_rcnn_from_config_with_crop_feature0 (True)): 0.02s
[       OK ] ModelBuilderTF2Test.test_create_faster_rcnn_from_config_with_crop_feature0 (True)
[ RUN      ] ModelBuilderTF2Test.test_create_faster_rcnn_from_config_with_crop_feature1 (False)
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_create_faster_rcnn_from_config_with_crop_feature1 (False)): 0.01s
I1202 02:30:41.033704 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_create_faster_rcnn_from_config_with_crop_feature1 (False)): 0.01s
[       OK ] ModelBuilderTF2Test.test_create_faster_rcnn_from_config_with_crop_feature1 (False)
[ RUN      ] ModelBuilderTF2Test.test_create_faster_rcnn_model_from_config_with_example_miner
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_create_faster_rcnn_model_from_config_with_example_miner): 0.01s
I1202 02:30:41.045653 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_create_faster_rcnn_model_from_config_with_example_miner): 0.01s
[       OK ] ModelBuilderTF2Test.test_create_faster_rcnn_model_from_config_with_example_miner
[ RUN      ] ModelBuilderTF2Test.test_create_faster_rcnn_models_from_config_faster_rcnn_with_matmul
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_create_faster_rcnn_models_from_config_faster_rcnn_with_matmul): 0.07s
I1202 02:30:41.119521 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_create_faster_rcnn_models_from_config_faster_rcnn_with_matmul): 0.07s
[       OK ] ModelBuilderTF2Test.test_create_faster_rcnn_models_from_config_faster_rcnn_with_matmul
[ RUN      ] ModelBuilderTF2Test.test_create_faster_rcnn_models_from_config_faster_rcnn_without_matmul
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_create_faster_rcnn_models_from_config_faster_rcnn_without_matmul): 0.07s
I1202 02:30:41.188717 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_create_faster_rcnn_models_from_config_faster_rcnn_without_matmul): 0.07s
[       OK ] ModelBuilderTF2Test.test_create_faster_rcnn_models_from_config_faster_rcnn_without_matmul
[ RUN      ] ModelBuilderTF2Test.test_create_faster_rcnn_models_from_config_mask_rcnn_with_matmul
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_create_faster_rcnn_models_from_config_mask_rcnn_with_matmul): 0.07s
I1202 02:30:41.262683 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_create_faster_rcnn_models_from_config_mask_rcnn_with_matmul): 0.07s
[       OK ] ModelBuilderTF2Test.test_create_faster_rcnn_models_from_config_mask_rcnn_with_matmul
[ RUN      ] ModelBuilderTF2Test.test_create_faster_rcnn_models_from_config_mask_rcnn_without_matmul
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_create_faster_rcnn_models_from_config_mask_rcnn_without_matmul): 0.07s
I1202 02:30:41.334958 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_create_faster_rcnn_models_from_config_mask_rcnn_without_matmul): 0.07s
[       OK ] ModelBuilderTF2Test.test_create_faster_rcnn_models_from_config_mask_rcnn_without_matmul
[ RUN      ] ModelBuilderTF2Test.test_create_rfcn_model_from_config
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_create_rfcn_model_from_config): 0.07s
I1202 02:30:41.404987 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_create_rfcn_model_from_config): 0.07s
[       OK ] ModelBuilderTF2Test.test_create_rfcn_model_from_config
[ RUN      ] ModelBuilderTF2Test.test_create_ssd_fpn_model_from_config
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_create_ssd_fpn_model_from_config): 0.02s
I1202 02:30:41.424791 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_create_ssd_fpn_model_from_config): 0.02s
[       OK ] ModelBuilderTF2Test.test_create_ssd_fpn_model_from_config
[ RUN      ] ModelBuilderTF2Test.test_create_ssd_models_from_config
I1202 02:30:41.553299 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:143] EfficientDet EfficientNet backbone version: efficientnet-b0
I1202 02:30:41.553393 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:144] EfficientDet BiFPN num filters: 64
I1202 02:30:41.553437 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:146] EfficientDet BiFPN num iterations: 3
I1202 02:30:41.555058 4583968192 efficientnet_model.py:147] round_filter input=32 output=32
I1202 02:30:41.564762 4583968192 efficientnet_model.py:147] round_filter input=32 output=32
I1202 02:30:41.564820 4583968192 efficientnet_model.py:147] round_filter input=16 output=16
I1202 02:30:41.609905 4583968192 efficientnet_model.py:147] round_filter input=16 output=16
I1202 02:30:41.610006 4583968192 efficientnet_model.py:147] round_filter input=24 output=24
I1202 02:30:41.709283 4583968192 efficientnet_model.py:147] round_filter input=24 output=24
I1202 02:30:41.709374 4583968192 efficientnet_model.py:147] round_filter input=40 output=40
I1202 02:30:41.911164 4583968192 efficientnet_model.py:147] round_filter input=40 output=40
I1202 02:30:41.911273 4583968192 efficientnet_model.py:147] round_filter input=80 output=80
I1202 02:30:42.066483 4583968192 efficientnet_model.py:147] round_filter input=80 output=80
I1202 02:30:42.066741 4583968192 efficientnet_model.py:147] round_filter input=112 output=112
I1202 02:30:42.213964 4583968192 efficientnet_model.py:147] round_filter input=112 output=112
I1202 02:30:42.214054 4583968192 efficientnet_model.py:147] round_filter input=192 output=192
I1202 02:30:42.421770 4583968192 efficientnet_model.py:147] round_filter input=192 output=192
I1202 02:30:42.421862 4583968192 efficientnet_model.py:147] round_filter input=320 output=320
I1202 02:30:42.474714 4583968192 efficientnet_model.py:147] round_filter input=1280 output=1280
I1202 02:30:42.504832 4583968192 efficientnet_model.py:457] Building model efficientnet with params ModelConfig(width_coefficient=1.0, depth_coefficient=1.0, resolution=224, dropout_rate=0.2, blocks=(BlockConfig(input_filters=32, output_filters=16, kernel_size=3, num_repeat=1, expand_ratio=1, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=16, output_filters=24, kernel_size=3, num_repeat=2, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=24, output_filters=40, kernel_size=5, num_repeat=2, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=40, output_filters=80, kernel_size=3, num_repeat=3, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=80, output_filters=112, kernel_size=5, num_repeat=3, expand_ratio=6, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=112, output_filters=192, kernel_size=5, num_repeat=4, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=192, output_filters=320, kernel_size=3, num_repeat=1, expand_ratio=6, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise')), stem_base_filters=32, top_base_filters=1280, activation='simple_swish', batch_norm='default', bn_momentum=0.99, bn_epsilon=0.001, weight_decay=5e-06, drop_connect_rate=0.2, depth_divisor=8, min_depth=None, use_se=True, input_channels=3, num_classes=1000, model_name='efficientnet', rescale_input=False, data_format='channels_last', dtype='float32')
I1202 02:30:42.549032 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:143] EfficientDet EfficientNet backbone version: efficientnet-b1
I1202 02:30:42.549125 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:144] EfficientDet BiFPN num filters: 88
I1202 02:30:42.549170 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:146] EfficientDet BiFPN num iterations: 4
I1202 02:30:42.550533 4583968192 efficientnet_model.py:147] round_filter input=32 output=32
I1202 02:30:42.560315 4583968192 efficientnet_model.py:147] round_filter input=32 output=32
I1202 02:30:42.560378 4583968192 efficientnet_model.py:147] round_filter input=16 output=16
I1202 02:30:42.638497 4583968192 efficientnet_model.py:147] round_filter input=16 output=16
I1202 02:30:42.638591 4583968192 efficientnet_model.py:147] round_filter input=24 output=24
I1202 02:30:42.784461 4583968192 efficientnet_model.py:147] round_filter input=24 output=24
I1202 02:30:42.784554 4583968192 efficientnet_model.py:147] round_filter input=40 output=40
I1202 02:30:42.927882 4583968192 efficientnet_model.py:147] round_filter input=40 output=40
I1202 02:30:42.927973 4583968192 efficientnet_model.py:147] round_filter input=80 output=80
I1202 02:30:43.125022 4583968192 efficientnet_model.py:147] round_filter input=80 output=80
I1202 02:30:43.125114 4583968192 efficientnet_model.py:147] round_filter input=112 output=112
I1202 02:30:43.325484 4583968192 efficientnet_model.py:147] round_filter input=112 output=112
I1202 02:30:43.325578 4583968192 efficientnet_model.py:147] round_filter input=192 output=192
I1202 02:30:43.583667 4583968192 efficientnet_model.py:147] round_filter input=192 output=192
I1202 02:30:43.583758 4583968192 efficientnet_model.py:147] round_filter input=320 output=320
I1202 02:30:43.695272 4583968192 efficientnet_model.py:147] round_filter input=1280 output=1280
I1202 02:30:43.720252 4583968192 efficientnet_model.py:457] Building model efficientnet with params ModelConfig(width_coefficient=1.0, depth_coefficient=1.1, resolution=240, dropout_rate=0.2, blocks=(BlockConfig(input_filters=32, output_filters=16, kernel_size=3, num_repeat=1, expand_ratio=1, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=16, output_filters=24, kernel_size=3, num_repeat=2, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=24, output_filters=40, kernel_size=5, num_repeat=2, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=40, output_filters=80, kernel_size=3, num_repeat=3, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=80, output_filters=112, kernel_size=5, num_repeat=3, expand_ratio=6, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=112, output_filters=192, kernel_size=5, num_repeat=4, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=192, output_filters=320, kernel_size=3, num_repeat=1, expand_ratio=6, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise')), stem_base_filters=32, top_base_filters=1280, activation='simple_swish', batch_norm='default', bn_momentum=0.99, bn_epsilon=0.001, weight_decay=5e-06, drop_connect_rate=0.2, depth_divisor=8, min_depth=None, use_se=True, input_channels=3, num_classes=1000, model_name='efficientnet', rescale_input=False, data_format='channels_last', dtype='float32')
I1202 02:30:43.769829 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:143] EfficientDet EfficientNet backbone version: efficientnet-b2
I1202 02:30:43.769924 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:144] EfficientDet BiFPN num filters: 112
I1202 02:30:43.769968 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:146] EfficientDet BiFPN num iterations: 5
I1202 02:30:43.771260 4583968192 efficientnet_model.py:147] round_filter input=32 output=32
I1202 02:30:43.780781 4583968192 efficientnet_model.py:147] round_filter input=32 output=32
I1202 02:30:43.780840 4583968192 efficientnet_model.py:147] round_filter input=16 output=16
I1202 02:30:43.858814 4583968192 efficientnet_model.py:147] round_filter input=16 output=16
I1202 02:30:43.858901 4583968192 efficientnet_model.py:147] round_filter input=24 output=24
I1202 02:30:44.004163 4583968192 efficientnet_model.py:147] round_filter input=24 output=24
I1202 02:30:44.004260 4583968192 efficientnet_model.py:147] round_filter input=40 output=48
I1202 02:30:44.264582 4583968192 efficientnet_model.py:147] round_filter input=40 output=48
I1202 02:30:44.264675 4583968192 efficientnet_model.py:147] round_filter input=80 output=88
I1202 02:30:44.460062 4583968192 efficientnet_model.py:147] round_filter input=80 output=88
I1202 02:30:44.460154 4583968192 efficientnet_model.py:147] round_filter input=112 output=120
I1202 02:30:44.660029 4583968192 efficientnet_model.py:147] round_filter input=112 output=120
I1202 02:30:44.660121 4583968192 efficientnet_model.py:147] round_filter input=192 output=208
I1202 02:30:44.918321 4583968192 efficientnet_model.py:147] round_filter input=192 output=208
I1202 02:30:44.918416 4583968192 efficientnet_model.py:147] round_filter input=320 output=352
I1202 02:30:45.033729 4583968192 efficientnet_model.py:147] round_filter input=1280 output=1408
I1202 02:30:45.063637 4583968192 efficientnet_model.py:457] Building model efficientnet with params ModelConfig(width_coefficient=1.1, depth_coefficient=1.2, resolution=260, dropout_rate=0.3, blocks=(BlockConfig(input_filters=32, output_filters=16, kernel_size=3, num_repeat=1, expand_ratio=1, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=16, output_filters=24, kernel_size=3, num_repeat=2, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=24, output_filters=40, kernel_size=5, num_repeat=2, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=40, output_filters=80, kernel_size=3, num_repeat=3, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=80, output_filters=112, kernel_size=5, num_repeat=3, expand_ratio=6, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=112, output_filters=192, kernel_size=5, num_repeat=4, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=192, output_filters=320, kernel_size=3, num_repeat=1, expand_ratio=6, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise')), stem_base_filters=32, top_base_filters=1280, activation='simple_swish', batch_norm='default', bn_momentum=0.99, bn_epsilon=0.001, weight_decay=5e-06, drop_connect_rate=0.2, depth_divisor=8, min_depth=None, use_se=True, input_channels=3, num_classes=1000, model_name='efficientnet', rescale_input=False, data_format='channels_last', dtype='float32')
I1202 02:30:45.112596 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:143] EfficientDet EfficientNet backbone version: efficientnet-b3
I1202 02:30:45.112696 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:144] EfficientDet BiFPN num filters: 160
I1202 02:30:45.112742 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:146] EfficientDet BiFPN num iterations: 6
I1202 02:30:45.114074 4583968192 efficientnet_model.py:147] round_filter input=32 output=40
I1202 02:30:45.124145 4583968192 efficientnet_model.py:147] round_filter input=32 output=40
I1202 02:30:45.124208 4583968192 efficientnet_model.py:147] round_filter input=16 output=24
I1202 02:30:45.202368 4583968192 efficientnet_model.py:147] round_filter input=16 output=24
I1202 02:30:45.202458 4583968192 efficientnet_model.py:147] round_filter input=24 output=32
I1202 02:30:45.348882 4583968192 efficientnet_model.py:147] round_filter input=24 output=32
I1202 02:30:45.349001 4583968192 efficientnet_model.py:147] round_filter input=40 output=48
I1202 02:30:45.494862 4583968192 efficientnet_model.py:147] round_filter input=40 output=48
I1202 02:30:45.494951 4583968192 efficientnet_model.py:147] round_filter input=80 output=96
I1202 02:30:45.748532 4583968192 efficientnet_model.py:147] round_filter input=80 output=96
I1202 02:30:45.748625 4583968192 efficientnet_model.py:147] round_filter input=112 output=136
I1202 02:30:46.002632 4583968192 efficientnet_model.py:147] round_filter input=112 output=136
I1202 02:30:46.002726 4583968192 efficientnet_model.py:147] round_filter input=192 output=232
I1202 02:30:46.322188 4583968192 efficientnet_model.py:147] round_filter input=192 output=232
I1202 02:30:46.322280 4583968192 efficientnet_model.py:147] round_filter input=320 output=384
I1202 02:30:46.440469 4583968192 efficientnet_model.py:147] round_filter input=1280 output=1536
I1202 02:30:46.473453 4583968192 efficientnet_model.py:457] Building model efficientnet with params ModelConfig(width_coefficient=1.2, depth_coefficient=1.4, resolution=300, dropout_rate=0.3, blocks=(BlockConfig(input_filters=32, output_filters=16, kernel_size=3, num_repeat=1, expand_ratio=1, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=16, output_filters=24, kernel_size=3, num_repeat=2, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=24, output_filters=40, kernel_size=5, num_repeat=2, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=40, output_filters=80, kernel_size=3, num_repeat=3, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=80, output_filters=112, kernel_size=5, num_repeat=3, expand_ratio=6, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=112, output_filters=192, kernel_size=5, num_repeat=4, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=192, output_filters=320, kernel_size=3, num_repeat=1, expand_ratio=6, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise')), stem_base_filters=32, top_base_filters=1280, activation='simple_swish', batch_norm='default', bn_momentum=0.99, bn_epsilon=0.001, weight_decay=5e-06, drop_connect_rate=0.2, depth_divisor=8, min_depth=None, use_se=True, input_channels=3, num_classes=1000, model_name='efficientnet', rescale_input=False, data_format='channels_last', dtype='float32')
I1202 02:30:46.529264 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:143] EfficientDet EfficientNet backbone version: efficientnet-b4
I1202 02:30:46.529356 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:144] EfficientDet BiFPN num filters: 224
I1202 02:30:46.529400 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:146] EfficientDet BiFPN num iterations: 7
I1202 02:30:46.530598 4583968192 efficientnet_model.py:147] round_filter input=32 output=48
I1202 02:30:46.541093 4583968192 efficientnet_model.py:147] round_filter input=32 output=48
I1202 02:30:46.541155 4583968192 efficientnet_model.py:147] round_filter input=16 output=24
I1202 02:30:46.620296 4583968192 efficientnet_model.py:147] round_filter input=16 output=24
I1202 02:30:46.620386 4583968192 efficientnet_model.py:147] round_filter input=24 output=32
I1202 02:30:46.969455 4583968192 efficientnet_model.py:147] round_filter input=24 output=32
I1202 02:30:46.969547 4583968192 efficientnet_model.py:147] round_filter input=40 output=56
I1202 02:30:47.166608 4583968192 efficientnet_model.py:147] round_filter input=40 output=56
I1202 02:30:47.166871 4583968192 efficientnet_model.py:147] round_filter input=80 output=112
I1202 02:30:47.462141 4583968192 efficientnet_model.py:147] round_filter input=80 output=112
I1202 02:30:47.462232 4583968192 efficientnet_model.py:147] round_filter input=112 output=160
I1202 02:30:47.791725 4583968192 efficientnet_model.py:147] round_filter input=112 output=160
I1202 02:30:47.791823 4583968192 efficientnet_model.py:147] round_filter input=192 output=272
I1202 02:30:48.225534 4583968192 efficientnet_model.py:147] round_filter input=192 output=272
I1202 02:30:48.225628 4583968192 efficientnet_model.py:147] round_filter input=320 output=448
I1202 02:30:48.347712 4583968192 efficientnet_model.py:147] round_filter input=1280 output=1792
I1202 02:30:48.383035 4583968192 efficientnet_model.py:457] Building model efficientnet with params ModelConfig(width_coefficient=1.4, depth_coefficient=1.8, resolution=380, dropout_rate=0.4, blocks=(BlockConfig(input_filters=32, output_filters=16, kernel_size=3, num_repeat=1, expand_ratio=1, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=16, output_filters=24, kernel_size=3, num_repeat=2, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=24, output_filters=40, kernel_size=5, num_repeat=2, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=40, output_filters=80, kernel_size=3, num_repeat=3, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=80, output_filters=112, kernel_size=5, num_repeat=3, expand_ratio=6, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=112, output_filters=192, kernel_size=5, num_repeat=4, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=192, output_filters=320, kernel_size=3, num_repeat=1, expand_ratio=6, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise')), stem_base_filters=32, top_base_filters=1280, activation='simple_swish', batch_norm='default', bn_momentum=0.99, bn_epsilon=0.001, weight_decay=5e-06, drop_connect_rate=0.2, depth_divisor=8, min_depth=None, use_se=True, input_channels=3, num_classes=1000, model_name='efficientnet', rescale_input=False, data_format='channels_last', dtype='float32')
I1202 02:30:48.447986 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:143] EfficientDet EfficientNet backbone version: efficientnet-b5
I1202 02:30:48.448080 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:144] EfficientDet BiFPN num filters: 288
I1202 02:30:48.448125 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:146] EfficientDet BiFPN num iterations: 7
I1202 02:30:48.449392 4583968192 efficientnet_model.py:147] round_filter input=32 output=48
I1202 02:30:48.459087 4583968192 efficientnet_model.py:147] round_filter input=32 output=48
I1202 02:30:48.459150 4583968192 efficientnet_model.py:147] round_filter input=16 output=24
I1202 02:30:48.575913 4583968192 efficientnet_model.py:147] round_filter input=16 output=24
I1202 02:30:48.576004 4583968192 efficientnet_model.py:147] round_filter input=24 output=40
I1202 02:30:48.818200 4583968192 efficientnet_model.py:147] round_filter input=24 output=40
I1202 02:30:48.818294 4583968192 efficientnet_model.py:147] round_filter input=40 output=64
I1202 02:30:49.059697 4583968192 efficientnet_model.py:147] round_filter input=40 output=64
I1202 02:30:49.059787 4583968192 efficientnet_model.py:147] round_filter input=80 output=128
I1202 02:30:49.404906 4583968192 efficientnet_model.py:147] round_filter input=80 output=128
I1202 02:30:49.404997 4583968192 efficientnet_model.py:147] round_filter input=112 output=176
I1202 02:30:49.934098 4583968192 efficientnet_model.py:147] round_filter input=112 output=176
I1202 02:30:49.934190 4583968192 efficientnet_model.py:147] round_filter input=192 output=304
I1202 02:30:50.438941 4583968192 efficientnet_model.py:147] round_filter input=192 output=304
I1202 02:30:50.439036 4583968192 efficientnet_model.py:147] round_filter input=320 output=512
I1202 02:30:50.646334 4583968192 efficientnet_model.py:147] round_filter input=1280 output=2048
I1202 02:30:50.684907 4583968192 efficientnet_model.py:457] Building model efficientnet with params ModelConfig(width_coefficient=1.6, depth_coefficient=2.2, resolution=456, dropout_rate=0.4, blocks=(BlockConfig(input_filters=32, output_filters=16, kernel_size=3, num_repeat=1, expand_ratio=1, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=16, output_filters=24, kernel_size=3, num_repeat=2, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=24, output_filters=40, kernel_size=5, num_repeat=2, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=40, output_filters=80, kernel_size=3, num_repeat=3, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=80, output_filters=112, kernel_size=5, num_repeat=3, expand_ratio=6, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=112, output_filters=192, kernel_size=5, num_repeat=4, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=192, output_filters=320, kernel_size=3, num_repeat=1, expand_ratio=6, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise')), stem_base_filters=32, top_base_filters=1280, activation='simple_swish', batch_norm='default', bn_momentum=0.99, bn_epsilon=0.001, weight_decay=5e-06, drop_connect_rate=0.2, depth_divisor=8, min_depth=None, use_se=True, input_channels=3, num_classes=1000, model_name='efficientnet', rescale_input=False, data_format='channels_last', dtype='float32')
I1202 02:30:50.761388 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:143] EfficientDet EfficientNet backbone version: efficientnet-b6
I1202 02:30:50.761481 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:144] EfficientDet BiFPN num filters: 384
I1202 02:30:50.761526 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:146] EfficientDet BiFPN num iterations: 8
I1202 02:30:50.762736 4583968192 efficientnet_model.py:147] round_filter input=32 output=56
I1202 02:30:50.773027 4583968192 efficientnet_model.py:147] round_filter input=32 output=56
I1202 02:30:50.773087 4583968192 efficientnet_model.py:147] round_filter input=16 output=32
I1202 02:30:50.888293 4583968192 efficientnet_model.py:147] round_filter input=16 output=32
I1202 02:30:50.888384 4583968192 efficientnet_model.py:147] round_filter input=24 output=40
I1202 02:30:51.179208 4583968192 efficientnet_model.py:147] round_filter input=24 output=40
I1202 02:30:51.179298 4583968192 efficientnet_model.py:147] round_filter input=40 output=72
I1202 02:30:51.472671 4583968192 efficientnet_model.py:147] round_filter input=40 output=72
I1202 02:30:51.472762 4583968192 efficientnet_model.py:147] round_filter input=80 output=144
I1202 02:30:51.872358 4583968192 efficientnet_model.py:147] round_filter input=80 output=144
I1202 02:30:51.872448 4583968192 efficientnet_model.py:147] round_filter input=112 output=200
I1202 02:30:52.292670 4583968192 efficientnet_model.py:147] round_filter input=112 output=200
I1202 02:30:52.292761 4583968192 efficientnet_model.py:147] round_filter input=192 output=344
I1202 02:30:52.941159 4583968192 efficientnet_model.py:147] round_filter input=192 output=344
I1202 02:30:52.941253 4583968192 efficientnet_model.py:147] round_filter input=320 output=576
I1202 02:30:53.345532 4583968192 efficientnet_model.py:147] round_filter input=1280 output=2304
I1202 02:30:53.386226 4583968192 efficientnet_model.py:457] Building model efficientnet with params ModelConfig(width_coefficient=1.8, depth_coefficient=2.6, resolution=528, dropout_rate=0.5, blocks=(BlockConfig(input_filters=32, output_filters=16, kernel_size=3, num_repeat=1, expand_ratio=1, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=16, output_filters=24, kernel_size=3, num_repeat=2, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=24, output_filters=40, kernel_size=5, num_repeat=2, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=40, output_filters=80, kernel_size=3, num_repeat=3, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=80, output_filters=112, kernel_size=5, num_repeat=3, expand_ratio=6, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=112, output_filters=192, kernel_size=5, num_repeat=4, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=192, output_filters=320, kernel_size=3, num_repeat=1, expand_ratio=6, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise')), stem_base_filters=32, top_base_filters=1280, activation='simple_swish', batch_norm='default', bn_momentum=0.99, bn_epsilon=0.001, weight_decay=5e-06, drop_connect_rate=0.2, depth_divisor=8, min_depth=None, use_se=True, input_channels=3, num_classes=1000, model_name='efficientnet', rescale_input=False, data_format='channels_last', dtype='float32')
I1202 02:30:53.475569 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:143] EfficientDet EfficientNet backbone version: efficientnet-b7
I1202 02:30:53.475664 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:144] EfficientDet BiFPN num filters: 384
I1202 02:30:53.475709 4583968192 ssd_efficientnet_bifpn_feature_extractor.py:146] EfficientDet BiFPN num iterations: 8
I1202 02:30:53.476958 4583968192 efficientnet_model.py:147] round_filter input=32 output=64
I1202 02:30:53.487647 4583968192 efficientnet_model.py:147] round_filter input=32 output=64
I1202 02:30:53.487706 4583968192 efficientnet_model.py:147] round_filter input=16 output=32
I1202 02:30:53.644389 4583968192 efficientnet_model.py:147] round_filter input=16 output=32
I1202 02:30:53.644482 4583968192 efficientnet_model.py:147] round_filter input=24 output=48
I1202 02:30:53.982273 4583968192 efficientnet_model.py:147] round_filter input=24 output=48
I1202 02:30:53.982454 4583968192 efficientnet_model.py:147] round_filter input=40 output=80
I1202 02:30:54.332922 4583968192 efficientnet_model.py:147] round_filter input=40 output=80
I1202 02:30:54.333017 4583968192 efficientnet_model.py:147] round_filter input=80 output=160
I1202 02:30:54.847017 4583968192 efficientnet_model.py:147] round_filter input=80 output=160
I1202 02:30:54.847109 4583968192 efficientnet_model.py:147] round_filter input=112 output=224
I1202 02:30:55.377130 4583968192 efficientnet_model.py:147] round_filter input=112 output=224
I1202 02:30:55.377221 4583968192 efficientnet_model.py:147] round_filter input=192 output=384
I1202 02:30:56.179420 4583968192 efficientnet_model.py:147] round_filter input=192 output=384
I1202 02:30:56.179515 4583968192 efficientnet_model.py:147] round_filter input=320 output=640
I1202 02:30:56.508601 4583968192 efficientnet_model.py:147] round_filter input=1280 output=2560
I1202 02:30:56.555413 4583968192 efficientnet_model.py:457] Building model efficientnet with params ModelConfig(width_coefficient=2.0, depth_coefficient=3.1, resolution=600, dropout_rate=0.5, blocks=(BlockConfig(input_filters=32, output_filters=16, kernel_size=3, num_repeat=1, expand_ratio=1, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=16, output_filters=24, kernel_size=3, num_repeat=2, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=24, output_filters=40, kernel_size=5, num_repeat=2, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=40, output_filters=80, kernel_size=3, num_repeat=3, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=80, output_filters=112, kernel_size=5, num_repeat=3, expand_ratio=6, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=112, output_filters=192, kernel_size=5, num_repeat=4, expand_ratio=6, strides=(2, 2), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise'), BlockConfig(input_filters=192, output_filters=320, kernel_size=3, num_repeat=1, expand_ratio=6, strides=(1, 1), se_ratio=0.25, id_skip=True, fused_conv=False, conv_type='depthwise')), stem_base_filters=32, top_base_filters=1280, activation='simple_swish', batch_norm='default', bn_momentum=0.99, bn_epsilon=0.001, weight_decay=5e-06, drop_connect_rate=0.2, depth_divisor=8, min_depth=None, use_se=True, input_channels=3, num_classes=1000, model_name='efficientnet', rescale_input=False, data_format='channels_last', dtype='float32')
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_create_ssd_models_from_config): 15.44s
I1202 02:30:56.867681 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_create_ssd_models_from_config): 15.44s
[       OK ] ModelBuilderTF2Test.test_create_ssd_models_from_config
[ RUN      ] ModelBuilderTF2Test.test_invalid_faster_rcnn_batchnorm_update
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_invalid_faster_rcnn_batchnorm_update): 0.0s
I1202 02:30:56.875756 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_invalid_faster_rcnn_batchnorm_update): 0.0s
[       OK ] ModelBuilderTF2Test.test_invalid_faster_rcnn_batchnorm_update
[ RUN      ] ModelBuilderTF2Test.test_invalid_first_stage_nms_iou_threshold
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_invalid_first_stage_nms_iou_threshold): 0.0s
I1202 02:30:56.877240 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_invalid_first_stage_nms_iou_threshold): 0.0s
[       OK ] ModelBuilderTF2Test.test_invalid_first_stage_nms_iou_threshold
[ RUN      ] ModelBuilderTF2Test.test_invalid_model_config_proto
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_invalid_model_config_proto): 0.0s
I1202 02:30:56.877588 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_invalid_model_config_proto): 0.0s
[       OK ] ModelBuilderTF2Test.test_invalid_model_config_proto
[ RUN      ] ModelBuilderTF2Test.test_invalid_second_stage_batch_size
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_invalid_second_stage_batch_size): 0.0s
I1202 02:30:56.878968 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_invalid_second_stage_batch_size): 0.0s
[       OK ] ModelBuilderTF2Test.test_invalid_second_stage_batch_size
[ RUN      ] ModelBuilderTF2Test.test_session
[  SKIPPED ] ModelBuilderTF2Test.test_session
[ RUN      ] ModelBuilderTF2Test.test_unknown_faster_rcnn_feature_extractor
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_unknown_faster_rcnn_feature_extractor): 0.0s
I1202 02:30:56.880214 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_unknown_faster_rcnn_feature_extractor): 0.0s
[       OK ] ModelBuilderTF2Test.test_unknown_faster_rcnn_feature_extractor
[ RUN      ] ModelBuilderTF2Test.test_unknown_meta_architecture
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_unknown_meta_architecture): 0.0s
I1202 02:30:56.880523 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_unknown_meta_architecture): 0.0s
[       OK ] ModelBuilderTF2Test.test_unknown_meta_architecture
[ RUN      ] ModelBuilderTF2Test.test_unknown_ssd_feature_extractor
INFO:tensorflow:time(__main__.ModelBuilderTF2Test.test_unknown_ssd_feature_extractor): 0.0s
I1202 02:30:56.881410 4583968192 test_util.py:2189] time(__main__.ModelBuilderTF2Test.test_unknown_ssd_feature_extractor): 0.0s
[       OK ] ModelBuilderTF2Test.test_unknown_ssd_feature_extractor
----------------------------------------------------------------------
Ran 24 tests in 18.518s

OK (skipped=1)

說明已經安裝成功

人臉圖像數據集

WIDER FACE數據集,是香港中文大學採集的一個非常大規模的數據集,包含了32203個圖像和393703張人臉圖像。它包含了非常多的在人臉檢測問題上的難點,比如尺度、姿勢、遮擋、表情、裝扮和光照等等。包含了61個事件類別組織的,對於每一個事件類別,選取其中的40%作爲訓練集,10%用於交叉驗證,50%作爲測試集。

  • Passcal VOC

我們首先會將這些數據集轉化成Passcal VOC格式的數據,Passcal VOC是我們在進行目標檢測非常常見的一個數據集,如何評價一個目標檢測,通用物體檢測它的算法好壞,通常會採用兩個數據集來評價,其中一個就是Passcal VOC,另一個就是COCO數據集。Passcal VOC包含了三個重要的文件夾:Annotations、ImageSets和JPEGImages。

在JPEGImages和Annotations分別存放了圖片和標籤信息。在這兩個文件夾中的文件的命名是相同的,只不過格式不同。在Annotations文件夾下面數據的標籤信息主要是通過xml格式的數據來進行標註的。在這個xml文件中會指向JPEGImages中的文件所對應的原始圖像的路徑以及包含了在圖像中的人臉框的座標。在ImageSets文件夾中存放了訓練和測試所用到的文件的列表。

我們在進行人臉檢測數據打包的時候會首先將數據轉化成Passcal VOC格式的數據,然後再利用Tensorflow提供的接口來對轉化好的WIDER FACE數據集再進行打包。我們會根據ImageSets文件夾中的文件列表將數據分成訓練集、驗證集和測試集這樣的幾個不同的數據集。

該數據集的下載地址爲:http://shuoyang1213.me/WIDERFACE/

在下載好的wider_face_split文件夾下,有關於圖像文件的說明,我們打開wider_face_train_bbx_gt.txt,會看到如下的內容

0--Parade/0_Parade_marchingband_1_849.jpg
1
449 330 122 149 0 0 0 0 0 0 
0--Parade/0_Parade_Parade_0_904.jpg
1
361 98 263 339 0 0 0 0 0 0 
0--Parade/0_Parade_marchingband_1_799.jpg
21
78 221 7 8 2 0 0 0 0 0 
78 238 14 17 2 0 0 0 0 0 
113 212 11 15 2 0 0 0 0 0 
134 260 15 15 2 0 0 0 0 0 
163 250 14 17 2 0 0 0 0 0 
201 218 10 12 2 0 0 0 0 0 
182 266 15 17 2 0 0 0 0 0 
245 279 18 15 2 0 0 0 0 0 
304 265 16 17 2 0 0 0 2 1 
328 295 16 20 2 0 0 0 0 0 
389 281 17 19 2 0 0 0 2 0 
406 293 21 21 2 0 1 0 0 0 
436 290 22 17 2 0 0 0 0 0 
522 328 21 18 2 0 1 0 0 0 
643 320 23 22 2 0 0 0 0 0 
653 224 17 25 2 0 0 0 0 0 
793 337 23 30 2 0 0 0 0 0 
535 311 16 17 2 0 0 0 1 0 
29 220 11 15 2 0 0 0 0 0 
3 232 11 15 2 0 0 0 2 0 
20 215 12 16 2 0 0 0 2 0 

這裏是訓練樣本的標註數據,首先

0--Parade/0_Parade_marchingband_1_849.jpg

表示圖片的路徑。下面的1表示有幾個人臉,而

449 330 122 149 0 0 0 0 0 0 

這樣一串數字代表了人臉的標註信息,其中前4個值代表了人臉框的座標信息。前2個值代表了人臉框左上角的座標,後面兩個值代表了人臉框的大小(即寬和高)。後面的6個值對應了人臉不同的屬性的描述,分別爲scale尺度、pose姿勢、occlusion遮擋、expression表情、makeup裝扮和illumination光照。現在我們來看一下WIDER_train文件夾,這是訓練集的圖片文件,下面有一個images文件夾,裏面又有61個子文件夾,分別代表了61個不同的場景。

我們進入11--Meeting文件夾,可以看到裏面都是關於會議的人臉圖片文件

現在我們來對這些原始文件進行打包成Passcal VOC,先創建需要的幾個文件夾和文件

mkdir -p widerface/ImageSets/Main
touch widerface/ImageSets/Main/train.txt
mkdir widerface/JPEGImages
mkdir widerface/Annotations

然後是將訓練集的數據給轉化過去,驗證集和測試集只需要修改相應的路徑就文件就可以了

import os
import cv2
import sys
import shutil
import numpy as np
from xml.dom.minidom import Document

def writexml(filename, saveimg, bboxes, xmlpath):
    doc = Document()
    annotation = doc.createElement('annotation')
    doc.appendChild(annotation)
    folder = doc.createElement('folder')
    folder_name = doc.createTextNode('widerface')
    folder.appendChild(folder_name)
    annotation.appendChild(folder)
    filenamenode = doc.createElement('filename')
    filename_name = doc.createTextNode(filename)
    filenamenode.appendChild(filename_name)
    annotation.appendChild(filenamenode)
    source = doc.createElement('source')
    annotation.appendChild(source)
    database = doc.createElement('database')
    database.appendChild(doc.createTextNode('wider face Database'))
    source.appendChild(database)
    annotation_s = doc.createElement('annotation')
    annotation_s.appendChild(doc.createTextNode('PASCAL VOC2007'))
    source.appendChild(annotation_s)
    image = doc.createElement('image')
    image.appendChild(doc.createTextNode('flickr'))
    source.appendChild(image)
    flickrid = doc.createElement('flickrid')
    flickrid.appendChild(doc.createTextNode('-1'))
    source.appendChild(flickrid)
    owner = doc.createElement('owner')
    annotation.appendChild(owner)
    flickrid_o = doc.createElement('flickrid')
    flickrid_o.appendChild(doc.createTextNode('guanjian'))
    owner.appendChild(flickrid_o)
    name_o = doc.createElement('name')
    name_o.appendChild(doc.createTextNode('muke'))
    owner.appendChild(name_o)

    size = doc.createElement('size')
    annotation.appendChild(size)

    width = doc.createElement('width')
    width.appendChild(doc.createTextNode(str(saveimg.shape[1])))
    height = doc.createElement('height')
    height.appendChild(doc.createTextNode(str(saveimg.shape[0])))
    depth = doc.createElement('depth')
    depth.appendChild(doc.createTextNode(str(saveimg.shape[2])))

    size.appendChild(width)

    size.appendChild(height)
    size.appendChild(depth)
    segmented = doc.createElement('segmented')
    segmented.appendChild(doc.createTextNode('0'))
    annotation.appendChild(segmented)
    for i in range(len(bboxes)):
        bbox = bboxes[i]
        objects = doc.createElement('object')
        annotation.appendChild(objects)
        object_name = doc.createElement('name')
        object_name.appendChild(doc.createTextNode('face'))
        objects.appendChild(object_name)
        pose = doc.createElement('pose')
        pose.appendChild(doc.createTextNode('Unspecified'))
        objects.appendChild(pose)
        truncated = doc.createElement('truncated')
        truncated.appendChild(doc.createTextNode('0'))
        objects.appendChild(truncated)
        difficult = doc.createElement('difficult')
        difficult.appendChild(doc.createTextNode('0'))
        objects.appendChild(difficult)
        bndbox = doc.createElement('bndbox')
        objects.appendChild(bndbox)
        xmin = doc.createElement('xmin')
        xmin.appendChild(doc.createTextNode(str(bbox[0])))
        bndbox.appendChild(xmin)
        ymin = doc.createElement('ymin')
        ymin.appendChild(doc.createTextNode(str(bbox[1])))
        bndbox.appendChild(ymin)
        xmax = doc.createElement('xmax')
        xmax.appendChild(doc.createTextNode(str(bbox[0] + bbox[2])))
        bndbox.appendChild(xmax)
        ymax = doc.createElement('ymax')
        ymax.appendChild(doc.createTextNode(str(bbox[1] + bbox[3])))
        bndbox.appendChild(ymax)
    with open(xmlpath, "w") as f:
        f.write(doc.toprettyxml(indent=''))

if __name__ == "__main__":

    rootdir = "/Users/admin/Downloads/widerface/"
    gtfile = "/Users/admin/Downloads/wider_face_split/wider_face_train_bbx_gt.txt"
    im_folder = "/Users/admin/Downloads/WIDER_train/images"
    ##這裏可以是test也可以是val
    fwrite = open("/Users/admin/Downloads/widerface/ImageSets/Main/train.txt", "w")

    with open(gtfile, "r") as gt:
        while True:
            gt_con = gt.readline()[:-1]
            if gt_con is None or gt_con == "":
                break
            im_path = im_folder + "/" + gt_con
            print(im_path)
            im_data = cv2.imread(im_path)
            if im_data is None:
                continue

            ##需要注意的一點是,圖片直接經過resize之後,會存在更多的長寬比例,所以我們直接加pad
            sc = max(im_data.shape)
            im_data_tmp = np.zeros([sc, sc, 3], dtype=np.uint8)
            off_w = (sc - im_data.shape[1]) // 2
            off_h = (sc - im_data.shape[0]) // 2

            ##對圖片進行周圍填充,填充爲正方形
            im_data_tmp[off_h:im_data.shape[0] + off_h, off_w:im_data.shape[1] + off_w, ...] = im_data
            im_data = im_data_tmp
            #
            # cv2.imshow("1", im_data)
            # cv2.waitKey(0)
            numbox = int(gt.readline())
            # numbox = 0
            bboxes = []
            for i in range(numbox):
                line = gt.readline()
                infos = line.split(" ")
                # x y w h ---
                # 去掉最後一個(\n)
                for j in range(infos.__len__() - 1):
                    infos[j] = int(infos[j])

                ##注意這裏加入了數據清洗
                ##保留resize到640×640 尺寸在8×8以上的人臉
                if infos[2] * 80 < im_data.shape[1] or infos[3] * 80 < im_data.shape[0]:
                    continue

                bbox = (infos[0] + off_w, infos[1] + off_h, infos[2], infos[3])
                # cv2.rectangle(im_data, (int(infos[0]) + off_w, int(infos[1]) + off_h),
                #               (int(infos[0]) + off_w + int(infos[2]), int(infos[1]) + off_h + int(infos[3])),
                #               color=(0, 0, 255), thickness=1)
                bboxes.append(bbox)

            # cv2.imshow("1", im_data)
            # cv2.waitKey(0)

            filename = gt_con.replace("/", "_")
            fwrite.write(filename.split(".")[0] + "\n")

            cv2.imwrite("{}/JPEGImages/{}".format(rootdir, filename), im_data)

            xmlpath = "{}/Annotations/{}.xml".format(rootdir, filename.split(".")[0])
            writexml(filename, im_data, bboxes, xmlpath)
    fwrite.close()

當然測試數據集是沒有bounding box數據的,所以在打包test數據集的時候,上面的代碼需要調整爲

import os
import cv2
import sys
import shutil
import numpy as np
from xml.dom.minidom import Document

def writexml(filename, saveimg, bboxes, xmlpath):
    doc = Document()
    annotation = doc.createElement('annotation')
    doc.appendChild(annotation)
    folder = doc.createElement('folder')
    folder_name = doc.createTextNode('widerface')
    folder.appendChild(folder_name)
    annotation.appendChild(folder)
    filenamenode = doc.createElement('filename')
    filename_name = doc.createTextNode(filename)
    filenamenode.appendChild(filename_name)
    annotation.appendChild(filenamenode)
    source = doc.createElement('source')
    annotation.appendChild(source)
    database = doc.createElement('database')
    database.appendChild(doc.createTextNode('wider face Database'))
    source.appendChild(database)
    annotation_s = doc.createElement('annotation')
    annotation_s.appendChild(doc.createTextNode('PASCAL VOC2007'))
    source.appendChild(annotation_s)
    image = doc.createElement('image')
    image.appendChild(doc.createTextNode('flickr'))
    source.appendChild(image)
    flickrid = doc.createElement('flickrid')
    flickrid.appendChild(doc.createTextNode('-1'))
    source.appendChild(flickrid)
    owner = doc.createElement('owner')
    annotation.appendChild(owner)
    flickrid_o = doc.createElement('flickrid')
    flickrid_o.appendChild(doc.createTextNode('muke'))
    owner.appendChild(flickrid_o)
    name_o = doc.createElement('name')
    name_o.appendChild(doc.createTextNode('guanjian'))
    owner.appendChild(name_o)

    size = doc.createElement('size')
    annotation.appendChild(size)

    width = doc.createElement('width')
    width.appendChild(doc.createTextNode(str(saveimg.shape[1])))
    height = doc.createElement('height')
    height.appendChild(doc.createTextNode(str(saveimg.shape[0])))
    depth = doc.createElement('depth')
    depth.appendChild(doc.createTextNode(str(saveimg.shape[2])))

    size.appendChild(width)

    size.appendChild(height)
    size.appendChild(depth)
    segmented = doc.createElement('segmented')
    segmented.appendChild(doc.createTextNode('0'))
    annotation.appendChild(segmented)
    # for i in range(len(bboxes)):
    #     bbox = bboxes[i]
    #     objects = doc.createElement('object')
    #     annotation.appendChild(objects)
    #     object_name = doc.createElement('name')
    #     object_name.appendChild(doc.createTextNode('face'))
    #     objects.appendChild(object_name)
    #     pose = doc.createElement('pose')
    #     pose.appendChild(doc.createTextNode('Unspecified'))
    #     objects.appendChild(pose)
    #     truncated = doc.createElement('truncated')
    #     truncated.appendChild(doc.createTextNode('0'))
    #     objects.appendChild(truncated)
    #     difficult = doc.createElement('difficult')
    #     difficult.appendChild(doc.createTextNode('0'))
    #     objects.appendChild(difficult)
    #     bndbox = doc.createElement('bndbox')
    #     objects.appendChild(bndbox)
    #     xmin = doc.createElement('xmin')
    #     xmin.appendChild(doc.createTextNode(str(bbox[0])))
    #     bndbox.appendChild(xmin)
    #     ymin = doc.createElement('ymin')
    #     ymin.appendChild(doc.createTextNode(str(bbox[1])))
    #     bndbox.appendChild(ymin)
    #     xmax = doc.createElement('xmax')
    #     xmax.appendChild(doc.createTextNode(str(bbox[0] + bbox[2])))
    #     bndbox.appendChild(xmax)
    #     ymax = doc.createElement('ymax')
    #     ymax.appendChild(doc.createTextNode(str(bbox[1] + bbox[3])))
    #     bndbox.appendChild(ymax)
    with open(xmlpath, "w") as f:
        f.write(doc.toprettyxml(indent=''))

if __name__ == "__main__":

    rootdir = "/Users/admin/Downloads/widerface/"
    gtfile = "/Users/admin/Downloads/wider_face_split/wider_face_test_filelist.txt"
    im_folder = "/Users/admin/Downloads/WIDER_test/images"
    ##這裏可以是test也可以是val
    fwrite = open("/Users/admin/Downloads/widerface/ImageSets/Main/test.txt", "w")

    with open(gtfile, "r") as gt:
        while True:
            gt_con = gt.readline()[:-1]
            if gt_con is None or gt_con == "":
                break
            im_path = im_folder + "/" + gt_con
            print(im_path)
            im_data = cv2.imread(im_path)
            if im_data is None:
                continue

            ##需要注意的一點是,圖片直接經過resize之後,會存在更多的長寬比例,所以我們直接加pad
            sc = max(im_data.shape)
            im_data_tmp = np.zeros([sc, sc, 3], dtype=np.uint8)
            off_w = (sc - im_data.shape[1]) // 2
            off_h = (sc - im_data.shape[0]) // 2

            ##對圖片進行周圍填充,填充爲正方形
            im_data_tmp[off_h:im_data.shape[0] + off_h, off_w:im_data.shape[1] + off_w, ...] = im_data
            im_data = im_data_tmp
            #
            # cv2.imshow("1", im_data)
            # cv2.waitKey(0)
            # numbox = int(gt.readline())
            # # numbox = 0
            # bboxes = []
            # for i in range(numbox):
            #     line = gt.readline()
            #     infos = line.split(" ")
            #     # x y w h ---
            #     # 去掉最後一個(\n)
            #     for j in range(infos.__len__() - 1):
            #         infos[j] = int(infos[j])
            #
            #     ##注意這裏加入了數據清洗
            #     ##保留resize到640×640 尺寸在8×8以上的人臉
            #     if infos[2] * 80 < im_data.shape[1] or infos[3] * 80 < im_data.shape[0]:
            #         continue
            #
            #     bbox = (infos[0] + off_w, infos[1] + off_h, infos[2], infos[3])
            #     # cv2.rectangle(im_data, (int(infos[0]) + off_w, int(infos[1]) + off_h),
            #     #               (int(infos[0]) + off_w + int(infos[2]), int(infos[1]) + off_h + int(infos[3])),
            #     #               color=(0, 0, 255), thickness=1)
            #     bboxes.append(bbox)

            # cv2.imshow("1", im_data)
            # cv2.waitKey(0)

            filename = gt_con.replace("/", "_")
            fwrite.write(filename.split(".")[0] + "\n")

            cv2.imwrite("{}/JPEGImages/{}".format(rootdir, filename), im_data)

            xmlpath = "{}/Annotations/{}.xml".format(rootdir, filename.split(".")[0])
            writexml(filename, im_data, None, xmlpath)
    fwrite.close()

我們來看一下生成的Annotations文件夾下的標註xml文件

<annotation>
<folder>widerface</folder>
<filename>0--Parade_0_Parade_marchingband_1_5.jpg</filename>
<source>
<database>wider face Database</database>
<annotation>PASCAL VOC2007</annotation>
<image>flickr</image>
<flickrid>-1</flickrid>
</source>
<owner>
<flickrid>guanjian</flickrid>
<name>guanjian</name>
</owner>
<size>
<width>1024</width>
<height>1024</height>
<depth>3</depth>
</size>
<segmented>0</segmented>
<object>
<name>face</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>495</xmin>
<ymin>347</ymin>
<xmax>532</xmax>
<ymax>398</ymax>
</bndbox>
</object>
<object>
<name>face</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>406</xmin>
<ymin>373</ymin>
<xmax>444</xmax>
<ymax>421</ymax>
</bndbox>
</object>
<object>
<name>face</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>316</xmin>
<ymin>383</ymin>
<xmax>354</xmax>
<ymax>425</ymax>
</bndbox>
</object>
<object>
<name>face</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>221</xmin>
<ymin>396</ymin>
<xmax>259</xmax>
<ymax>438</ymax>
</bndbox>
</object>
<object>
<name>face</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>23</xmin>
<ymin>378</ymin>
<xmax>55</xmax>
<ymax>413</ymax>
</bndbox>
</object>
<object>
<name>face</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>919</xmin>
<ymin>336</ymin>
<xmax>960</xmax>
<ymax>379</ymax>
</bndbox>
</object>
<object>
<name>face</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>784</xmin>
<ymin>322</ymin>
<xmax>822</xmax>
<ymax>367</ymax>
</bndbox>
</object>
<object>
<name>face</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>714</xmin>
<ymin>360</ymin>
<xmax>753</xmax>
<ymax>405</ymax>
</bndbox>
</object>
<object>
<name>face</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>594</xmin>
<ymin>355</ymin>
<xmax>626</xmax>
<ymax>391</ymax>
</bndbox>
</object>
<object>
<name>face</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>146</xmin>
<ymin>385</ymin>
<xmax>179</xmax>
<ymax>418</ymax>
</bndbox>
</object>
</annotation>

首先

<filename>0--Parade_0_Parade_marchingband_1_5.jpg</filename>

是針對於哪張圖片的標註文件。source標籤代表了圖像的來源,這對我們模型訓練不重要。owner同樣也是不重要的信息。

<size>
<width>1024</width>
<height>1024</height>
<depth>3</depth>
</size>

是非常重要的信息,它包含了當前圖片的尺寸以及通道數。

<object>
<name>face</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>495</xmin>
<ymin>347</ymin>
<xmax>532</xmax>
<ymax>398</ymax>
</bndbox>
</object>

也是非常重要的信息,它包含了當前圖片需要檢測的信息。當前圖片存在多少個物體就會有多少個object,每一個object都對應到了一個物體的標註信息。在人臉檢測中會將object的name定義爲face。pose表示了姿態,truncated表示是否產生了截斷,difficult表示了當前是否是一個難例。我們重點關注的是bounding box的信息bndbox,它包括了物體矩形框的兩個座標值,一個是左上角的座標,一個是右下角的座標,所以我們在數據集轉化的時候,需要將widerface的四個值(x,y,w,h)轉化成兩個座標。

而在生成的ImageSets/Main/train.txt文件中的內容如下

0--Parade_0_Parade_marchingband_1_849
0--Parade_0_Parade_Parade_0_904
0--Parade_0_Parade_marchingband_1_799
0--Parade_0_Parade_marchingband_1_117
0--Parade_0_Parade_marchingband_1_778
0--Parade_0_Parade_Parade_0_343
0--Parade_0_Parade_marchingband_1_205
0--Parade_0_Parade_Parade_0_106
0--Parade_0_Parade_Parade_0_476
0--Parade_0_Parade_marchingband_1_12
0--Parade_0_Parade_marchingband_1_273
0--Parade_0_Parade_marchingband_1_928
0--Parade_0_Parade_Parade_0_337
0--Parade_0_Parade_marchingband_1_579
0--Parade_0_Parade_Parade_0_90
0--Parade_0_Parade_marchingband_1_300
0--Parade_0_Parade_Parade_0_782
0--Parade_0_Parade_Parade_0_449
0--Parade_0_Parade_Parade_0_325
0--Parade_0_Parade_Parade_0_136
0--Parade_0_Parade_Parade_0_1014
0--Parade_0_Parade_marchingband_1_454
0--Parade_0_Parade_marchingband_1_483

我們可以看到它就對應了圖片文件和標註文件對應的文件名。

Tensorflow+SSD源碼框架解讀

我們先在models-master/research/object_detection/dataset_tools目錄下有一個create_pascal_tf_record.py的文件,它是一個用於將Passcal VOC文件轉化成tf_record文件格式的腳本。有關tf_record的內容請參考Tensorflow技術點整理 中的tfrecord基礎API。

我們來看一下這個文件

SETS = ['train', 'val', 'trainval', 'test']
YEARS = ['VOC2007', 'VOC2012', 'merged']

這是入參的兩個列表。YEARS代表了Passcal VOC的年份信息。我們又可以在其中看到這樣一段代碼

example = tf.train.Example(features=tf.train.Features(feature={
    'image/height': dataset_util.int64_feature(height),
    'image/width': dataset_util.int64_feature(width),
    'image/filename': dataset_util.bytes_feature(
        data['filename'].encode('utf8')),
    'image/source_id': dataset_util.bytes_feature(
        data['filename'].encode('utf8')),
    'image/key/sha256': dataset_util.bytes_feature(key.encode('utf8')),
    'image/encoded': dataset_util.bytes_feature(encoded_jpg),
    'image/format': dataset_util.bytes_feature('jpeg'.encode('utf8')),
    'image/object/bbox/xmin': dataset_util.float_list_feature(xmin),
    'image/object/bbox/xmax': dataset_util.float_list_feature(xmax),
    'image/object/bbox/ymin': dataset_util.float_list_feature(ymin),
    'image/object/bbox/ymax': dataset_util.float_list_feature(ymax),
    'image/object/class/text': dataset_util.bytes_list_feature(classes_text),
    'image/object/class/label': dataset_util.int64_list_feature(classes),
    'image/object/difficult': dataset_util.int64_list_feature(difficult_obj),
    'image/object/truncated': dataset_util.int64_list_feature(truncated),
    'image/object/view': dataset_util.bytes_list_feature(poses),
}))

這裏的

'image/height': dataset_util.int64_feature(height)

其實就是

def int64_feature(value):
  return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))

這跟我們在Tensorflow技術點整理中的寫法是一致的。從內容上看,它包含了圖片的信息,bounding box的信息以及object的一些信息來構成了tf_record的特徵。那麼我們如何將該腳本用於我們自己的數據集轉化成tf_record呢?我們需要對該文件的代碼內容作出一定的修改。

首先我們需要先創建一個輸出tf_record的目錄

mkdir widerface/tf_record

然後在dataset_tools文件夾下新建一個create_face_tf_record.py的Python文件,將create_pascal_tf_record.py的所有代碼拷貝過來進行修改

再把環境變量執行一下

export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/slim
cd object_detection/dataset_tools/
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import hashlib
import io
import logging
import os

from lxml import etree
import PIL.Image
import tensorflow.compat.v1 as tf

from object_detection.utils import dataset_util
from object_detection.utils import label_map_util


flags = tf.app.flags
# 修改處
flags.DEFINE_string('data_dir', '/Users/admin/Downloads/', 'Root directory to raw PASCAL VOC dataset.')
flags.DEFINE_string('set', 'train', 'Convert training set, validation set or '
                    'merged set.')
flags.DEFINE_string('annotations_dir', 'Annotations',
                    '(Relative) path to annotations directory.')
# 修改處
flags.DEFINE_string('year', 'widerface', 'Desired challenge year.')
# 修改處
flags.DEFINE_string('output_path', '/Users/admin/Downloads/widerface/tf_record/widerface.tfrecords', 'Path to output TFRecord')
# 修改處
flags.DEFINE_string('label_map_path', '../data/face_label_map.pbtxt',
                    'Path to label map proto')
flags.DEFINE_boolean('ignore_difficult_instances', False, 'Whether to ignore '
                     'difficult instances')
FLAGS = flags.FLAGS

SETS = ['train', 'val', 'trainval', 'test']
# 修改處
YEARS = ['fddb', 'widerface']


def dict_to_tf_example(data,
                       dataset_directory,
                       label_map_dict,
                       ignore_difficult_instances=False,
                       image_subdirectory='JPEGImages'):
  img_path = os.path.join(data['folder'], image_subdirectory, data['filename'])
  full_path = os.path.join(dataset_directory, img_path)
  with tf.gfile.GFile(full_path, 'rb') as fid:
    encoded_jpg = fid.read()
  encoded_jpg_io = io.BytesIO(encoded_jpg)
  image = PIL.Image.open(encoded_jpg_io)
  if image.format != 'JPEG':
    raise ValueError('Image format not JPEG')
  key = hashlib.sha256(encoded_jpg).hexdigest()

  width = int(data['size']['width'])
  height = int(data['size']['height'])

  xmin = []
  ymin = []
  xmax = []
  ymax = []
  classes = []
  classes_text = []
  truncated = []
  poses = []
  difficult_obj = []
  if 'object' in data:
    for obj in data['object']:
      difficult = bool(int(obj['difficult']))
      if ignore_difficult_instances and difficult:
        continue

      difficult_obj.append(int(difficult))

      xmin.append(float(obj['bndbox']['xmin']) / width)
      ymin.append(float(obj['bndbox']['ymin']) / height)
      xmax.append(float(obj['bndbox']['xmax']) / width)
      ymax.append(float(obj['bndbox']['ymax']) / height)
      classes_text.append(obj['name'].encode('utf8'))
      classes.append(label_map_dict[obj['name']])
      truncated.append(int(obj['truncated']))
      poses.append(obj['pose'].encode('utf8'))

  example = tf.train.Example(features=tf.train.Features(feature={
      'image/height': dataset_util.int64_feature(height),
      'image/width': dataset_util.int64_feature(width),
      'image/filename': dataset_util.bytes_feature(
          data['filename'].encode('utf8')),
      'image/source_id': dataset_util.bytes_feature(
          data['filename'].encode('utf8')),
      'image/key/sha256': dataset_util.bytes_feature(key.encode('utf8')),
      'image/encoded': dataset_util.bytes_feature(encoded_jpg),
      'image/format': dataset_util.bytes_feature('jpeg'.encode('utf8')),
      'image/object/bbox/xmin': dataset_util.float_list_feature(xmin),
      'image/object/bbox/xmax': dataset_util.float_list_feature(xmax),
      'image/object/bbox/ymin': dataset_util.float_list_feature(ymin),
      'image/object/bbox/ymax': dataset_util.float_list_feature(ymax),
      'image/object/class/text': dataset_util.bytes_list_feature(classes_text),
      'image/object/class/label': dataset_util.int64_list_feature(classes),
      'image/object/difficult': dataset_util.int64_list_feature(difficult_obj),
      'image/object/truncated': dataset_util.int64_list_feature(truncated),
      'image/object/view': dataset_util.bytes_list_feature(poses),
  }))
  return example


def main(_):
  if FLAGS.set not in SETS:
    raise ValueError('set must be in : {}'.format(SETS))
  if FLAGS.year not in YEARS:
    raise ValueError('year must be in : {}'.format(YEARS))

  data_dir = FLAGS.data_dir
  # 修改處
  years = ['fddb', 'widerface']
  if FLAGS.year != 'merged':
    years = [FLAGS.year]

  writer = tf.python_io.TFRecordWriter(FLAGS.output_path)

  label_map_dict = label_map_util.get_label_map_dict(FLAGS.label_map_path)

  for year in years:
    logging.info('Reading from PASCAL %s dataset.', year)
    # 修改處
    examples_path = os.path.join(data_dir, year, 'ImageSets', 'Main',
                                 FLAGS.set + '.txt')
    annotations_dir = os.path.join(data_dir, year, FLAGS.annotations_dir)
    examples_list = dataset_util.read_examples_list(examples_path)
    for idx, example in enumerate(examples_list):
      if idx % 100 == 0:
        logging.info('On image %d of %d', idx, len(examples_list))
      path = os.path.join(annotations_dir, example + '.xml')
      with tf.gfile.GFile(path, 'r') as fid:
        xml_str = fid.read()
      xml = etree.fromstring(xml_str)
      data = dataset_util.recursive_parse_xml_to_dict(xml)['annotation']

      tf_example = dict_to_tf_example(data, FLAGS.data_dir, label_map_dict,
                                      FLAGS.ignore_difficult_instances)
      writer.write(tf_example.SerializeToString())

  writer.close()


if __name__ == '__main__':
  tf.app.run()

在命令行模式下執行(此處不要直接在PyCharm執行)

python create_face_tf_record.py

執行日誌(部分)

I1203 03:42:34.626746 4456746432 create_face_tf_record.py:169] Reading from PASCAL widerface dataset.
I1203 03:42:34.656785 4456746432 create_face_tf_record.py:177] On image 0 of 12880
/Users/admin/Downloads/models-master/research/object_detection/utils/dataset_util.py:83: FutureWarning: The behavior of this method will change in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.
  if not xml:
I1203 03:42:35.088140 4456746432 create_face_tf_record.py:177] On image 100 of 12880
I1203 03:42:35.442538 4456746432 create_face_tf_record.py:177] On image 200 of 12880
I1203 03:42:35.795451 4456746432 create_face_tf_record.py:177] On image 300 of 12880
I1203 03:42:36.170350 4456746432 create_face_tf_record.py:177] On image 400 of 12880
I1203 03:42:36.515269 4456746432 create_face_tf_record.py:177] On image 500 of 12880
I1203 03:42:36.843372 4456746432 create_face_tf_record.py:177] On image 600 of 12880
I1203 03:42:37.196257 4456746432 create_face_tf_record.py:177] On image 700 of 12880
I1203 03:42:37.540148 4456746432 create_face_tf_record.py:177] On image 800 of 12880
I1203 03:42:37.878384 4456746432 create_face_tf_record.py:177] On image 900 of 12880
I1203 03:42:38.230068 4456746432 create_face_tf_record.py:177] On image 1000 of 12880
I1203 03:42:38.592378 4456746432 create_face_tf_record.py:177] On image 1100 of 12880
I1203 03:42:38.944746 4456746432 create_face_tf_record.py:177] On image 1200 of 12880
I1203 03:42:39.302329 4456746432 create_face_tf_record.py:177] On image 1300 of 12880

如此便生成了Tensorflow自帶的tf_record格式的文件

我們在接下來訓練的時候就會用到該文件。

如果是test的轉換,需要修改如下

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import hashlib
import io
import logging
import os

from lxml import etree
import PIL.Image
import tensorflow.compat.v1 as tf

from object_detection.utils import dataset_util
from object_detection.utils import label_map_util


flags = tf.app.flags
# 修改處
flags.DEFINE_string('data_dir', '/Users/admin/Downloads/', 'Root directory to raw PASCAL VOC dataset.')
# 修改處
flags.DEFINE_string('set', 'test', 'Convert training set, validation set or '
                    'merged set.')
flags.DEFINE_string('annotations_dir', 'Annotations',
                    '(Relative) path to annotations directory.')
# 修改處
flags.DEFINE_string('year', 'widerface', 'Desired challenge year.')
# 修改處
flags.DEFINE_string('output_path', '/Users/admin/Downloads/widerface/tf_record/test.tfrecords', 'Path to output TFRecord')
# 修改處
flags.DEFINE_string('label_map_path', '../data/face_label_map.pbtxt',
                    'Path to label map proto')
flags.DEFINE_boolean('ignore_difficult_instances', False, 'Whether to ignore '
                     'difficult instances')
FLAGS = flags.FLAGS

SETS = ['train', 'val', 'trainval', 'test']
# 修改處
YEARS = ['fddb', 'widerface']


def dict_to_tf_example(data,
                       dataset_directory,
                       label_map_dict,
                       ignore_difficult_instances=False,
                       image_subdirectory='JPEGImages'):
  img_path = os.path.join(data['folder'], image_subdirectory, data['filename'])
  full_path = os.path.join(dataset_directory, img_path)
  with tf.gfile.GFile(full_path, 'rb') as fid:
    encoded_jpg = fid.read()
  encoded_jpg_io = io.BytesIO(encoded_jpg)
  image = PIL.Image.open(encoded_jpg_io)
  if image.format != 'JPEG':
    raise ValueError('Image format not JPEG')
  key = hashlib.sha256(encoded_jpg).hexdigest()

  width = int(data['size']['width'])
  height = int(data['size']['height'])

  xmin = []
  ymin = []
  xmax = []
  ymax = []
  classes = []
  classes_text = []
  truncated = []
  poses = []
  difficult_obj = []
  if 'object' in data:
    for obj in data['object']:
      difficult = bool(int(obj['difficult']))
      if ignore_difficult_instances and difficult:
        continue

      difficult_obj.append(int(difficult))

      xmin.append(float(obj['bndbox']['xmin']) / width)
      ymin.append(float(obj['bndbox']['ymin']) / height)
      xmax.append(float(obj['bndbox']['xmax']) / width)
      ymax.append(float(obj['bndbox']['ymax']) / height)
      classes_text.append(obj['name'].encode('utf8'))
      classes.append(label_map_dict[obj['name']])
      truncated.append(int(obj['truncated']))
      poses.append(obj['pose'].encode('utf8'))

  example = tf.train.Example(features=tf.train.Features(feature={
      'image/height': dataset_util.int64_feature(height),
      'image/width': dataset_util.int64_feature(width),
      'image/filename': dataset_util.bytes_feature(
          data['filename'].encode('utf8')),
      'image/source_id': dataset_util.bytes_feature(
          data['filename'].encode('utf8')),
      'image/key/sha256': dataset_util.bytes_feature(key.encode('utf8')),
      'image/encoded': dataset_util.bytes_feature(encoded_jpg),
      'image/format': dataset_util.bytes_feature('jpeg'.encode('utf8')),
      'image/object/bbox/xmin': dataset_util.float_list_feature(xmin),
      'image/object/bbox/xmax': dataset_util.float_list_feature(xmax),
      'image/object/bbox/ymin': dataset_util.float_list_feature(ymin),
      'image/object/bbox/ymax': dataset_util.float_list_feature(ymax),
      'image/object/class/text': dataset_util.bytes_list_feature(classes_text),
      'image/object/class/label': dataset_util.int64_list_feature(classes),
      'image/object/difficult': dataset_util.int64_list_feature(difficult_obj),
      'image/object/truncated': dataset_util.int64_list_feature(truncated),
      'image/object/view': dataset_util.bytes_list_feature(poses),
  }))
  return example


def main(_):
  if FLAGS.set not in SETS:
    raise ValueError('set must be in : {}'.format(SETS))
  if FLAGS.year not in YEARS:
    raise ValueError('year must be in : {}'.format(YEARS))

  data_dir = FLAGS.data_dir
  # 修改處
  years = ['fddb', 'widerface']
  if FLAGS.year != 'merged':
    years = [FLAGS.year]

  writer = tf.python_io.TFRecordWriter(FLAGS.output_path)

  label_map_dict = label_map_util.get_label_map_dict(FLAGS.label_map_path)

  for year in years:
    logging.info('Reading from PASCAL %s dataset.', year)
    # 修改處
    examples_path = os.path.join(data_dir, year, 'ImageSets', 'Main',
                                 FLAGS.set + '.txt')
    annotations_dir = os.path.join(data_dir, year, FLAGS.annotations_dir)
    examples_list = dataset_util.read_examples_list(examples_path)
    for idx, example in enumerate(examples_list):
      if idx % 100 == 0:
        logging.info('On image %d of %d', idx, len(examples_list))
      path = os.path.join(annotations_dir, example + '.xml')
      with tf.gfile.GFile(path, 'r') as fid:
        xml_str = fid.read()
      xml = etree.fromstring(xml_str)
      data = dataset_util.recursive_parse_xml_to_dict(xml)['annotation']

      tf_example = dict_to_tf_example(data, FLAGS.data_dir, label_map_dict,
                                      FLAGS.ignore_difficult_instances)
      writer.write(tf_example.SerializeToString())

  writer.close()


if __name__ == '__main__':
  tf.app.run()

執行完成後會產生該文件

我們在完成了以上數據轉換之後,就可以利用tensorflow的目標檢測接口函數來完成訓練。我們可以先看一下框架代碼的object_detection目錄下的model_main_tf2.py腳本。在這個腳本中提供了接下來訓練的SSD的框架,具體如何去選擇網絡模型以及對模型的參數如何進行設置,這些都是通過入參和相應的配置文件來完成對整個人臉檢測問題的配置。

我們進入object_detection/samples/configs目錄下可以看到有很多的目標檢測網絡模型的配置文件

這裏我們選擇ResNet50來作爲SSD的主幹網絡,我們可以看到ssd_resnet50_v1_fpn_shared_box_predictor_640x640_coco14_sync.config的配置文件,但是實際上該配置文件並不適合用於這次的人臉檢測任務,我們需要對其進行修改。首先,我們需要在object_detection/samples/configs目錄下新建一個ssd_resnet50_v1_fpn_shared_box_predictor_640x640_coco14_sync_face.confiig的配置文件,然後將ssd_resnet50_v1_fpn_shared_box_predictor_640x640_coco14_sync.config的內容全部拷貝過來進行修改。

ssd {
  inplace_batchnorm_update: true
  freeze_batchnorm: false
  num_classes: 1
  box_coder {
    faster_rcnn_box_coder {
      y_scale: 10.0
      x_scale: 10.0
      height_scale: 5.0
      width_scale: 5.0
    }
  }

對於人臉檢測任務來說,檢測目標只有1個即爲人臉,所以num_classes需要改成1,這裏需要注意的是目標檢測是不包含背景的。

train_input_reader: {
  tf_record_input_reader {
    input_path: "/Users/admin/Downloads/widerface/tf_record/widerface.tfrecords"
  }
  label_map_path: "/Users/admin/Downloads/models-master/research/object_detection/data/face_label_map.pbtxt"
}
eval_input_reader: {
  tf_record_input_reader {
    input_path: "/Users/admin/Downloads/widerface/tf_record/test.tfrecords"
  }
  label_map_path: "/Users/admin/Downloads/models-master/research/object_detection/data/face_label_map.pbtxt"
  shuffle: false
  num_readers: 1
}

然後是我們之前轉化好的訓練集和測試集的tf_record文件路徑以及相應人臉檢測的label_map。

train_config: {
  # fine_tune_checkpoint: "PATH_TO_BE_CONFIGURED/model.ckpt"
  batch_size: 64
  sync_replicas: true
  startup_delay_steps: 0
  replicas_to_aggregate: 8
  num_steps: 25000
  data_augmentation_options {
    random_horizontal_flip {
    }
  }

另外由於我們不使用預訓練模型,所以需要將預訓練模型相應的配置給註釋掉。然後就可以進行訓練了,先建立一個存放模型的文件夾

cd Downloads/widerface/
mkdir resnet50v1-fpn

 這個是tensorflow 1的配置文件,由於我們使用的是tensorflow 2,所以我們需要另外下載一個fensorflow 2的配置,進入https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/tf2_detection_zoo.md,這裏我們依然使用ResNet網絡來作爲主幹網絡,所以我們下載的是

下載解壓後將其拷貝到object_detection目錄下,並將pipeline.config文件重新命名爲ssd_resnet50_v1_fpn_640x640_coco17_tpu-8.config

cd models-master/research/object_detection
cp -r /Users/admin/Downloads/ssd_resnet50_v1_fpn_640x640_coco17_tpu-8 ./
cd ssd_resnet50_v1_fpn_640x640_coco17_tpu-8/
mv pipeline.config ssd_resnet50_v1_fpn_640x640_coco17_tpu-8.config

同樣要修改的地方爲

ssd {
  num_classes: 1
  image_resizer {
    fixed_shape_resizer {
      height: 640
      width: 640
    }
  }
train_input_reader {
  label_map_path: "/Users/admin/Downloads/models-master/research/object_detection/data/face_label_map.pbtxt"
  tf_record_input_reader {
    input_path: "/Users/admin/Downloads/widerface/tf_record/widerface.tfrecords"
  }
}
eval_input_reader {
  label_map_path: "/Users/admin/Downloads/models-master/research/object_detection/data/face_label_map.pbtxt"
  shuffle: false
  num_epochs: 1
  tf_record_input_reader {
    input_path: "/Users/admin/Downloads/widerface/tf_record/test.tfrecords"
  }
fine_tune_checkpoint: "/Users/admin/Downloads/models-master/research/object_detection/ssd_resnet50_v1_fpn_640x640_coco17_tpu-8/checkpoint/ckpt-0"
fine_tune_checkpoint_type: "detection"
num_steps: 100000
optimizer {
  momentum_optimizer {
    learning_rate {
      cosine_decay_learning_rate {
        learning_rate_base: 0.03999999910593033
        total_steps: 100000
        warmup_learning_rate: 0.013333000242710114
        warmup_steps: 2000
      }
    }
    momentum_optimizer_value: 0.8999999761581421
  }
  use_moving_average: false
}

安裝coco-api

git clone https://github.com/cocodataset/cocoapi.git
cd cocoapi/PythonAPI/

mac下需要安裝xcrun

xcode-select --install

執行安裝coco-api並拷貝到框架源碼目錄下

make
cp -r pycocotools /Users/admin/Downloads/models-master/research/
pip install gin-config==0.1.1
pip install tensorflow-addons

然後進入models-master/research/目錄下執行

python object_detection/model_main_tf2.py --pipeline_config_path=/Users/admin/Downloads/models-master/research/object_detection/ssd_resnet50_v1_fpn_640x640_coco17_tpu-8/ssd_resnet50_v1_fpn_640x640_coco17_tpu-8.config --model_dir=/Users/admin/Downloads/widerface/resnet50v1-fpn --num_train_steps=100000 --sample_1_of_n_eval_examples=1  --alsologtostderr

現在我們來看一下SSD網絡ssd_resnet50_v1_fpn_640x640_coco17_tpu-8.config配置內容,更詳細的瞭解裏面的內容方便我們可以更好的調優。

model {
  ssd {
    num_classes: 1
    image_resizer {
      fixed_shape_resizer {
        height: 640
        width: 640
      }
    }
    feature_extractor {
      type: "ssd_resnet50_v1_fpn_keras"
      depth_multiplier: 1.0
      min_depth: 16
      conv_hyperparams {
        regularizer {
          l2_regularizer {
            weight: 0.00039999998989515007
          }
        }
        initializer {
          truncated_normal_initializer {
            mean: 0.0
            stddev: 0.029999999329447746
          }
        }
        activation: RELU_6
        batch_norm {
          decay: 0.996999979019165
          scale: true
          epsilon: 0.0010000000474974513
        }
      }
      override_base_feature_extractor_hyperparams: true
      fpn {
        min_level: 3
        max_level: 7
      }
    }
    box_coder {
      faster_rcnn_box_coder {
        y_scale: 10.0
        x_scale: 10.0
        height_scale: 5.0
        width_scale: 5.0
      }
    }
    matcher {
      argmax_matcher {
        matched_threshold: 0.5
        unmatched_threshold: 0.5
        ignore_thresholds: false
        negatives_lower_than_unmatched: true
        force_match_for_each_row: true
        use_matmul_gather: true
      }
    }
    similarity_calculator {
      iou_similarity {
      }
    }
    box_predictor {
      weight_shared_convolutional_box_predictor {
        conv_hyperparams {
          regularizer {
            l2_regularizer {
              weight: 0.00039999998989515007
            }
          }
          initializer {
            random_normal_initializer {
              mean: 0.0
              stddev: 0.009999999776482582
            }
          }
          activation: RELU_6
          batch_norm {
            decay: 0.996999979019165
            scale: true
            epsilon: 0.0010000000474974513
          }
        }
        depth: 256
        num_layers_before_predictor: 4
        kernel_size: 3
        class_prediction_bias_init: -4.599999904632568
      }
    }
    anchor_generator {
      multiscale_anchor_generator {
        min_level: 3
        max_level: 7
        anchor_scale: 4.0
        aspect_ratios: 1.0
        aspect_ratios: 2.0
        aspect_ratios: 0.5
        scales_per_octave: 2
      }
    }
    post_processing {
      batch_non_max_suppression {
        score_threshold: 9.99999993922529e-09
        iou_threshold: 0.6000000238418579
        max_detections_per_class: 100
        max_total_detections: 100
        use_static_shapes: false
      }
      score_converter: SIGMOID
    }
    normalize_loss_by_num_matches: true
    loss {
      localization_loss {
        weighted_smooth_l1 {
        }
      }
      classification_loss {
        weighted_sigmoid_focal {
          gamma: 2.0
          alpha: 0.25
        }
      }
      classification_weight: 1.0
      localization_weight: 1.0
    }
    encode_background_as_zeros: true
    normalize_loc_loss_by_codesize: true
    inplace_batchnorm_update: true
    freeze_batchnorm: false
  }
}
train_config {
  batch_size: 64
  data_augmentation_options {
    random_horizontal_flip {
    }
  }
  data_augmentation_options {
    random_crop_image {
      min_object_covered: 0.0
      min_aspect_ratio: 0.75
      max_aspect_ratio: 3.0
      min_area: 0.75
      max_area: 1.0
      overlap_thresh: 0.0
    }
  }
  sync_replicas: true
  optimizer {
    momentum_optimizer {
      learning_rate {
        cosine_decay_learning_rate {
          learning_rate_base: 0.03999999910593033
          total_steps: 100000
          warmup_learning_rate: 0.013333000242710114
          warmup_steps: 2000
        }
      }
      momentum_optimizer_value: 0.8999999761581421
    }
    use_moving_average: false
  }
  fine_tune_checkpoint: "/Users/admin/Downloads/models-master/research/object_detection/ssd_resnet50_v1_fpn_640x640_coco17_tpu-8/checkpoint/ckpt-0"
  num_steps: 100000
  startup_delay_steps: 0.0
  replicas_to_aggregate: 8
  max_number_of_boxes: 100
  unpad_groundtruth_tensors: false
  fine_tune_checkpoint_type: "detection"
  use_bfloat16: true
  fine_tune_checkpoint_version: V2
}
train_input_reader {
  label_map_path: "/Users/admin/Downloads/models-master/research/object_detection/data/face_label_map.pbtxt"
  tf_record_input_reader {
    input_path: "/Users/admin/Downloads/widerface/tf_record/widerface.tfrecords"
  }
}
eval_config {
  metrics_set: "coco_detection_metrics"
  use_moving_averages: false
}
eval_input_reader {
  label_map_path: "/Users/admin/Downloads/models-master/research/object_detection/data/face_label_map.pbtxt"
  shuffle: false
  num_epochs: 1
  tf_record_input_reader {
    input_path: "/Users/admin/Downloads/widerface/tf_record/test.tfrecords"
  }
}

首先

image_resizer {
  fixed_shape_resizer {
    height: 640
    width: 640
  }
}

表示輸入網絡的圖片大小,這裏是640*640,一般該值越大,訓練出來的精度越高,但訓練速度也越慢。一般爲128的整數倍,如256*256、384*384、512*512等等,這樣做的好處就是在輸入的feature map在128位的下采樣的時候,能夠保證輸出都是2的整數次冪,這樣能夠保證精度不會有損失。在進行Anchor提取的時候,每一個pix anchor所對應的在原始圖像上的區域都是非常明確的,每個點所對應的感受野都是一個整數倍。重點優化點在於height: 640、width: 640圖像尺寸,對於網絡的性能影響還是比較大的。

feature_extractor {
  type: "ssd_resnet50_v1_fpn_keras"
  depth_multiplier: 1.0
  min_depth: 16
  conv_hyperparams {
    regularizer {
      l2_regularizer {
        weight: 0.00039999998989515007
      }
    }
    initializer {
      truncated_normal_initializer {
        mean: 0.0
        stddev: 0.029999999329447746
      }
    }
    activation: RELU_6
    batch_norm {
      decay: 0.996999979019165
      scale: true
      epsilon: 0.0010000000474974513
    }
  }
  override_base_feature_extractor_hyperparams: true
  fpn {
    min_level: 3
    max_level: 7
  }
}

feature_extractor表示特徵提取層,也就是主幹網絡層,這裏採用的是ResNet50 fpn的結構,所以定義type: "ssd_resnet50_v1_fpn_keras"。fpn考慮的是網絡的第3層到第7層,也就是說下采樣2^3到2^7,也就是說這5個尺度上的特徵圖。min_depth: 16表示在主幹網絡層最小的通道數爲16,depth_multiplier: 1.0表示網絡的寬度是1.0,也就是正常的寬度。conv_hyperparams定義了卷積的參數,比如激活函數activation: RELU_6,加入了L2正則l2_regularizer,採用了批歸一化batch_norm,有關批歸一化的內容可以參考Tensorflow技術點整理 中的歸一化與批歸一化。重點優化點在於min_depth: 16卷積核的通道數以及fpn中min_level: 3和max_level: 7所對應的數量。通過這裏主要是壓縮計算量。

而對於type: "ssd_resnet50_v1_fpn_keras"這個網絡結構是定義在object_detection/models目錄下的ssd_resnet_v1_fpn_feature_extractor.py中的,這裏我們可以看到有一個SSDResnet50V1FpnFeatureExtractor的類,就定義了我們使用的網絡爲resnet_v1_50

super(SSDResnet50V1FpnFeatureExtractor, self).__init__(
    is_training,
    depth_multiplier,
    min_depth,
    pad_to_multiple,
    conv_hyperparams_fn,
    resnet_v1.resnet_v1_50,
    'resnet_v1_50',
    'fpn',
    fpn_min_level,
    fpn_max_level,
    additional_layer_depth,
    reuse_weights=reuse_weights,
    use_explicit_padding=use_explicit_padding,
    use_depthwise=use_depthwise,
    use_native_resize_op=use_native_resize_op,
    override_base_feature_extractor_hyperparams=
    override_base_feature_extractor_hyperparams)
box_coder {
  faster_rcnn_box_coder {
    y_scale: 10.0
    x_scale: 10.0
    height_scale: 5.0
    width_scale: 5.0
  }
}

box_coder實際上是對於SSD預測的結果通常稱之爲Bounding box,它通常包括4個值,採用了Faster RCNN編碼的方式,對於預測出來的結果會進行歸一化的處理。這個歸一化處理並不是將結果約束到0~1之間,而是在其基礎上乘上了一個倍率,對於x、y scale有了10倍的放縮,對於寬和高有了5倍的放縮。對於bounding box放大它的尺度之後,對於最終的訓練結果不會有太大的影響,只是將要回歸的值變大之後就意味着loss也會相應的變大。整個訓練過程整個loss曲線會有一些細微的差異。

matcher {
  argmax_matcher {
    matched_threshold: 0.5
    unmatched_threshold: 0.5
    ignore_thresholds: false
    negatives_lower_than_unmatched: true
    force_match_for_each_row: true
    use_matmul_gather: true
  }
}

matcher對於整個訓練而言是非常關鍵的一組參數。在這理我們定義瞭如何去評判一個預測的人臉框它是否是真值,是否是正樣本還是負樣本。我們會利用預測出來的人臉框同真實的人臉框GT來進行比對,通過IOU來進行衡量,如果IOU匹配的閾值大於0.5,那我們就認爲它是一個正樣本;如果小於0.5,就認爲是一個負樣本。通過matched_threshold: 0.5和unmatched_threshold: 0.5這兩個值能夠完成樣本的正負性的判定,也就是樣本類別的判定,這個類別也就是樣本所對應的真值。在進行目標檢測任務的時候,怎麼樣算是識別正確呢?就是通過這種IOU的匹配度來決定的,並不是說兩個框一定要百分百的重合,只要在滿足約束條件下(就是這裏定義的閾值),就意味着檢測人臉框檢測正確。檢測正確的前提下,再通過IOU去衡量當前檢測的結果它的準確度或者精度,當然匹配度越高意味着模型的精度越高。重點優化點就在於matched_threshold: 0.5和unmatched_threshold: 0.5這兩個IOU匹配的閾值。

box_predictor {
  weight_shared_convolutional_box_predictor {
    conv_hyperparams {
      regularizer {
        l2_regularizer {
          weight: 0.00039999998989515007
        }
      }
      initializer {
        random_normal_initializer {
          mean: 0.0
          stddev: 0.009999999776482582
        }
      }
      activation: RELU_6
      batch_norm {
        decay: 0.996999979019165
        scale: true
        epsilon: 0.0010000000474974513
      }
    }
    depth: 256
    num_layers_before_predictor: 4
    kernel_size: 3
    class_prediction_bias_init: -4.599999904632568
  }
}

box_predictor定義了Prior box層,weight_shared_convolutional_box_predictor定義了卷積層是共享參數的。depth: 256表示它的輸出的通道數是256,num_layers_before_predictor: 4表示卷積層的層數爲4,kernel_size: 3表示卷積核的大小爲3*3,activation: RELU_6表示激活函數爲Relu。重點優化點在於depth: 256,如果想要壓縮計算量的話可以將該值調低一點。num_layers_before_predictor: 4卷基層的數量調少一點。

anchor_generator {
  multiscale_anchor_generator {
    min_level: 3
    max_level: 7
    anchor_scale: 4.0
    aspect_ratios: 1.0
    aspect_ratios: 2.0
    aspect_ratios: 0.5
    scales_per_octave: 2
  }
}

anchor_generator定義了Anchor機制,對於feature map上的每一個點都會作爲一個anchor,這裏會選擇不同的參數來選擇bounding box。這裏定義了anchor尺寸anchor_scale,長寬比aspect_ratios。通過長寬比和尺寸,就能夠對anchor的大小進行計算。人臉框的比例也就是1:1,1:2,最大也不會超過1:3的尺寸,所以定義長寬比的時候定義了1:1,1:2,2:1這三個比例。重點優化的地方就是anchor_scale: 4.0、aspect_ratios: 1.0、aspect_ratios: 2.0、aspect_ratios: 0.5。考慮到人臉尺寸的長寬比例,將這個屬性可以調的更低一點。比如1.0可以調成1.5,0.5可以調成0.7,0.8。

post_processing {
  batch_non_max_suppression {
    score_threshold: 9.99999993922529e-09
    iou_threshold: 0.6000000238418579
    max_detections_per_class: 100
    max_total_detections: 100
    use_static_shapes: false
  }
  score_converter: SIGMOID
}

post_processing定義了NMS的部分,這裏最重要的就是IOU的閾值iou_threshold: 0.6000000238418579,人臉檢測的問題和通用物體檢測的問題區別就在這裏,在人臉檢測問題中該值可以i調低一點,主要問題就是在人臉檢測中可能會存在小人臉的結構,這些小人臉的結構就意味着人臉框的尺寸是非常小的,尺寸小就意味着在計算IOU的時候是比較敏感的。在這裏將該值調低能夠更好的保證人臉的檢出率。重點優化的地方就在於iou_threshold: 0.6000000238418579,我們可以i將IOU的閾值調低一點。

loss {
  localization_loss {
    weighted_smooth_l1 {
    }
  }
  classification_loss {
    weighted_sigmoid_focal {
      gamma: 2.0
      alpha: 0.25
    }
  }
  classification_weight: 1.0
  localization_weight: 1.0
}

loss代表損失函數,對於迴歸損失函數localization_loss採用的是smooth_l1,對於分類損失函數classification_loss採用了focal loss,focal loss考慮了正負樣本的比率的問題,對於樣本不平衡問題有一個比較好的性能上的提升。重點優化的地方就在於classification_weight: 1.0和localization_weight: 1.0,我們可以調整兩個損失函數的權重比例來對我們的模型產生一定的影響。

train_config {
  batch_size: 64
  data_augmentation_options {
    random_horizontal_flip {
    }
  }
  data_augmentation_options {
    random_crop_image {
      min_object_covered: 0.0
      min_aspect_ratio: 0.75
      max_aspect_ratio: 3.0
      min_area: 0.75
      max_area: 1.0
      overlap_thresh: 0.0
    }
  }
  sync_replicas: true
  optimizer {
    momentum_optimizer {
      learning_rate {
        cosine_decay_learning_rate {
          learning_rate_base: 0.03999999910593033
          total_steps: 100000
          warmup_learning_rate: 0.013333000242710114
          warmup_steps: 2000
        }
      }
      momentum_optimizer_value: 0.8999999761581421
    }
    use_moving_average: false
  }
  fine_tune_checkpoint: "/Users/admin/Downloads/models-master/research/object_detection/ssd_resnet50_v1_fpn_640x640_coco17_tpu-8/checkpoint/ckpt-0"
  num_steps: 100000
  startup_delay_steps: 0.0
  replicas_to_aggregate: 8
  max_number_of_boxes: 100
  unpad_groundtruth_tensors: false
  fine_tune_checkpoint_type: "detection"
  use_bfloat16: true
  fine_tune_checkpoint_version: V2
}

train_config定義了訓練的參數,batch_size: 64表示分批訓練的樣本數量,num_steps: 100000表示最大的訓練迭代次數,data_augmentation_options數據增強用到了crop,優化器optimizer採用的是momentum優化器,並傳入了學習率的參數,初始值爲0.03999999910593033,迭代次數total_steps: 100000,迭代步長warmup_learning_rate: 0.013333000242710114。重點的優化點就在於batch_size: 64,在我們顯存容量允許的條件下,儘可能將其調大。另外我們可以加入更多的數據增強的方法,這裏實際上只使用了兩種數據增強的方法。optimizer優化器,我們可以選擇一些其他的優化器來對比一下模型的性能。

模型轉化pb文件

在widerface/resnet50v1-fpn目錄下會生成一系列的文件,其中有一個checkpoint文件,裏面包含了下面的內容

model_checkpoint_path: "ckpt-1"
all_model_checkpoint_paths: "ckpt-1"
all_model_checkpoint_timestamps: 1638554680.600845

如果我們的模型經過了全面的訓練,則ckpt-後面的數字會比較大,代表着訓練次數,一般會到50000多次,但由於我這裏沒有進行全面訓練,所以這裏顯示的爲1。這個時候模型包含了除了checkpoint之外的這三個文件

在進行測試的時候,我們首先將這三個文件轉化成一個graph,即一個pb文件。然後再利用這個pb文件進行前向運算。tensorflow也提供了將當前的這三個文件轉成了pb文件的腳本。這個時候就可以將已經訓練好的模型的三個文件變成一個文件。我們在該目錄下新建一個文件夾pb

我們在object_detection目錄下會看到一個export_inference_graph.py的腳本文件。依然是回到research目錄下執行

export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/slim
python object_detection/export_inference_graph.py --input_type=image_tensor --pipeline_config_path=/Users/admin/Downloads/models-master/research/object_detection/ssd_resnet50_v1_fpn_640x640_coco17_tpu-8/ssd_resnet50_v1_fpn_640x640_coco17_tpu-8.config --trained_checkpoint_prefix=/Users/admin/Downloads/widerface/resnet50v1-fpn/ckpt-1 --output_directory=/Users/admin/Downloads/widerface/resnet50v1-fpn/pb

轉化完成後會在pb文件夾下面生成一個frozen_inference_graph.pb的文件。最後在opencv中調用模型

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    cap = cv2.VideoCapture("/Users/admin/Documents/video.mp4")
    config = "/Users/admin/Downloads/models-master/research/object_detection/data/face_label_map.pbtxt"
    model = "/Users/admin/Downloads/widerface/resnet50v1-fpn/pb/frozen_inference_graph.pb"
    # 加載tensorflow模型
    net = cv2.dnn.readNetFromTensorflow(model, config)
    while True:
        ret, frame = cap.read()
        if ret:
            image_height, image_width, _ = frame.shape
            # dnn.blobFromImage對圖像預處理,
            # scalefactor執行完減均值後,需要縮放圖像,默認是1,需要注意,scalefactor = 1 / σ,這是真正乘上的值。
            # size:輸入h和w的大小(輸入是300 * 300)
            # swapRB :如果執行減均值,通道順序是R、G、B。 如果,輸入圖像通道順序是B、G、R,那麼請確保swapRB = True,交換通道。
            # net.setInput輸入處理的結果
            net.setInput(cv2.dnn.blobFromImage(frame, 1, (300, 300), swapRB=True))
            # 預測,前向運算,輸出是一個4維的矩陣
            # 矩陣中向量的第3維表示有多少個推薦矩形框
            # 矩陣中向量的第4維是一個7維的向量,從第二維開始依次是
            # 分類標籤,置信度,推薦矩形框的x,y,w,h
            output = net.forward()
            for detection in output[0, 0, :, :]:
                # 獲取置信度
                confidence = detection[2]
                # 如果置信度大於0.5
                if confidence > .5:
                    # 獲取推薦矩形框的x,y,w,h
                    box_x = detection[3] * image_width
                    box_y = detection[4] * image_height
                    box_width = detection[5] * image_width
                    box_height = detection[6] * image_height
                    cv2.rectangle(frame, (int(box_x), int(box_y)), (int(box_width), int(box_height)), (0, 255, 0),
                                  2)
            cv2.imshow('img', frame)
        key = cv2.waitKey(20)
        if key & 0xFF == ord('q'):
            break
    cap.release()
    cv2.destroyAllWindows()

人臉匹配

  • 什麼是人臉匹配?

  1. 1:1驗證,verification,在上圖的上半部分中,輸入是一個人臉,要匹配也是一個人臉,我們要判斷這兩張臉是否是同一個人。我們將這種1:1的匹配問題定義爲人臉驗證
  2. 1:N識別,identification,在上圖的下半部分,輸入是一個人臉,我們根據數據庫中已經存在的N個人臉來去對當前輸入的這一張人臉的身份進行判定。我們將這種1:N的人臉匹配的問題定義爲人臉識別
  3. 人臉檢索,跟人臉識別類似,只不過我們會返回排序(Rank)前n個的人臉就可以了。對輸入的人臉和數據庫中的人臉的相似性進行計算,然後進行排序,我們稱這種人臉匹配的問題定義爲人臉檢索

人臉識別其實就是將排序第一的人臉加入一個閾值來進行判斷,如果它們的相似度不滿足條件,這個時候我們會認爲他不是數據庫中的人臉。

  • 人臉匹配算法性能評價指標
  1. 人臉驗證:ROC曲線,PR曲線。對於1:1的人臉匹配問題,是和否其實就是一個二分類問題,這時對於評價它的好壞,我們會採用ROC曲線或者PR曲線來進行驗證。有關ROC曲線和PR曲線可以參考機器學習算法整理(三) 。這裏我們同樣也會定義一個閾值,然後根據這個閾值來判斷"是"還是"不是"。不同的閾值就會對應到不同的P和R。這樣就可以根據不同的閾值的不同的P、R畫出PR曲線。同理ROC也是根據不同的閾值畫出不同的ROC曲線。根據這個指標我們就能評價一個人臉驗證算法模型的準確度或是精度。當然還有一個重要的指標就是FPS,包括模型的大小,模型的參數量,模型的計算量。我們會根據ROC曲線或是PR曲線來去決定最終算法模型選取什麼樣的閾值。這個閾值的選取是非常重要的,它能保證模型的精度或者是召回率。這個閾值也是工程實踐中非常重要的超參數。
  2. 人臉識別:CMC曲線,CMC曲線是衡量和排序相關的一些算法,比如說人臉識別、行人、圖像檢索等,這些方法以及度量等都可以i採用CMC曲線來進行衡量。

  • CMC曲線

在ImageNet挑戰賽中,一般會有兩個指標,一個是top1 error,一個是top5 error。

  1. 累積匹配曲線
  2. Top1 error,在進行類別判定的時候,會有一個類別的概率分佈,這個概率分佈表達了當前的圖片屬於某一個類別的概率值,概率最大的就是我們最終要選擇的類別。如果概率最大的那個類別所對應的值是錯誤的,此時Top1 error就會累積。最後錯誤樣本的數量/總樣本的數量=Top1 error。
  3. Top5 error,對於概率最大的前5個類別,只要包含當前這個物體的類別,此時我們就認爲它是對的。如果不包含則認爲是錯的。所以對於Top5 error會比Top1 error小。也就是說對於前5個只要有一個是對的,就認爲是對的。

上圖就是一個CMC曲線,它的縱座標表示準確度,也就是精度;橫座標爲Rank,也就是Top N error。縱座標=1-Top N error。比如說對於給定的一個人臉,對輸入的人臉進行特徵提取之後,去計算同數據庫裏的N個人臉之間的相似性之後對這個N個人臉進行排序,取Top N,如果是Top 1的話,就來統計在第一個樣本中同輸入的人臉是同一個人臉,就認爲匹配正確;如果不是同一個人臉,就認爲匹配錯誤,這個時候就會有一個Top 1的錯誤率或者是準確度,這個值就會繪製在CMC曲線的第一個點上。Top 2就是取前2個人臉,只要有一個是對的,就認爲匹配正確,否則就認爲匹配錯誤。對於輸入的M個人臉,同樣也可以去統計Top 2的錯誤率,就能繪製出Top 2的準確度。這個時候我們就能繪製出CMC曲線,從1到+∞的時候。CMC曲線一定是一個上升的曲線,也就是說Top N的錯誤率一定是比Top 1小的。錯誤率小就意味着準確率高。評價一個排序算法的好壞同樣是使用CMC曲線的面積來描述。

通常在實際項目中,我們會關心Top 1或者Top N(N≤10),因爲一般只會返回前10個結果,我們希望這個錯誤率越小越好。當然如果Top 1的錯誤率能夠達到非常準的話,這個算法也是更加好的。關於CMC曲線除了人臉匹配算法中會用到來評價它的效果的好壞,在行人檢測(person re-id)算法中通常也是使用CMC曲線來評價效果的好壞。

人臉匹配方法

它包括了兩個角度——特徵表示和相似性度量。特徵表示在一些傳統的計算機視覺中會用到的概念,相似性度量通常會和一些優化問題產生聯繫。在深度學習技術之前,在解決圖片的一些業務的時候,通常都可以劃分爲特徵表示、相似性度量或者是分類器這樣的不同的模塊。目前深度學習的方法,它實際上也離不開這樣的一個框架,只是在進行深度學習模型搭建的時候,會採用網絡,利用網絡來完成對於圖像特徵的抽取以及和相似性的度量。

  • 人臉特徵表示

對於一張數字圖片是由數字矩陣來構成的,對於當前的一張數字矩陣實際上是描述了當前圖片中的一些顏色信息,也就是上圖中的顏色特徵。根據這些顏色特徵,我們可以去提取顏色直方圖或者提取一些主顏色做color name。這些方法都是基於顏色的特徵矩陣來進行統計分析的。對於顏色特徵一般會輸出一個顏色向量,這個向量通常是一個直方圖。利用這個直方圖就能夠去表示一個圖片。我們認爲當前的這個直方圖是這個圖片中提取出來的一些特徵,這些特徵是具有一些魯棒性。比如說對於一張圖片,它具有一個數字矩陣,同時這張圖片經過了一定的旋轉,此時這個旋轉後的圖片的數字矩陣與之前的數字矩陣就不同了。但是如果我們對這兩個數字矩陣進行直方圖統計,我們會發現這兩個數字矩陣所對應的直方圖的顏色向量是一樣的(直方圖中的值都是排序過的),這個統計特徵就是顏色直方圖特徵。當然除了這種方式外還有很多其他的統計顏色特徵的方法,但最終都會得到一個顏色向量。我們稱這種直方圖統計的方法爲Patten,也就是所謂的模式識別

我們對顏色矩陣進行差分,會拿到紋理特徵,紋理特徵是更加細粒度的特徵。差分是相對於微分而言的,是研究離散型數字的工具,而微分是研究連續性函數。查分將原函數f(x) 映射到f(x+a)-f(x+b) 等差數列:a1 a2 a3……an……,其中an+1= an + d( n = 1,2,…n )d爲常數,稱爲公差, 即 d = an+1 -an , 這就是一個差分。拿到紋理特徵的好處,比如說當前的圖像矩陣,它受到了不同的光照的刺激,經過不同的光照之後,矩陣中的顏色值可能會相應的增高。此時我們不管是去統計頻次還是統計其他的一些信息,我們會發現原圖像矩陣和光照後的圖像矩陣存在很大的差異。這個時候我們對原圖像矩陣進行差分,即用矩陣相鄰的顏色值相減得到圖像矩陣的一些紋理信息。同樣我們會對光照後的圖像矩陣也進行差分,同樣也能夠拿到一組紋理信息,而這兩組紋理信息是相同的,則從紋理上來看,這兩張圖片還是同一個圖片。所以對於紋理特徵而言在解決一些光照問題而言也會有一些比較好的性質。除了光照以外對旋轉也會有一個比較好的適應。之前在傳統的圖像識別中的Haar,HOG都用到了紋理特徵。

形狀特徵是對圖像中的結構去進行的一些分析。數字圖像所獲取的一些圖像內容往往是一些物體或者一些場景。這些信息往往是會有一些共同的結構性質,比如說一個人的基本外形是比較固定的,這個時候我們去提取人的"骨架",也能夠拿到一個人的形狀。又比如一些圓形、三角形一些比較簡單的幾何形狀。這些都是我們如何去從當前的顏色圖像中去進行紋理特徵的提取,再進一步的提取形狀特徵。這些都是從當前的顏色矩陣去人爲的、手動的去設計出這樣的一些可以解釋的特徵。

在提取了上面的人爲設計的信息之後,我們還可以添加一些監督信息,類別信息,或者一些已知的先驗信息,利用這些信息對當前的特徵進行一些優化學習,在這個基礎上再進一步進行特徵的抽取,此時的特徵就是經過學習之後的特徵。這些特徵通常定義爲中層次的語義特徵。這些特徵因爲加入了一些監督的信息,即用到了樣本的標籤,這些特徵通常會比我們人爲的去設計的這些特徵會更加的魯棒。對於特徵的表示和學習是深度學習技術普及之前,大家在學術界研究最多的方法。很多優化的方法都是基於對特徵的學習來進行建模的。

  • 特徵表示的優化學習
  1. PCA,主成分,當前的樣本集很有可能存在噪聲。在進行主成分分析的時候,對特徵向量進行多個維度的投影,在投影之後會拿到它在不同維度上的空間分量,如果某個分量特別小,就有可能意味着當前這個分量是由噪聲產生的,所以通過PCA能夠完成特徵降維,取其中的幾個主要的成分。有關PCA的相關內容可以參考機器學習算法整理 中的PCA與梯度上升法
  2. LDA,PCA是無監督的,而LDA是有監督的,去想辦法優化特徵空間。對n維的特徵空間,會投影到m維的特徵空間上去,在n維上不可分的樣本,投影到m(m<n)維特徵空間上去就會變的可分。對於m維特徵空間來說,同類樣本的距離會更近,不同類樣本的距離會更遠。如果去聚類,則會有聚出不同的類別。在解決LDA問題的時候一般是採用SVD矩陣分解。LDA我們稱之爲判別分析或者是鑑別分析。除了這種高維向低維投影,還有低維向高維投影,比如SVM裏的核函數。SVM認爲樣本在低維空間裏不可分,但是總有一個高維空間裏使得這些低維空間裏不可分的樣本變的可分,此時就構造出了SVM的優化函數,並且通過核函數來進行求解。有關特徵空間的內容可以參考線性代數整理 中的空間,向量空間和歐幾里得空間。聚類可以參考聚類 。SVD矩陣分解可以參考線性代數整理(三) 中的對稱矩陣與矩陣的SVD分解。有關SVM核函數的內容可以參考機器學習算法整理(三) 中的SVM中使用多項式特徵和核函數。
  3. 遷移學習,對於空間上存在不同分佈的兩種不同的樣本A和B,此時我們將這兩種樣本向同一個空間投影成a和b,在它們共有的common(公共)子空間裏,我們認爲這兩種樣本是同分布的,同分布就意味着用a=λb表示,其中λ爲一個矩陣。這個等式就會用在遷移學習建模時候的一個約束。它就相當於是對當前條件的一個數學建模,有了這個建模之後就能構造出優化函數並且對這個優化函數進行合理的求解。
  4. 稀疏表示,在信號分解領域、圖像降噪領域用的比較多,屬於深度學習之前的一代技術。在正則化項中能夠通過稀疏約束去完成一個參數的約束。稀疏表示就是將當前的一個圖像特徵X分解成兩個矩陣D(詞典)和α(因子)再加上E(噪聲),即X=Dα+EL0。這裏E也是一個稀疏矩陣,所以會添加一個L0(0範式)。這個時候就能構造出一個稀疏表示的優化函數,完成對X矩陣的分解,去掉E,再將D和α重新相乘就會得到一個降噪之後的X矩陣。
  5. 低秩(low_rank)學習,對於圖像數據而言,它是具有一些局部的相似性的,這些局部的相似性表現在圖像矩陣中,就是當前的圖像矩陣是低秩的,圖像在不同行和列之間是有一個相似性的,這個相似性就有可能在數學上去進行一一表示,我們可以用其中圖像中的一行去表示另一行。如果我們可以用其中的一行去表示另一行,這個時候在求矩陣秩的時候,就不是滿秩的,所以對於圖像特徵會有一個低秩的性質。低秩約束也是我們在圖像建模時候會用到的約束。
  6. 哈希學習,通過哈希編碼,我們能夠實現對數據表示以及數據相似度求取的快速計算。而且哈希表示能夠完成數據壓縮等一些非常難的任務。

以上都是傳統的提取圖像特徵的方式。

  • 深度學習提取特徵

我們在對圖像進行分類的時候,如果分類效果越好就意味着網絡在進行特徵抽取和表示能力上會變得更強。對於深度學習而言,它其實本質上在解決計算機視覺任務的時候,其實就是在進行圖像特徵的提取,只是這些特徵會受到要解決的任務本身的約束。對於深度學習在人臉特徵表示的時候,通常架構會包括一個主幹網絡,這個主幹網絡就是CNN(ResNet、VGGNet、SENet等等),在進行卷積特徵提取之後,輸入的3*H1*W1維的圖片在經過特徵表示之後變成M*H2*W2維的feature maps。這裏H2和W2是經過下采樣之後的,M是多個卷積核的數量使得提取出了M個通道的feature map。在提取出特徵圖之後再通過一個FC層,將feature maps拉成一個向量,就睡扁平化處理。當然在此過程中會加入歸一化處理,將這個向量歸一化到0~1之間去。歸一化會使特徵空間加入約束。這個特徵向量實際上就是我們對輸入圖片的特徵表示,跟之前的直方圖是一個意思。我們通過這個特徵向量就能夠完成分類、迴歸、檢測等等這些任務。特徵向量能夠針對我們的任務完成對輸入圖像的一個非常好的,魯棒的一個表示。對於深度學習同之前的人爲的去設計特徵,或者去進行子空間優化學習這些特徵,相比而言它能夠去自己學習特徵。自己學習特徵就需要自己去學習網絡的參數,在學習好這些網絡的參數之後,就幫助網絡完成了對特徵的提取。目前對圖像特徵表示,深度學習是最主流的方法,而且提取出來的圖片特徵的效果會更好。我們在進行人臉匹配的時候,會將人臉匹配問題分成兩部分,一個是用深度學習去提取人臉特徵,將一副人臉圖像轉化成一個向量,然後再去對人臉向量之間的相似度進行判斷,也就是度量

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章