from time import time from io import BytesIO import torch import streamlit as st import streamlit.components.v1 as components import numpy as np import torch import logging from os import environ from transformers import OwlViTProcessor, OwlViTForObjectDetection from bot import Bot, Message from parse import parse from clickhouse_connect import get_client from classifier import Classifier, prompt2vec, tune, SplitLayer from query_model import simple_query, topk_obj_query, rev_query from card_model import card, obj_card, style from box_utils import postprocess environ["TOKENIZERS_PARALLELISM"] = "true" OBJ_DB_NAME = "mqdb_demo.coco_owl_vit_b_32_objects" IMG_DB_NAME = "mqdb_demo.coco_owl_vit_b_32_images" MODEL_ID = "google/owlvit-base-patch32" DIMS = 512 qtime = 0 def build_model(name="google/owlvit-base-patch32"): """Model builder function Args: name (str, optional): Name for HuggingFace OwlViT model. Defaults to "google/owlvit-base-patch32". Returns: (model, processor): OwlViT model and its processor for both image and text """ device = "cpu" if torch.cuda.is_available(): device = "cuda" model = OwlViTForObjectDetection.from_pretrained(name).to(device) processor = OwlViTProcessor.from_pretrained(name) return model, processor @st.experimental_singleton(show_spinner=False) def init_owlvit(): """Initialize OwlViT Model Returns: model, processor """ model, processor = build_model(MODEL_ID) return model, processor @st.experimental_singleton(show_spinner=False) def init_db(): """Initialize the Database Connection Returns: meta_field: Meta field that records if an image is viewed or not client: Database connection object """ meta = [] r = parse("{http_pre}://{host}:{port}", st.secrets["DB_URL"]) client = get_client( host=r['host'], port=r['port'], user=st.secrets["USER"], password=st.secrets["PASSWD"], interface=r['http_pre'], ) return meta, client def refresh_index(): """Clean the session""" del st.session_state["meta"] st.session_state.meta = [] st.session_state.query_num = 0 logging.info(f"Refresh for '{st.session_state.meta}'") # Need to clear singleton function with streamlit API init_db.clear() # refresh session states st.session_state.meta, st.session_state.index = init_db() if "clf" in st.session_state: del st.session_state.clf if "xq" in st.session_state: del st.session_state.xq if "topk_img_id" in st.session_state: del st.session_state.topk_img_id def query(xq, exclude_list=None): """Query matched w.r.t a given vector In this part, we will retrieve A LOT OF data from the server, including TopK boxes and their embeddings, the counterpart of non-TopK boxes in TopK images. Args: xq (numpy.ndarray or list of floats): Query vector Returns: matches: list of Records object. Keys referrring to selected columns group by images. Exclude the user's viewlist. img_matches: list of Records object. Containing other non-TopK but hit objects among TopK images. side_matches: list of Records object. Containing REAL TopK objects disregard the user's view history """ attempt = 0 xq = xq xq = xq / np.linalg.norm(xq, axis=-1, ord=2, keepdims=True) status_bar = [st.empty(), st.empty()] status_bar[0].write("Retrieving Another TopK Images...") pbar = status_bar[1].progress(0) while attempt < 3: try: matches = topk_obj_query( st.session_state.index, xq, IMG_DB_NAME, OBJ_DB_NAME, exclude_list=exclude_list, topk=10, ) img_ids = [r["img_id"] for r in matches] if "topk_img_id" not in st.session_state: st.session_state.topk_img_id = img_ids status_bar[0].write("Retrieving TopK Images...") pbar.progress(25) o_matches = rev_query( st.session_state.index, xq, st.session_state.topk_img_id, IMG_DB_NAME, OBJ_DB_NAME, thresh=0.1, ) status_bar[0].write("Retrieving TopKs Objects...") pbar.progress(50) side_matches = simple_query( st.session_state.index, xq, IMG_DB_NAME, OBJ_DB_NAME, thresh=-1, topk=10, ) status_bar[0].write("Retrieving Non-TopK in Another TopK Images...") pbar.progress(75) if len(img_ids) > 0: img_matches = rev_query( st.session_state.index, xq, img_ids, IMG_DB_NAME, OBJ_DB_NAME, thresh=0.1, ) else: img_matches = [] status_bar[0].write("DONE!") pbar.progress(100) break except Exception as e: # force reload if we have trouble on connections or something else logging.warning(str(e)) st.session_state.meta, st.session_state.index = init_db() attempt += 1 matches = [] _ = [s.empty() for s in status_bar] if len(matches) == 0: logging.error(f"No matches found for '{OBJ_DB_NAME}'") return matches, img_matches, side_matches, o_matches @st.experimental_singleton(show_spinner=False) def init_random_query(): """Initialize a random query vector Returns: xq: a random vector """ xq = np.random.rand(1, DIMS) xq /= np.linalg.norm(xq, keepdims=True, axis=-1) return xq def submit(meta): """Tune the model w.r.t given score from user.""" # Only updating the meta if the train button is pressed st.session_state.meta.extend(meta) st.session_state.step += 1 matches = st.session_state.matched_boxes X, y = list( zip( *( ( v[0], st.session_state.text_prompts.index(st.session_state[f"label-{i}"]), ) for i, v in matches.items() ) ) ) st.session_state.xq = tune( st.session_state.clf, X, y, iters=int(st.session_state.iters) ) ( st.session_state.matches, st.session_state.img_matches, st.session_state.side_matches, st.session_state.o_matches, ) = query(st.session_state.xq, st.session_state.meta) # st.set_page_config(layout="wide") # To hack the streamlit style we define our own style. # Boxes are drawn in SVGs. st.write(style(), unsafe_allow_html=True) bot = Bot(app_name="HF OwlViT", enabled=True, bot_key=st.secrets['BOT_KEY']) try: with st.spinner("Connecting DB..."): st.session_state.meta, st.session_state.index = init_db() with st.spinner("Loading Models..."): # Initialize model model, tokenizer = init_owlvit() # If its a fresh start... (query not set) if "xq" not in st.session_state: with st.container(): st.title("Object Detection Safari") start = [st.empty() for _ in range(8)] start[0].info( """ We extracted boxes from **287,104** images in COCO Dataset, including its train / val / test / unlabeled images, collecting **165,371,904 boxes** which are then filtered with common prompts. You can search with almost any words or phrases you can think of. Please enjoy your journey of an adventure to COCO. """ ) prompt = start[1].text_input( "Prompt:", value="", placeholder="Examples: football, billboard, stop sign, watermark ...", ) with start[2].container(): st.write( "You can search with multiple keywords. Plese separate with commas but with no space." ) st.write("For example: `cat,dog,tree`") st.markdown( """

