Compare commits

...

2 Commits

Author SHA1 Message Date
430ee4fac7 [classifier] working but not really... 2025-02-25 18:45:37 +01:00
f1a4f4dc84 [tracks] disentanglement 2025-02-25 09:15:10 +01:00
5 changed files with 179 additions and 118 deletions

View File

@ -22,6 +22,7 @@ class DetectionSceneSignals(QObject):
class DetectionTimelineSignals(QObject): class DetectionTimelineSignals(QObject):
windowMoved = Signal() windowMoved = Signal()
manualMove = Signal()
class DetectionSignals(QObject): class DetectionSignals(QObject):
hover = Signal((int, QPointF)) hover = Signal((int, QPointF))

View File

@ -57,7 +57,7 @@ class TrackingData(QObject):
return self._selection return self._selection
def setSelectionRange(self, col, start, stop): def setSelectionRange(self, col, start, stop):
logging.debug("Trackingdata: set selection range based on column %s to %.2f - %.2f", col, start, stop) logging.info("Trackingdata: set selection range based on column %s to %.2f - %.2f", col, start, stop)
col_indices = np.where((self[col] >= start) & (self[col] < stop))[0] col_indices = np.where((self[col] >= start) & (self[col] < stop))[0]
self._selection = self._indices[col_indices] self._selection = self._indices[col_indices]
if len(col_indices) < 1: if len(col_indices) < 1:
@ -77,10 +77,9 @@ class TrackingData(QObject):
ids : array-like ids : array-like
An array-like object containing the IDs to be set as user selections. An array-like object containing the IDs to be set as user selections.
""" """
print(ids) logging.debug("TrackingData.setSelection: %i number of ids", len(ids))
self._selection = self._find(ids) self._selection = self._find(ids)
self._selected_ids = ids self._selected_ids = ids
print(self._selection, self._selected_ids)
def setTrack(self, track_id:int, setUserLabeled:bool=True)-> None: def setTrack(self, track_id:int, setUserLabeled:bool=True)-> None:
"""Assign a new track_id to the user-selected detections """Assign a new track_id to the user-selected detections
@ -92,7 +91,7 @@ class TrackingData(QObject):
setUserLabeled : bool setUserLabeled : bool
Should the "userlabeled" state of the detections be set to True? Otherwise they will be left untouched. Should the "userlabeled" state of the detections be set to True? Otherwise they will be left untouched.
""" """
print(self._selection) logging.info("TrackingData: set track id %i for selection, set user-labeled status %s", track_id, str(setUserLabeled))
self["track"][self._selection] = track_id self["track"][self._selection] = track_id
if setUserLabeled: if setUserLabeled:
self.setUserLabeledStatus(True, True) self.setUserLabeledStatus(True, True)
@ -114,7 +113,7 @@ class TrackingData(QObject):
else: else:
self["userlabeled"][:] = new_status self["userlabeled"][:] = new_status
def revertAssignmentStatus(self): def revertUserLabeledStatus(self):
logging.debug("TrackingData:Un-setting assignment status of all data!") logging.debug("TrackingData:Un-setting assignment status of all data!")
self["userlabeled"][:] = False self["userlabeled"][:] = False

View File

