File size: 12,417 Bytes
d0e7c02
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
"""Tasks: what the model sees and answers. A task gives its examples, its slot layout for an arm,
the soft targets, and how a read turns back into a prediction and a score.

  agent   computer use: action + point (+ swipe end, key, scroll), AGUVIS steps + Wave-UI clicks
  box     an element's or object's box as centre + log size, Wave-UI elements + RefCOCO referents
  seg     a referred object's mask as a centre + K ray lengths, RefCOCO

The assistant turn is the slot layout, each slot ending in the placeholder:

  action: ___ x: ___ y: ___ x2: ___ y2: ___ key: ___ sdir: ___ samt: ___

and a slot's answer is the next-token distribution at the token before its placeholder.
"""

import math

from . import labels as L
from .data import aguvis, refcoco, waveui
from .geometry import box_iou, mask_iou, poly_mask, radial_decode, radial_encode
from .images import image_of, image_size
from .numbers import LOG_MIN, number_targets, one_hot, read_number, slots_for, unwarp, warp


def coarse_desc(arm, what):
    """How a number's slots are described in a system prompt."""
    return f"the tens digit (00-99) and then the fine code of {what}" if arm.coarse else f"the code of {what}"


class Task:
    name = ""
    numbers = {}  # number -> "lin" | "log"

    def layout(self, arm):
        raise NotImplementedError

    def system(self, arm):
        raise NotImplementedError

    def user(self, ex):
        raise NotImplementedError

    def number_of(self, slot):
        return slot[:-1] if slot.endswith("c") and slot[:-1] in self.numbers else slot

    def messages(self, ex, arm, assistant=None):
        return [{"role": "system", "content": [{"type": "text", "text": self.system(arm)}]},
                {"role": "user", "content": [{"type": "image", "image": image_of(ex)},
                                             {"type": "text", "text": self.user(ex)}]},
                {"role": "assistant", "content": [{"type": "text", "text": assistant if assistant is not None
                                                   else " ".join(f"{s}: ___" for s in self.layout(arm))}]}]

    def labels(self, arm, tok):
        out = {}
        for s in self.layout(arm):
            n = self.number_of(s)
            out[s] = L.DIGITS if (n in self.numbers and s.endswith("c") and n != s) else (
                arm.fine if n in self.numbers else self.categorical_labels(s, tok))
        return out

    def categorical_labels(self, slot, tok):
        raise KeyError(slot)

    def config(self, arm, tok):
        """Everything a reader needs to rebuild this task's prompt and labels (saved in config.json)."""
        return dict(slots=self.layout(arm), system=self.system(arm), numbers=self.numbers,
                    labels=self.labels(arm, tok))


