# Circlevis
Circlevis is the replay viewer (aka visualizer) in [Circleguard](https://github.com/circleguard/circleguard). It was split off into its own repository to allow other projects to use it, should they so choose.
Circlevis is a [pyqt](https://pypi.org/project/PyQt6/) widget.
## Installation
Circlevis can be installed from pip:
```bash
pip install circlevis
```
## Usage
Circlevis can be used in two ways:
### VisualizerApp
The easiest way is to instantiate a `VisualizerApp`, which subclasses `QApplication` so you don't have to create a main application yourself. This is best for quick visualization, when you only want to open circlevis and nothing else.
```python
from circleguard import *
from circlevis import VisualizerApp, BeatmapInfo
cg = Circleguard("key")
r = ReplayMap(509610, 6304246)
# replays must be loaded before passed to the visualizer
cg.load(r)
# BeatmapInfo tells circlevis how it should load the beatmap before it displays
# it. You can pass either a map id (in which case circlevis will download the map
# from osu!'s servers) or a path to a .osu file (in which case circlevis will
# load the beatmap from that file).
# If you don't want any beatmap to be displayed, instantiate an empty BeatmapInfo
# (bm = BeatmapInfo()) and pass that to the visualizer.
bm = BeatmapInfo(map_id=r.map_id)
app = VisualizerApp(bm, replays=[r])
# this calls qt's `exec` function, which shows the application and enters the
# gui run loop, blocking any code after this call.
app.exec()
```
You can also visualize only a map, without any replay:
```python
from circlevis import VisualizerApp, BeatmapInfo
bm = BeatmapInfo(map_id=509610)
app = VisualizerApp(bm)
app.exec()
```
### Visualizer
If you want to integrate the visualizer into an existing project (which already has its own `QApplication`), you should instead instantiate the `Visualizer` class. `Visualizer` subclasses `QMainWindow` instead of `QApplication` and can be used like any other widget.
```python
from circleguard import *
from circlevis import Visualizer, BeatmapInfo
cg = Circleguard("key")
r = ReplayMap(509610, 6304246)
cg.load(r)
bm = BeatmapInfo(map_id=r.map_id)
visualizer_window = Visualizer(bm, replays=[r])
visualizer_window.show()
# or do something fancy with it instead of showing it immediately
```
### Other Arguments
Both `VisualizerApp` and `Visualizer` can take several optional arguments:
* `events` - a list of timestamps (in ms). If a frame with that timestamp is found in the replay, it is colored gold
* `library` - A [slider](https://github.com/llllllllll/slider) `Library` class, which will be used instead of creating a new one if passed
* `speeds` - a list of possible speeds the visualizer can play at. These can be switched between in real time with the speed up or speed down icons on the visualizer, or by pressing the up or down keys
* `start_speed` - which speed to start playback at. This value must be in `speeds`
* `paint_info` - whether to draw information about the map and replays in the upper left hand corner
## Classifier
Circlevis also provides a `Classifier` class, which builds on the visualizer to provide an easy way to batch classify replays one at a time. For instance, imagine you want to go through a map's leaderboard and assign a "cursordance score" to each replay, depending on how often the user cursordanced. The classifier will show you the first replay and wait for you to press a number key that assigns a cursordance score to that replay. When you do so, it saves the score and shows the next replay. Repeat until all replays are classified.
To use, you need a list of hotkeys that you will use to control the classification of the replays, a circleguard instance, and a list of Replay instances. Here's an example for the aforementioned "cursordance scoring" use case, where you can assign replays a score from 1 to 10:
```python
from collections import defaultdict
from circleguard import Circleguard
from circlevis import Classifier, ClassifierHotkey
cg = Circleguard("api_key")
class JudgeClassifier(Classifier):
def __init__(self, replays, cg):
self.scores = defaultdict(list)
hotkeys = [
ClassifierHotkey(Qt.Key.Key_1, lambda r: self.assign_score(1, r)),
ClassifierHotkey(Qt.Key.Key_2, lambda r: self.assign_score(2, r)),
ClassifierHotkey(Qt.Key.Key_3, lambda r: self.assign_score(3, r)),
ClassifierHotkey(Qt.Key.Key_4, lambda r: self.assign_score(4, r)),
ClassifierHotkey(Qt.Key.Key_5, lambda r: self.assign_score(5, r)),
ClassifierHotkey(Qt.Key.Key_6, lambda r: self.assign_score(6, r)),
ClassifierHotkey(Qt.Key.Key_7, lambda r: self.assign_score(7, r)),
ClassifierHotkey(Qt.Key.Key_8, lambda r: self.assign_score(8, r)),
ClassifierHotkey(Qt.Key.Key_9, lambda r: self.assign_score(9, r)),
ClassifierHotkey(Qt.Key.Key_0, lambda r: self.assign_score(10, r)),
]
super().__init__(replays, cg, hotkeys)
def assign_score(self, score, replay):
print(f"scoring {replay} as a {score}")
self.scores[score].append(replay)
# show the next replay now that we've scored this one
self.next_replay()
def done(self):
print(f"final scores: {self.scores}")
replays = cg.Map(221777, "1-10")
classifier = JudgeClassifier(replays, cg)
classifier.start()
```
### Programmatically Taking Screenshots
A cookbook recipe to save the current state of the visualizer at arbitrary timestamps in the map:
```python
from circleguard import *
from circlevis import *
cg = Circleguard("api_key")
r = cg.Map(2102290, "1", mods=Mod.HD, load=True)[0]
bm = BeatmapInfo(map_id=r.map_id)
screenshot_times = [727, 8000, 15214]
class ScreenshotVisualizer(VisualizerApp):
def on_load(self):
self.pause()
for i, t in enumerate(screenshot_times):
self.seek_to(t)
image = self.save_as_image()
image.save(f"image-{i}.png")
self.exit()
vis = ScreenshotVisualizer(bm, [r])
vis.exec()
```
If you want to take screenshots of multiple replays over multiple maps, it gets a bit trickier because we can only instantiate one `QApplication` over the lifetime of the program, even if we try to instantiate them in sequence. But we can still slightly abuse `Classifier` to achieve this:
```python
m = cg.Map(221777, "1-2", load=True)
# fill in with whatever screenshot times you want
screenshot_times = {
m[0]: [123, 234, 456],
m[1]: [10000, 20000, 30000]
}
class ScreenshotVisualizer(Visualizer):
def __init__(self, callback, screenshot_times, *args, **kwargs):
self.callback = callback
self.screenshot_times = screenshot_times
super().__init__(*args, **kwargs)
def on_load(self):
self.pause()
for t in self.screenshot_times:
self.seek_to(t)
image = self.save_as_image()
image.save(f"replay-{self.replays[0].username}-{t}.png")
self.callback()
class ScreenshotClassifier(Classifier):
def visualizer(self, bm, replay):
callback = lambda: self.next_replay()
times = screenshot_times[replay]
return ScreenshotVisualizer(callback, times, bm, [replay])
c = ScreenshotClassifier(m, cg, [])
c.start()
```
Raw data
{
"_id": null,
"home_page": "https://github.com/circleguard/circlevis",
"name": "circlevis",
"maintainer": "",
"docs_url": null,
"requires_python": "",
"maintainer_email": "",
"keywords": "osu!,python,Qt",
"author": "Liam DeVoe",
"author_email": "orionldevoe@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/71/fc/8ecf7b200b886f391404161f9baaa801a37c4600678fcb1ef9993ae490f1/circlevis-1.4.7.tar.gz",
"platform": null,
"description": "# Circlevis\n\nCirclevis is the replay viewer (aka visualizer) in [Circleguard](https://github.com/circleguard/circleguard). It was split off into its own repository to allow other projects to use it, should they so choose.\n\nCirclevis is a [pyqt](https://pypi.org/project/PyQt6/) widget.\n\n## Installation\n\nCirclevis can be installed from pip:\n\n```bash\npip install circlevis\n```\n\n## Usage\n\nCirclevis can be used in two ways:\n\n### VisualizerApp\n\nThe easiest way is to instantiate a `VisualizerApp`, which subclasses `QApplication` so you don't have to create a main application yourself. This is best for quick visualization, when you only want to open circlevis and nothing else.\n\n```python\nfrom circleguard import *\nfrom circlevis import VisualizerApp, BeatmapInfo\n\ncg = Circleguard(\"key\")\nr = ReplayMap(509610, 6304246)\n# replays must be loaded before passed to the visualizer\ncg.load(r)\n\n# BeatmapInfo tells circlevis how it should load the beatmap before it displays\n# it. You can pass either a map id (in which case circlevis will download the map\n# from osu!'s servers) or a path to a .osu file (in which case circlevis will\n# load the beatmap from that file).\n# If you don't want any beatmap to be displayed, instantiate an empty BeatmapInfo\n# (bm = BeatmapInfo()) and pass that to the visualizer.\nbm = BeatmapInfo(map_id=r.map_id)\napp = VisualizerApp(bm, replays=[r])\n# this calls qt's `exec` function, which shows the application and enters the\n# gui run loop, blocking any code after this call.\napp.exec()\n```\n\nYou can also visualize only a map, without any replay:\n\n```python\nfrom circlevis import VisualizerApp, BeatmapInfo\n\nbm = BeatmapInfo(map_id=509610)\napp = VisualizerApp(bm)\napp.exec()\n```\n\n### Visualizer\n\nIf you want to integrate the visualizer into an existing project (which already has its own `QApplication`), you should instead instantiate the `Visualizer` class. `Visualizer` subclasses `QMainWindow` instead of `QApplication` and can be used like any other widget.\n\n```python\nfrom circleguard import *\nfrom circlevis import Visualizer, BeatmapInfo\n\ncg = Circleguard(\"key\")\nr = ReplayMap(509610, 6304246)\ncg.load(r)\n\nbm = BeatmapInfo(map_id=r.map_id)\nvisualizer_window = Visualizer(bm, replays=[r])\nvisualizer_window.show()\n# or do something fancy with it instead of showing it immediately\n```\n\n### Other Arguments\n\nBoth `VisualizerApp` and `Visualizer` can take several optional arguments:\n\n* `events` - a list of timestamps (in ms). If a frame with that timestamp is found in the replay, it is colored gold\n* `library` - A [slider](https://github.com/llllllllll/slider) `Library` class, which will be used instead of creating a new one if passed\n* `speeds` - a list of possible speeds the visualizer can play at. These can be switched between in real time with the speed up or speed down icons on the visualizer, or by pressing the up or down keys\n* `start_speed` - which speed to start playback at. This value must be in `speeds`\n* `paint_info` - whether to draw information about the map and replays in the upper left hand corner\n\n## Classifier\n\nCirclevis also provides a `Classifier` class, which builds on the visualizer to provide an easy way to batch classify replays one at a time. For instance, imagine you want to go through a map's leaderboard and assign a \"cursordance score\" to each replay, depending on how often the user cursordanced. The classifier will show you the first replay and wait for you to press a number key that assigns a cursordance score to that replay. When you do so, it saves the score and shows the next replay. Repeat until all replays are classified.\n\nTo use, you need a list of hotkeys that you will use to control the classification of the replays, a circleguard instance, and a list of Replay instances. Here's an example for the aforementioned \"cursordance scoring\" use case, where you can assign replays a score from 1 to 10:\n\n```python\nfrom collections import defaultdict\nfrom circleguard import Circleguard\nfrom circlevis import Classifier, ClassifierHotkey\n\ncg = Circleguard(\"api_key\")\n\nclass JudgeClassifier(Classifier):\n def __init__(self, replays, cg):\n\n self.scores = defaultdict(list)\n\n hotkeys = [\n ClassifierHotkey(Qt.Key.Key_1, lambda r: self.assign_score(1, r)),\n ClassifierHotkey(Qt.Key.Key_2, lambda r: self.assign_score(2, r)),\n ClassifierHotkey(Qt.Key.Key_3, lambda r: self.assign_score(3, r)),\n ClassifierHotkey(Qt.Key.Key_4, lambda r: self.assign_score(4, r)),\n ClassifierHotkey(Qt.Key.Key_5, lambda r: self.assign_score(5, r)),\n ClassifierHotkey(Qt.Key.Key_6, lambda r: self.assign_score(6, r)),\n ClassifierHotkey(Qt.Key.Key_7, lambda r: self.assign_score(7, r)),\n ClassifierHotkey(Qt.Key.Key_8, lambda r: self.assign_score(8, r)),\n ClassifierHotkey(Qt.Key.Key_9, lambda r: self.assign_score(9, r)),\n ClassifierHotkey(Qt.Key.Key_0, lambda r: self.assign_score(10, r)),\n ]\n super().__init__(replays, cg, hotkeys)\n\n def assign_score(self, score, replay):\n print(f\"scoring {replay} as a {score}\")\n self.scores[score].append(replay)\n # show the next replay now that we've scored this one\n self.next_replay()\n\n def done(self):\n print(f\"final scores: {self.scores}\")\n\nreplays = cg.Map(221777, \"1-10\")\nclassifier = JudgeClassifier(replays, cg)\nclassifier.start()\n```\n\n### Programmatically Taking Screenshots\n\nA cookbook recipe to save the current state of the visualizer at arbitrary timestamps in the map:\n\n```python\nfrom circleguard import *\nfrom circlevis import *\n\ncg = Circleguard(\"api_key\")\n\nr = cg.Map(2102290, \"1\", mods=Mod.HD, load=True)[0]\nbm = BeatmapInfo(map_id=r.map_id)\nscreenshot_times = [727, 8000, 15214]\n\nclass ScreenshotVisualizer(VisualizerApp):\n def on_load(self):\n self.pause()\n for i, t in enumerate(screenshot_times):\n self.seek_to(t)\n image = self.save_as_image()\n image.save(f\"image-{i}.png\")\n self.exit()\n\nvis = ScreenshotVisualizer(bm, [r])\nvis.exec()\n```\n\nIf you want to take screenshots of multiple replays over multiple maps, it gets a bit trickier because we can only instantiate one `QApplication` over the lifetime of the program, even if we try to instantiate them in sequence. But we can still slightly abuse `Classifier` to achieve this:\n\n```python\nm = cg.Map(221777, \"1-2\", load=True)\n# fill in with whatever screenshot times you want\nscreenshot_times = {\n m[0]: [123, 234, 456],\n m[1]: [10000, 20000, 30000]\n}\n\nclass ScreenshotVisualizer(Visualizer):\n def __init__(self, callback, screenshot_times, *args, **kwargs):\n self.callback = callback\n self.screenshot_times = screenshot_times\n super().__init__(*args, **kwargs)\n\n def on_load(self):\n self.pause()\n for t in self.screenshot_times:\n self.seek_to(t)\n image = self.save_as_image()\n image.save(f\"replay-{self.replays[0].username}-{t}.png\")\n self.callback()\n\nclass ScreenshotClassifier(Classifier):\n def visualizer(self, bm, replay):\n callback = lambda: self.next_replay()\n times = screenshot_times[replay]\n return ScreenshotVisualizer(callback, times, bm, [replay])\n\nc = ScreenshotClassifier(m, cg, [])\nc.start()\n```\n",
"bugtrack_url": null,
"license": "",
"summary": "A Qt Widget for visualizing osu! beatmaps and replays.",
"version": "1.4.7",
"split_keywords": [
"osu!",
"python",
"qt"
],
"urls": [
{
"comment_text": "",
"digests": {
"md5": "6f67a374c7eb121c69548ac9b582be1a",
"sha256": "b379c8f0992724000773efd2e3d893f8efc423f7bbbe0ea5305753570d8548e9"
},
"downloads": -1,
"filename": "circlevis-1.4.7-py3-none-any.whl",
"has_sig": false,
"md5_digest": "6f67a374c7eb121c69548ac9b582be1a",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 144380,
"upload_time": "2022-12-11T00:02:41",
"upload_time_iso_8601": "2022-12-11T00:02:41.658633Z",
"url": "https://files.pythonhosted.org/packages/d0/26/6c60416e09d7fcdfe73960c4a600145ddfbf419f841482d496e4253fe01e/circlevis-1.4.7-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"md5": "fb09f6c271c88da584f388f885548c84",
"sha256": "b3c7381543067746eff22040e8122eb5721a0f4f0da2cc5a338dc8d3653c25b2"
},
"downloads": -1,
"filename": "circlevis-1.4.7.tar.gz",
"has_sig": false,
"md5_digest": "fb09f6c271c88da584f388f885548c84",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 139451,
"upload_time": "2022-12-11T00:02:44",
"upload_time_iso_8601": "2022-12-11T00:02:44.434606Z",
"url": "https://files.pythonhosted.org/packages/71/fc/8ecf7b200b886f391404161f9baaa801a37c4600678fcb1ef9993ae490f1/circlevis-1.4.7.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2022-12-11 00:02:44",
"github": true,
"gitlab": false,
"bitbucket": false,
"github_user": "circleguard",
"github_project": "circlevis",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "circlevis"
}