@ -12,6 +12,17 @@ from fixtracks.utils.trackingdata import TrackingData
from IPython import embed from IPython import embed
class Detection():
def __init__(self, id, frame, track, position, orientation, length, userlabeled):
self.id = id
self.frame = frame
self.track = track
self.position = position
self.score = 0.0
self.angle = orientation
self.length = length
self.userlabeled = userlabeled
class WorkerSignals(QObject): class WorkerSignals(QObject):
error = Signal(str) error = Signal(str)
running = Signal(bool) running = Signal(bool)
@ -24,7 +35,8 @@ class ConsitencyDataLoader(QRunnable):
super().__init__() super().__init__()
self.signals = WorkerSignals() self.signals = WorkerSignals()
self.data = data self.data = data
self.bendedness = self.positions = None self.bendedness = None
self.positions = None
self.lengths = None self.lengths = None
self.orientations = None self.orientations = None
self.userlabeled = None self.userlabeled = None
@ -70,6 +82,18 @@ class ConsistencyWorker(QRunnable):
@Slot() @Slot()
def run(self): def run(self):
def get_detections(frame, indices):
detections = []
for i in indices:
if np.any(self.positions[i] < 0.1):
logging.debug("Encountered probably invalid position %s", str(self.positions[i]))
continue
d = Detection(i, frame, self.tracks[i], self.positions[i],
self.orientations[i], self.lengths[i],
self.userlabeled[i])
detections.append(d)
return detections
def needs_checking(original, new): def needs_checking(original, new):
res = False res = False
for n, o in zip(new, original): for n, o in zip(new, original):
@ -82,107 +106,135 @@ class ConsistencyWorker(QRunnable):
print("all detections would be assigned to one track!") print("all detections would be assigned to one track!")
return res return res
def assign_by_distance(f, p): def assign_by_distance(d):
t1_step = f - last_frame[0] t1_step = d.frame - last_detections[1].frame
t2_step = f - last_frame[1] t2_step = d.frame - last_detections[2].frame
if t1_step == 0 or t2_step == 0: if t1_step == 0 or t2_step == 0:
print(f"framecount is zero! current frame {f}, last frame {last_frame[0]} and {last_frame[1]}") print(f"framecount is zero! current frame {f}, last frame {last_detections[1].frame} and {last_detections[2].frame}")
distance_to_trackone = np.linalg.norm(d.position - last_detections[1].position)/t1_step
distance_to_trackone = np.linalg.norm(p - last_pos[0])/t1_step distance_to_tracktwo = np.linalg.norm(d.position - last_detections[2].position)/t2_step
distance_to_tracktwo = np.linalg.norm(p - last_pos[1])/t2_step
most_likely_track = np.argmin([distance_to_trackone, distance_to_tracktwo]) + 1 most_likely_track = np.argmin([distance_to_trackone, distance_to_tracktwo]) + 1
distances = np.zeros(2) distances = np.zeros(2)
distances[0] = distance_to_trackone distances[0] = distance_to_trackone
distances[1] = distance_to_tracktwo distances[1] = distance_to_tracktwo
return most_likely_track, distances return most_likely_track, distances
def assign_by_orientation(f, o): def assign_by_orientation(d):
t1_step = f - last_frame[0] t1_step = d.frame - last_detections[1].frame
t2_step = f - last_frame[1] t2_step = d.frame - last_detections[2].frame
orientationchange = (last_angle - o) orientationchanges = np.zeros(2)
orientationchange[orientationchange > 180] = 360 - orientationchange[orientationchange > 180] for i in [1, 2]:
orientationchange /= np.array([t1_step, t2_step]) orientationchanges[i-1] = (last_detections[i].angle - d.angle)
# orientationchange = np.abs(np.unwrap((last_angle - o)/np.array([t1_step, t2_step])))
most_likely_track = np.argmin(np.abs(orientationchange)) + 1 orientationchanges[orientationchanges > 180] = 360 - orientationchanges[orientationchanges > 180]
return most_likely_track, orientationchange orientationchanges /= np.array([t1_step, t2_step])
most_likely_track = np.argmin(np.abs(orientationchanges)) + 1
def assign_by_length(o): return most_likely_track, orientationchanges
length_difference = np.abs((last_length - o))
most_likely_track = np.argmin(length_difference) + 1 def assign_by_length(d):
return most_likely_track, length_difference length_differences = np.zeros(2)
length_differences[0] = np.abs((last_detections[1].length - d.length))
def do_assignment(f, indices, assignments): length_differences[1] = np.abs((last_detections[2].length - d.length))
for i, idx in enumerate(indices): most_likely_track = np.argmin(length_differences) + 1
self.tracks[idx] = assignments[i] return most_likely_track, length_differences
last_pos[assignments[i]-1] = pp[i]
last_frame[assignments[i]-1] = f unique_frames = np.unique(self.frames)
last_angle[assignments[i]-1] = self.orientations[idx] steps = int((len(unique_frames) - self._startframe) // 100)
last_length[assignments[i]-1] += ((self.lengths[idx] - last_length[assignments[i]-1])/processed)
last_pos = [self.positions[(self.tracks == 1) & (self.frames <= self._startframe)][-1],
self.positions[(self.tracks == 2) & (self.frames <= self._startframe)][-1]]
last_frame = [self.frames[(self.tracks == 1) & (self.frames <= self._startframe)][-1],
self.frames[(self.tracks == 2) & (self.frames <= self._startframe)][-1]]
last_angle = [self.orientations[(self.tracks == 1) & (self.frames <= self._startframe)][-1],
self.orientations[(self.tracks == 2) & (self.frames <= self._startframe)][-1]]
last_length = [self.lengths[(self.tracks == 1) & (self.frames <= self._startframe)][-1],
self.lengths[(self.tracks == 2) & (self.frames <= self._startframe)][-1]]
errors = 0 errors = 0
processed = 1 processed = 1
progress = 0 progress = 0
self._stoprequest = False self._stoprequest = False
maxframes = np.max(self.frames) last_detections = {1: None, 2: None, -1: None}
startframe = np.max(last_frame)
steps = int((maxframes - startframe) // 200)
for f in np.unique(self.frames[self.frames > startframe]): for f in unique_frames[unique_frames >= self._startframe]:
processed += 1
self.signals.currentframe.emit(f)
if self._stoprequest: if self._stoprequest:
break break
error = False
self.signals.currentframe.emit(f)
indices = np.where(self.frames == f)[0] indices = np.where(self.frames == f)[0]
pp = self.positions[indices] detections = get_detections(f, indices)
originaltracks = self.tracks[indices] done = [False, False]
dist_assignments = np.zeros_like(originaltracks) if len(detections) == 0:
angle_assignments = np.zeros_like(originaltracks) continue
length_assignments = np.zeros_like(originaltracks)
userlabeled = np.zeros_like(originaltracks) if len(detections) > 1 and np.any([detections[0].userlabeled, detections[1].userlabeled]):
distances = np.zeros((len(originaltracks), 2)) # more than one detection
if detections[0].userlabeled and detections[1].userlabeled:
if detections[0].track == detections[1].track:
error = True
logging.info("Classification error both detections in the same frame are assigned to the same track!")
elif detections[0].userlabeled and not detections[1].userlabeled:
detections[1].track = 1 if detections[0].track == 2 else 2
elif not detections[0].userlabeled and detections[1].userlabeled:
detections[0].track = 1 if detections[1].track == 2 else 2
if not error:
last_detections[detections[0].track] = detections[0]
last_detections[detections[1].track] = detections[1]
self.tracks[detections[0].id] = detections[0].track
self.tracks[detections[1].id] = detections[1].track
done[0] = True
done[1] = True
elif len(detections) == 1 and detections[0].userlabeled: # ony one detection and labeled
last_detections[detections[0].track] = detections[0]
done[0] = True
if np.sum(done) == len(detections):
continue
# if f == 2088:
# embed()
# return
if error and self._stoponerror:
self.signals.error.emit("Classification error both detections in the same frame are assigned to the same track!")
break
dist_assignments = np.zeros(2, dtype=int)
orientation_assignments = np.zeros_like(dist_assignments)
length_assignments = np.zeros_like(dist_assignments)
distances = np.zeros((2, 2))
orientations = np.zeros_like(distances) orientations = np.zeros_like(distances)
lengths = np.zeros_like(distances) lengths = np.zeros_like(distances)
assignments = np.zeros((2, 2))
for i, d in enumerate(detections):
dist_assignments[i], distances[i, :] = assign_by_distance(d)
orientation_assignments[i], orientations[i,:] = assign_by_orientation(d)
length_assignments[i], lengths[i, :] = assign_by_length(d)
assignments[i, :] = dist_assignments # (dist_assignments * 10 + orientation_assignments + length_assignments) / 3
diffs = np.diff(assignments, axis=1)
error = False
temp = {}
message = ""
for i, d in enumerate(detections):
temp = {}
if diffs[i] == 0: # both are equally likely
d.track = -1
error = True
message = "Classification error both detections in the same frame are assigned to the same track!"
break
if diffs[i] < 0:
d.track = 1
else:
d.track = 2
self.tracks[d.id] = d.track
if d.track not in temp:
temp[d.track] = d
else:
error = True
message = "Double assignment to the same track!"
break
for i, (idx, p) in enumerate(zip(indices, pp)): if not error:
if self.userlabeled[idx]: for k in temp:
print("user") last_detections[temp[k].track] = temp[k]
userlabeled[i] = True
last_pos[originaltracks[i]-1] = pp[i]
last_frame[originaltracks[i]-1] = f
last_angle[originaltracks[i]-1] = self.orientations[idx]
last_length[originaltracks[i]-1] += ((self.lengths[idx] - last_length[originaltracks[i]-1]) / processed)
continue
dist_assignments[i], distances[i, :] = assign_by_distance(f, p)
angle_assignments[i], orientations[i,:] = assign_by_orientation(f, self.orientations[idx])
length_assignments[i], lengths[i, :] = assign_by_length(self.lengths[idx])
if np.any(userlabeled):
continue
# check (re) assignment, update, and proceed
if not needs_checking(originaltracks, dist_assignments):
do_assignment(f, indices, dist_assignments)
else: else:
if not (np.all(length_assignments == 1) or np.all(length_assignments == 2)): # if I find a solution by body length logging.info("frame %i: Cannot decide who is who! %s", f, message)
logging.debug("frame %i: Decision based on body length", f) for idx in indices:
do_assignment(f, indices, length_assignments) self.tracks[idx] = -1
elif not (np.all(angle_assignments == 1) or np.all(angle_assignments == 2)): # else there is a solution based on orientation errors += 1
logging.info("frame %i: Decision based on orientation", f) if self._stoponerror:
do_assignment(f, indices, angle_assignments) self.signals.error.emit(message)
else: break
logging.info("frame %i: Cannot decide who is who") processed += 1
for idx in indices:
self.tracks[idx] = -1
errors += 1
if self._stoponerror:
break
if steps > 0 and f % steps == 0: if steps > 0 and f % steps == 0:
progress += 1 progress += 1
@ -481,18 +533,25 @@ class ConsistencyClassifier(QWidget):
self._all_scores = self._dataworker.scores self._all_scores = self._dataworker.scores
self._frames = self._dataworker.frames self._frames = self._dataworker.frames
self._tracks = self._dataworker.tracks self._tracks = self._dataworker.tracks
self._dataworker = None
if np.sum(self._userlabeled) < 1:
logging.error("ConsistencyTracker: I need at least 1 user-labeled frame to start with!")
self.setEnabled(False)
else:
t1_userlabeled = self._frames[self._userlabeled & (self._tracks == 1)]
t2_userlabeled = self._frames[self._userlabeled & (self._tracks == 2)]
max_startframe = np.min([t1_userlabeled[-1], t2_userlabeled[-1]])
min_startframe = np.max([t1_userlabeled[0], t2_userlabeled[0]])
self._maxframes = np.max(self._frames) self._maxframes = np.max(self._frames)
# FIXME the following line causes an error when there are no detections in the range
min_frame = max([self._frames[self._tracks == 1][0], self._frames[self._tracks == 2][0]]) + 1
self._maxframeslabel.setText(str(self._maxframes)) self._maxframeslabel.setText(str(self._maxframes))
self._startframe_spinner.setMinimum(min_frame) self._startframe_spinner.setMinimum(min_startframe)
self._startframe_spinner.setMaximum(self._frames[-1]) self._startframe_spinner.setMaximum(max_startframe)
self._startframe_spinner.setValue(self._frames[0] + 1) self._startframe_spinner.setValue(min_startframe)
self._startframe_spinner.setSingleStep(20)
self._startbtn.setEnabled(True) self._startbtn.setEnabled(True)
self._assignedlabel.setText("0") self._assignedlabel.setText("0")
self._errorlabel.setText("0") self._errorlabel.setText("0")
self._dataworker = None self.setEnabled(True)
self.setEnabled(True)
@Slot(float) @Slot(float)
def on_progress(self, value): def on_progress(self, value):
@ -607,14 +666,14 @@ def main():
import pickle import pickle
from fixtracks.info import PACKAGE_ROOT from fixtracks.info import PACKAGE_ROOT
datafile = PACKAGE_ROOT / "data/merged2.pkl" datafile = PACKAGE_ROOT / "data/merged_small_beginning.pkl"
with open(datafile, "rb") as f: with open(datafile, "rb") as f:
df = pickle.load(f) df = pickle.load(f)
data = TrackingData() data = TrackingData(as_dict(df))
data.setData(as_dict(df))
coords = data.coordinates() coords = data.coordinates()
cogs = data.centerOfGravity()
userlabeled = data["userlabeled"]
app = QApplication([]) app = QApplication([])
window = QWidget() window = QWidget()
window.setMinimumSize(200, 200) window.setMinimumSize(200, 200)
@ -624,7 +683,7 @@ def main():
# else: # else:
w = ClassifierWidget() w = ClassifierWidget()
w.setData(data) w.setData(data)
w.size_classifier.setCoordinates(coords) # w.size_classifier.setCoordinates(coords)
layout = QVBoxLayout() layout = QVBoxLayout()
layout.addWidget(w) layout.addWidget(w)

View File

@ -30,7 +30,7 @@ class Window(QGraphicsRectItem):
def setWindowX(self, newx): def setWindowX(self, newx):
logging.debug("timeline.window: set position to %.3f", newx) logging.debug("timeline.window: set position to %.3f", newx)
self.setX(newx) self.setX(newx)
self.signals.windowMoved.emit() # self.signals.windowMoved.emit()
def setWindowWidth(self, newwidth): def setWindowWidth(self, newwidth):
logging.debug("timeline.window: update window width to %f", newwidth) logging.debug("timeline.window: update window width to %f", newwidth)
@ -38,7 +38,7 @@ class Window(QGraphicsRectItem):
r = self.rect() r = self.rect()
r.setWidth(newwidth) r.setWidth(newwidth)
self.setRect(r) self.setRect(r)
self.signals.windowMoved.emit() # self.signals.windowMoved.emit()
def setWindow(self, newx:float, newwidth:float): def setWindow(self, newx:float, newwidth:float):
""" """
@ -58,7 +58,7 @@ class Window(QGraphicsRectItem):
r = self.rect() r = self.rect()
self.setRect(newx, r.y(), self._width, r.height()) self.setRect(newx, r.y(), self._width, r.height())
self.update() self.update()
self.signals.windowMoved.emit() # self.signals.windowMoved.emit()
def mouseMoveEvent(self, event): def mouseMoveEvent(self, event):
super().mouseMoveEvent(event) super().mouseMoveEvent(event)
@ -77,7 +77,7 @@ class Window(QGraphicsRectItem):
if r.y() != self._y: if r.y() != self._y:
self.setY(self._y) self.setY(self._y)
super().mouseReleaseEvent(event) super().mouseReleaseEvent(event)
self.signals.windowMoved.emit() self.signals.manualMove.emit()
def hoverEnterEvent(self, event): def hoverEnterEvent(self, event):
super().hoverEnterEvent(event) super().hoverEnterEvent(event)
@ -116,7 +116,7 @@ class DetectionTimeline(QWidget):
font.setBold(True) font.setBold(True)
self._window = Window(0, 0, 100, 60, window_pen, transparent_brush) self._window = Window(0, 0, 100, 60, window_pen, transparent_brush)
self._window.signals.windowMoved.connect(self.on_windowMoved) self._window.signals.manualMove.connect(self.on_windowMoved)
self._scene = QGraphicsScene(QRectF(0, 0, self._total_width, 85.)) self._scene = QGraphicsScene(QRectF(0, 0, self._total_width, 85.))
self._scene.setBackgroundBrush(self._bg_brush) self._scene.setBackgroundBrush(self._bg_brush)

View File

@ -31,6 +31,7 @@ class FixTracks(QWidget):
self._currentWindowPos = 0 # in frames self._currentWindowPos = 0 # in frames
self._currentWindowWidth = 0 # in frames self._currentWindowWidth = 0 # in frames
self._maxframes = 0 self._maxframes = 0
self._manualmove = False
self._data = None self._data = None
self._detectionView = DetectionView() self._detectionView = DetectionView()
@ -161,9 +162,10 @@ class FixTracks(QWidget):
def update(self): def update(self):
start_frame = self._currentWindowPos start_frame = self._currentWindowPos
stop_frame = start_frame + self._currentWindowWidth stop_frame = start_frame + self._currentWindowWidth
self._timeline.setWindow(start_frame / self._maxframes,
self._currentWindowWidth/self._maxframes)
logging.debug("Tracks:update: Updating View for detection range %i, %i frames", start_frame, stop_frame) logging.debug("Tracks:update: Updating View for detection range %i, %i frames", start_frame, stop_frame)
self._data.setSelectionRange("frame", start_frame, stop_frame) self._data.setSelectionRange("frame", start_frame, stop_frame)
self._controls_widget.setWindow(start_frame, stop_frame) self._controls_widget.setWindow(start_frame, stop_frame)
kp = self._keypointcombo.currentText().lower() kp = self._keypointcombo.currentText().lower()
kpi = -1 if "center" in kp else int(kp) kpi = -1 if "center" in kp else int(kp)
@ -208,11 +210,11 @@ class FixTracks(QWidget):
self._saveBtn.setEnabled(True) self._saveBtn.setEnabled(True)
self._currentWindowPos = 0 self._currentWindowPos = 0
self._currentWindowWidth = self._windowspinner.value() self._currentWindowWidth = self._windowspinner.value()
self._maxframes = self._data.max("frame") self._maxframes = np.max(self._data["frame"])
self.populateKeypointCombo(self._data.numKeypoints()) self.populateKeypointCombo(self._data.numKeypoints())
self._timeline.setData(self._data) self._timeline.setData(self._data)
self._timeline.setWindow(self._currentWindowPos / self._maxframes, # self._timeline.setWindow(self._currentWindowPos / self._maxframes,
self._currentWindowWidth / self._maxframes) # self._currentWindowWidth / self._maxframes)
self._detectionView.setData(self._data) self._detectionView.setData(self._data)
self._classifier.setData(self._data) self._classifier.setData(self._data)
self.update() self.update()
@ -276,7 +278,7 @@ class FixTracks(QWidget):
def on_revertUserFlags(self): def on_revertUserFlags(self):
logging.debug("Tracks:revert ALL UserFlags and track assignments") logging.debug("Tracks:revert ALL UserFlags and track assignments")
self._data.revertAssignmentStatus() self._data.revertUserLabeledStatus()
self._data.revertTrackAssignments() self._data.revertTrackAssignments()
self._timeline.update() self._timeline.update()
self.update() self.update()
@ -289,8 +291,10 @@ class FixTracks(QWidget):
def on_windowChanged(self): def on_windowChanged(self):
logging.debug("Tracks:Timeline reports window change ") logging.debug("Tracks:Timeline reports window change ")
self._currentWindowPos = np.round(self._timeline.rangeStart * self._maxframes) if not self._manualmove:
self.update() self._currentWindowPos = np.round(self._timeline.rangeStart * self._maxframes)
self.update()
self._manualmove = False
def on_windowSizeChanged(self, value): def on_windowSizeChanged(self, value):
"""Reacts on the user window-width selection. Selection is done in the unit of frames. """Reacts on the user window-width selection. Selection is done in the unit of frames.
@ -306,8 +310,7 @@ class FixTracks(QWidget):
self._controls_widget.setSelectedTracks(None) self._controls_widget.setSelectedTracks(None)
def on_detectionsSelected(self, detections): def on_detectionsSelected(self, detections):
logging.debug("Tracks: Detections selected") logging.debug("Tracks: %i Detections selected", len(detections))
print(detections)
tracks = np.zeros(len(detections), dtype=int) tracks = np.zeros(len(detections), dtype=int)
ids = np.zeros_like(tracks) ids = np.zeros_like(tracks)
frames = np.zeros_like(tracks) frames = np.zeros_like(tracks)
@ -325,12 +328,11 @@ class FixTracks(QWidget):
self._controls_widget.setSelectedTracks(tracks) self._controls_widget.setSelectedTracks(tracks)
self._skeleton.clear() self._skeleton.clear()
self._skeleton.addSkeletons(coordinates, ids, frames, tracks, QBrush(QColor(10, 255, 65, 255))) self._skeleton.addSkeletons(coordinates, ids, frames, tracks, QBrush(QColor(10, 255, 65, 255)))
self.update()
def moveWindow(self, stepsize): def moveWindow(self, stepsize):
step = np.round(stepsize * (self._currentWindowWidth)) logging.info("Tracks.moveWindow: move window with stepsize %.2f", stepsize)
new_start_frame = self._currentWindowPos + step self._manualmove = True
self._timeline.setWindowPos(new_start_frame / self._maxframes) new_start_frame = self._currentWindowPos + np.round(stepsize * self._currentWindowWidth)
self._currentWindowPos = new_start_frame self._currentWindowPos = new_start_frame
self._controls_widget.setSelectedTracks(None) self._controls_widget.setSelectedTracks(None)
self.update() self.update()