class AgentTask(Task):
    """Computer use. Only the slots an action uses are trained on that example (MASK), so x2 is free
    to mean nothing on a click. Text arguments (what to type, the final answer) are not slots: the base
    model writes them."""
    name = "agent"
    numbers = {"x": "lin", "y": "lin", "x2": "lin", "y2": "lin"}
    MASK = {"click": ["x", "y"], "select": ["x", "y"], "swipe": ["x", "y", "x2", "y2"],
            "scroll": ["sdir", "samt"], "press": ["key"]}

    def layout(self, arm):
        return (["action"] + slots_for("x", arm) + slots_for("y", arm) + slots_for("x2", arm)
                + slots_for("y2", arm) + ["key", "sdir", "samt"])

    def system(self, arm):
        return ("You are a GUI agent. Given a screenshot, the task and the previous actions, answer the next step. "
                "action: " + ", ".join(aguvis.ACTIONS) + ". "
                f"For x, {coarse_desc(arm, 'the horizontal position of the point, from the left edge to the right edge')}; "
                f"for y, the same vertically from the top to the bottom. x2 and y2: the end point of a swipe, written "
                f"the same way. key: the key to press. sdir and samt: scroll direction and amount.")

    def user(self, ex):
        return ex["user"]

    def categorical_labels(self, slot, tok):
        return {"action": L.word_label_ids(tok, "action", aguvis.ACTIONS),
                "key": L.word_label_ids(tok, "key", aguvis.KEYS),
                "sdir": L.word_label_ids(tok, "sdir", aguvis.SDIR), "samt": L.DIGITS}[slot]

    def targets(self, ex, arm):
        a, g = ex["action"], ex["args"]
        tg = {"action": one_hot(len(aguvis.ACTIONS), aguvis.ACTIONS.index(a))}
        for n in self.MASK.get(a, []):
            if n in self.numbers:
                if g.get(n) is None:
                    continue
                w = g.get("box" if n.startswith("x") else "boy", 0.02)  # the target's width / height, if known
                tg.update(number_targets(n, g[n], 0.25 * w, arm))
            elif n == "key":
                tg["key"] = one_hot(len(aguvis.KEYS), aguvis.KEYS.index(g.get("key", "other")))
            elif n == "sdir" and g.get("sdir") in aguvis.SDIR:
                tg["sdir"] = one_hot(len(aguvis.SDIR), aguvis.SDIR.index(g["sdir"]))
            elif n == "samt" and g.get("samt") is not None:
                tg["samt"] = one_hot(10, int(g["samt"]))
        return tg

    def decode(self, lp, arm, how="default"):
        out = dict(action=aguvis.ACTIONS[int(lp["action"].argmax())],
                   x=read_number(lp, "x", arm, how), y=read_number(lp, "y", arm, how))
        if "x2" in lp:
            out.update(x2=read_number(lp, "x2", arm, how), y2=read_number(lp, "y2", arm, how))
        return out

    def score(self, pred, ex, thresh=0.14):
        """A step is right when the action is, and (for a click, select or swipe) the point is within
        0.14 of the target on each axis."""
        ok_a = pred["action"] == ex["action"]
        ok_p = True
        if ex["action"] in ("click", "select", "swipe"):
            ok_p = abs(pred["x"] - ex["args"]["x"]) <= thresh and abs(pred["y"] - ex["args"]["y"]) <= thresh
        return dict(action=float(ok_a), point=float(ok_p) if ex["action"] in ("click", "select", "swipe") else None,
                    step=float(ok_a and ok_p))

    def examples(self, n_agent, n_click, n_val):
        """(train, val): AGUVIS steps + Wave-UI clicks; val is the first n_val steps of the same pool
        (held out from this run; use aguvis.test_steps() to compare runs)."""
        pool = aguvis.steps(n_agent + n_val)
        val, train = pool[:n_val], pool[n_val:]
        return train + (waveui.clicks(n_click) if n_click else []), val


class BoxTask(Task):
    """An element's or object's box as centre + size; sizes log-warped."""
    name = "box"
    numbers = {"cx": "lin", "cy": "lin", "w": "log", "h": "log"}

    def layout(self, arm):
        return slots_for("cx", arm) + slots_for("cy", arm) + slots_for("w", arm) + slots_for("h", arm)

    def system(self, arm):
        return ("You locate an element or object in an image and give its bounding box as a centre and a size. "
                f"For cx, {coarse_desc(arm, 'the horizontal position of the centre, from the left edge to the right edge')}; "
                "for cy, the same vertically from the top to the bottom. "
                f"For w, {coarse_desc(arm, 'the box width on a scale from tiny to the whole image')}; "
                "for h, the same for the height.")

    def user(self, ex):
        return f"Element: {ex['instruction']}"

    def norm_box(self, ex):
        W, H = image_size(ex)
        x1, y1, x2, y2 = ex["box"]
        return [x1 / W, y1 / H, x2 / W, y2 / H] if not ex.get("norm") else list(ex["box"])

    def targets(self, ex, arm):
        x1, y1, x2, y2 = self.norm_box(ex)
        w, h = max(x2 - x1, LOG_MIN), max(y2 - y1, LOG_MIN)
        tg = {}
        tg.update(number_targets("cx", (x1 + x2) / 2, 0.12 * w, arm))
        tg.update(number_targets("cy", (y1 + y2) / 2, 0.12 * h, arm))
        tg.update(number_targets("w", warp(w, "log"), 0.02, arm))
        tg.update(number_targets("h", warp(h, "log"), 0.02, arm))
        return tg

    def decode(self, lp, arm, how="default"):
        cx, cy = read_number(lp, "cx", arm, how), read_number(lp, "cy", arm, how)
        w, h = unwarp(read_number(lp, "w", arm, how), "log"), unwarp(read_number(lp, "h", arm, how), "log")
        return [max(0.0, cx - w / 2), max(0.0, cy - h / 2), min(1.0, cx + w / 2), min(1.0, cy + h / 2)]

    def score(self, pred, ex):
        b = self.norm_box(ex)
        i = box_iou(pred, b)
        cx, cy = (pred[0] + pred[2]) / 2, (pred[1] + pred[3]) / 2
        return dict(iou=i, iou50=float(i >= 0.5), iou75=float(i >= 0.75),
                    center_in=float(b[0] <= cx <= b[2] and b[1] <= cy <= b[3]))

    def examples(self, n_train, n_val, n_photo=0, seed=0):
        """(train, val): Wave-UI element boxes, plus n_photo RefCOCO referent boxes (general objects).
        val: Wave-UI validation boxes, and val_photo: RefCOCO val referents (None without photos)."""
        train = waveui.boxes(n_train)
        val = waveui.val_boxes(n_val)
        val_photo = None
        if n_photo:
            train += refcoco.photo_boxes(n_photo, "train", seed + 100)
            val_photo = refcoco.photo_boxes(n_val, "validation", seed + 100)
        return train, val, val_photo