Don\'t know what to search? Try Random!

""", unsafe_allow_html=True, ) upld_model = start[4].file_uploader( "Or you can upload your previous run!", type="onnx" ) upld_btn = start[5].button( "Use Loaded Weights", disabled=upld_model is None, on_click=refresh_index ) with start[3]: col = st.columns(8) has_no_prompt = len(prompt) == 0 and upld_model is None prompt_xq = col[6].button( "Prompt", disabled=len(prompt) == 0, on_click=refresh_index ) random_xq = col[7].button( "Random", disabled=not has_no_prompt, on_click=refresh_index ) matches = [] img_matches = [] if random_xq: xq = init_random_query() st.session_state.xq = xq prompt = "unknown" st.session_state.text_prompts = prompt.split(",") + ["none"] _ = [elem.empty() for elem in start] t0 = time() ( st.session_state.matches, st.session_state.img_matches, st.session_state.side_matches, st.session_state.o_matches, ) = query(st.session_state.xq, st.session_state.meta) t1 = time() qtime = (t1 - t0) * 1000 elif prompt_xq or upld_btn: if upld_model is not None: import onnx from onnx import numpy_helper _model = onnx.load(upld_model) st.session_state.text_prompts = [ node.name for node in _model.graph.output ] + ["none"] weights = _model.graph.initializer xq = numpy_helper.to_array(weights[0]).T assert ( xq.shape[0] == len(st.session_state.text_prompts) - 1 and xq.shape[1] == DIMS ) st.session_state.xq = xq _ = [elem.empty() for elem in start] else: logging.info(f"Input prompt is {prompt}") st.session_state.text_prompts = prompt.split(",") + ["none"] input_ids, xq = prompt2vec( st.session_state.text_prompts[:-1], model, tokenizer ) st.session_state.xq = xq _ = [elem.empty() for elem in start] t0 = time() ( st.session_state.matches, st.session_state.img_matches, st.session_state.side_matches, st.session_state.o_matches, ) = query(st.session_state.xq, st.session_state.meta) t1 = time() qtime = (t1 - t0) * 1000 # If its not a fresh start (query is set) if "xq" in st.session_state: o_matches = st.session_state.o_matches side_matches = st.session_state.side_matches img_matches = st.session_state.img_matches matches = st.session_state.matches # initialize classifier if "clf" not in st.session_state: st.session_state.clf = Classifier(st.session_state.index, OBJ_DB_NAME, st.session_state.xq) st.session_state.step = 0 if qtime > 0: st.info( "Query done in {0:.2f} ms and returned {1:d} images with {2:d} boxes".format( qtime, len(matches), sum( [ len(m["box_id"]) + len(im["box_id"]) for m, im in zip(matches, img_matches) ] ), ) ) lnprob = torch.nn.Linear(st.session_state.xq.shape[1], st.session_state.xq.shape[0], bias=False) lnprob.weight = torch.nn.Parameter(st.session_state.clf.weight) # export the model into executable ONNX st.session_state.dnld_model = BytesIO() torch.onnx.export( torch.nn.Sequential(lnprob, SplitLayer()), torch.zeros([1, len(st.session_state.xq[0])]), st.session_state.dnld_model, input_names=["input"], output_names=st.session_state.text_prompts[:-1], ) dnld_nam = st.text_input( "Download Name:", f'{("_".join([i.replace(" ", "-") for i in st.session_state.text_prompts[:-1]]) if "text_prompts" in st.session_state else "model")}.onnx', max_chars=50, ) dnld_btn = st.download_button( "Download your classifier!", st.session_state.dnld_model, dnld_nam ) # build up a sidebar to display REAL TopK in DB # this will change during user's finetune. But sometime it would lead to bad results side_bar_len = min(240 // len(st.session_state.text_prompts), 120) with st.sidebar: with st.expander("Top-K Images"): with st.container(): boxes_w_img, _ = postprocess( o_matches, st.session_state.text_prompts, o_matches, agnostic_ratio=1-0.6**(st.session_state.step+1), class_ratio=1-0.2**(st.session_state.step+1) ) boxes_w_img = sorted(boxes_w_img, key=lambda x: x[4], reverse=True) for img_id, img_url, img_w, img_h, img_score, boxes in boxes_w_img: args = img_url, img_w, img_h, boxes st.write(card(*args), unsafe_allow_html=True) with st.expander("Top-K Objects", expanded=True): side_cols = st.columns(len(st.session_state.text_prompts[:-1])) for _cols, m in zip(side_cols, side_matches): with _cols.container(): for cx, cy, w, h, logit, img_url, img_w, img_h in zip( m["cx"], m["cy"], m["w"], m["h"], m["logit"], m["img_url"], m["img_w"], m["img_h"], ): st.write( "{:s}: {:.4f}".format( st.session_state.text_prompts[m["label"]], logit ) ) _html = obj_card( img_url, img_w, img_h, cx, cy, w, h, dst_len=side_bar_len ) components.html(_html, side_bar_len, side_bar_len) with st.container(): # Here let the user interact with batch labeling with st.form("batch", clear_on_submit=False): col = st.columns([1, 9]) # If there is nothing to show about if len(matches) <= 0: st.warning( "Oops! We didn't find anything relevant to your query! Pleas try another one :/" ) else: st.session_state.iters = st.slider( "Number of Iterations to Update", min_value=0, max_value=10, step=1, value=2, ) # No matter what happened the user wants a way back col[1].form_submit_button("Choose a new prompt", on_click=refresh_index) # If there are things to show if len(matches) > 0: with st.container(): prompt_labels = st.session_state.text_prompts # Post processing boxes regarding to their score, intersection boxes_w_img, meta = postprocess( matches, st.session_state.text_prompts, img_matches, agnostic_ratio=1-0.6**(st.session_state.step+1), class_ratio=1-0.2**(st.session_state.step+1) ) # Sort the result according to their relavancy boxes_w_img = sorted(boxes_w_img, key=lambda x: x[4], reverse=True) st.session_state.matched_boxes = {} # For each images in the retrieved images, DISPLAY for img_id, img_url, img_w, img_h, img_score, boxes in boxes_w_img: # prepare inputs for training st.session_state.matched_boxes.update({b[0]: b for b in boxes}) args = img_url, img_w, img_h, boxes # display boxes with st.expander( "{:s}: {:.4f}".format(img_id, img_score), expanded=True ): ind_b = 0 # 4 columns: (img, obj, obj, obj) img_row = st.columns([4, 2, 2, 2]) img_row[0].write(card(*args), unsafe_allow_html=True) # crop objects out of the original image for b in boxes: _id, cx, cy, w, h, label, logit, is_selected = b[:8] with img_row[1 + ind_b % 3].container(): st.write("{:s}: {:.4f}".format(label, logit)) # quite hacky: with streamlit components API _html = obj_card( img_url, img_w, img_h, *b[1:5], dst_len=120 ) components.html(_html, 120, 120) # the user will choose the right label of the given object st.selectbox( "Class", prompt_labels, index=prompt_labels.index(label), key=f"label-{_id}", ) ind_b += 1 col[0].form_submit_button("Train!", on_click=lambda: submit(meta)) except Exception as e: msg = Message() msg.content = str(e.with_traceback(None)) msg.type_hint = str(type(e).__name__) bot.incident(msg)