class SegTask(Task):
    """A referred object's mask as a centre + K ray lengths; rays are fractions of the image diagonal,
    log-warped."""
    name = "seg"

    def __init__(self, K=24):
        self.K = K
        self.numbers = {"cx": "lin", "cy": "lin", **{f"r{k}": "log" for k in range(K)}}

    def layout(self, arm):
        out = slots_for("cx", arm) + slots_for("cy", arm)
        for k in range(self.K):
            out += slots_for(f"r{k}", arm)
        return out

    def system(self, arm):
        K = self.K
        return ("You outline an object in an image. cx and cy: a point well inside the object; "
                f"r0 to r{K - 1}: how far the object reaches from that point along {K} directions, r0 pointing right "
                f"and the directions turning clockwise in equal steps. For cx, "
                f"{coarse_desc(arm, 'the horizontal position of the point, from the left edge to the right edge')}; "
                "for cy, the same vertically from the top to the bottom. For each r, "
                f"{coarse_desc(arm, 'the distance on a scale from tiny to the whole image diagonal')}.")

    def user(self, ex):
        return f"Object: {ex['sentence']}"

    def encoded(self, ex):
        """(cx, cy, d) in pixels, computed once per example."""
        if "radial" not in ex:
            W, H = ex["size"]
            ex["radial"] = radial_encode(poly_mask(ex["polys"], W, H), self.K)
        return ex["radial"]

    def targets(self, ex, arm):
        W, H = ex["size"]
        cx, cy, d = self.encoded(ex)
        bx1, by1, bx2, by2 = ex["bbox"]
        diag = math.hypot(W, H)
        tg = {}
        tg.update(number_targets("cx", cx / W, 0.12 * (bx2 - bx1) / W, arm))
        tg.update(number_targets("cy", cy / H, 0.12 * (by2 - by1) / H, arm))
        for k in range(self.K):
            tg.update(number_targets(f"r{k}", warp(d[k] / diag, "log"), 0.03, arm))
        return tg

    def decode(self, lp, arm, how="default"):
        cx, cy = read_number(lp, "cx", arm, how), read_number(lp, "cy", arm, how)
        r = [unwarp(read_number(lp, f"r{k}", arm, how), "log") for k in range(self.K)]
        return dict(cx=cx, cy=cy, r=r)

    def score(self, pred, ex):
        W, H = ex["size"]
        diag = math.hypot(W, H)
        m, pts = radial_decode(pred["cx"] * W, pred["cy"] * H, [r * diag for r in pred["r"]], W, H)
        gt = poly_mask(ex["polys"], W, H)
        iou, inter, union = mask_iou(m, gt)
        xs, ys = [p[0] for p in pts], [p[1] for p in pts]
        pb = [max(0, min(xs)), max(0, min(ys)), min(W, max(xs)), min(H, max(ys))]
        rec = box_iou([pb[0] / W, pb[1] / H, pb[2] / W, pb[3] / H],
                      [ex["bbox"][0] / W, ex["bbox"][1] / H, ex["bbox"][2] / W, ex["bbox"][3] / H])
        return dict(iou=float(iou), inter=inter, union=union, rec50=float(rec >= 0.5))

    def examples(self, n_train, n_val, seed=0):
        train = refcoco.examples(refcoco.rows("train"), n_train, seed)
        val = refcoco.examples(refcoco.rows("validation"), n_val, seed + 1, one_sentence=False)
        return train, val


TASKS = {"agent": AgentTask, "box": BoxTask, "seg": SegTask}